Skip to content

Commit bcaecf1

Browse files
committed
Run post_send before in-place execution in InMemoryBroker
With InMemoryBroker(await_inplace=True) the middleware hooks fired in the order pre_send -> pre_execute -> task -> post_execute -> post_send, because kick() awaited the full receiver callback (pre_execute/task/post_execute) before control returned to the sender that invokes post_send. Now kick() schedules the in-place receiver as a task and returns immediately, and the task is awaited in a new _finish_kick() hook that runs right after post_send. This restores the expected ordering pre_send -> post_send -> pre_execute -> task -> post_execute while the task is still fully executed before kiq() returns. Fixes #586.
1 parent 9c339d6 commit bcaecf1

4 files changed

Lines changed: 86 additions & 2 deletions

File tree

taskiq/abc/broker.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,18 @@ async def kick(
237237
:param message: name of a task.
238238
"""
239239

240+
async def _finish_kick(self) -> None: # noqa: B027
241+
"""
242+
Hook that runs after a message is sent and ``post_send`` fired.
243+
244+
Most brokers send messages elsewhere and have nothing to do here,
245+
so this is a no-op by default. Brokers that execute tasks in-place
246+
(like :class:`~taskiq.InMemoryBroker` with ``await_inplace``) can
247+
override this method to await the in-place execution *after* the
248+
``post_send`` middleware hook has run, preserving the expected
249+
``pre_send -> post_send -> pre_execute -> ...`` ordering.
250+
"""
251+
240252
@abstractmethod
241253
def listen(self) -> AsyncGenerator[bytes | AckableMessage, None]:
242254
"""

taskiq/brokers/inmemory_broker.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ def __init__(
146146
)
147147
self.await_inplace = await_inplace
148148
self._running_tasks: set[asyncio.Task[Any]] = set()
149+
self._inplace_tasks: set[asyncio.Task[Any]] = set()
149150

150151
async def kick(self, message: BrokerMessage) -> None:
151152
"""
@@ -162,14 +163,37 @@ async def kick(self, message: BrokerMessage) -> None:
162163
raise UnknownTaskError(task_name=message.task_name)
163164

164165
receiver_cb = self.receiver.callback(message=message.message)
166+
task = asyncio.create_task(receiver_cb)
167+
165168
if self.await_inplace:
166-
await receiver_cb
169+
# Schedule the receiver as a task instead of awaiting it here so
170+
# that control returns to the sender and the ``post_send``
171+
# middleware hook fires *before* the task starts executing. The
172+
# task is then awaited in `_finish_kick`, right after `post_send`,
173+
# keeping the in-place execution while fixing the hook ordering.
174+
self._inplace_tasks.add(task)
175+
task.add_done_callback(self._inplace_tasks.discard)
167176
return
168177

169-
task = asyncio.create_task(receiver_cb)
170178
self._running_tasks.add(task)
171179
task.add_done_callback(self._running_tasks.discard)
172180

181+
async def _finish_kick(self) -> None:
182+
"""
183+
Await in-place task execution scheduled by `kick`.
184+
185+
This runs after the `post_send` middleware hook, so with
186+
``await_inplace=True`` the hook ordering becomes
187+
``pre_send -> post_send -> pre_execute -> task -> post_execute``
188+
while tasks are still fully executed before `kiq` returns.
189+
"""
190+
if not self._inplace_tasks:
191+
return
192+
to_await = list(self._inplace_tasks)
193+
self._inplace_tasks.clear()
194+
for task in to_await:
195+
await task
196+
173197
def listen(self) -> AsyncGenerator[bytes, None]:
174198
"""
175199
Inmemory broker cannot listen.

taskiq/kicker.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ async def kiq(
169169
if middleware.__class__.post_send != TaskiqMiddleware.post_send:
170170
await maybe_awaitable(middleware.post_send(message))
171171

172+
await self.broker._finish_kick() # noqa: SLF001
173+
172174
return AsyncTaskiqTask(
173175
task_id=message.task_id,
174176
result_backend=self.broker.result_backend,

tests/brokers/test_inmemory.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
import pytest
55

66
from taskiq import InMemoryBroker
7+
from taskiq.abc.middleware import TaskiqMiddleware
78
from taskiq.events import TaskiqEvents
9+
from taskiq.message import TaskiqMessage
10+
from taskiq.result import TaskiqResult
811
from taskiq.state import TaskiqState
912

1013

@@ -114,3 +117,46 @@ async def test_task() -> None:
114117
assert slept
115118
assert await task.is_ready()
116119
assert not broker._running_tasks
120+
121+
122+
async def test_inplace_middleware_order() -> None:
123+
"""post_send must fire before task execution with await_inplace=True."""
124+
events: list[str] = []
125+
126+
class OrderMiddleware(TaskiqMiddleware):
127+
async def pre_send(self, message: TaskiqMessage) -> TaskiqMessage:
128+
events.append("pre_send")
129+
return message
130+
131+
async def post_send(self, message: TaskiqMessage) -> None:
132+
events.append("post_send")
133+
134+
async def pre_execute(self, message: TaskiqMessage) -> TaskiqMessage:
135+
events.append("pre_execute")
136+
return message
137+
138+
async def post_execute(
139+
self,
140+
message: TaskiqMessage,
141+
result: "TaskiqResult[object]",
142+
) -> None:
143+
events.append("post_execute")
144+
145+
broker = InMemoryBroker(await_inplace=True).with_middlewares(OrderMiddleware())
146+
147+
@broker.task
148+
async def test_task() -> None:
149+
events.append("task")
150+
151+
task = await test_task.kiq()
152+
153+
# The task must be fully executed by the time kiq returns.
154+
assert await task.is_ready()
155+
assert not broker._inplace_tasks
156+
assert events == [
157+
"pre_send",
158+
"post_send",
159+
"pre_execute",
160+
"task",
161+
"post_execute",
162+
]

0 commit comments

Comments
 (0)