Skip to content

fix(receiver): harden prefetch and listener lifecycle - #645

Open
GefMar wants to merge 2 commits into
masterfrom
fix/receiver-prefetch-listener-lifecycle
Open

fix(receiver): harden prefetch and listener lifecycle#645
GefMar wants to merge 2 commits into
masterfrom
fix/receiver-prefetch-listener-lifecycle

Conversation

@GefMar

@GefMar GefMar commented Jul 15, 2026

Copy link
Copy Markdown
Member

Track delivery and prefetch capacity ownership across shutdown paths. Close pending listener reads, preserve late deliveries, and restore fail-fast transport error propagation.

Keep middleware accounting balanced, report discarded deliveries, and retain deferred lifecycle diagnostics when task group failures overlap.

Refs #528

im evaluated PR#630 and retained its valid requirement: the Receiver must not pull a message it cannot execute.

However, that implementation also introduces a broad retry loop that treats transport, middleware, and programming errors alike, while pending-read, iterator, late-delivery, and semaphore ownership remain implicit.

This PR instead fixes the underlying ownership invariant. Each admitted delivery explicitly owns capacity from broker read through callback completion, pending reads are cancelled and awaited, iterators are closed, and failures remain visible to the worker lifecycle owner.

Retry and backoff remain a separate policy rather than being introduced implicitly as part of the prefetch fix.

Track delivery and prefetch capacity ownership across shutdown paths.
Close pending listener reads, preserve late deliveries, and restore
fail-fast transport error propagation.

Keep middleware accounting balanced, report discarded deliveries, and
retain deferred lifecycle diagnostics when task group failures overlap.
@GefMar
GefMar force-pushed the fix/receiver-prefetch-listener-lifecycle branch from e15d278 to 4321e09 Compare July 15, 2026 17:14
@GefMar GefMar linked an issue Jul 15, 2026 that may be closed by this pull request
@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.06763% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.71%. Comparing base (ae2b788) to head (b6f07dc).

Files with missing lines Patch % Lines
taskiq/receiver/receiver.py 98.04% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #645      +/-   ##
==========================================
+ Coverage   81.29%   82.71%   +1.41%     
==========================================
  Files          69       69              
  Lines        2577     2742     +165     
==========================================
+ Hits         2095     2268     +173     
+ Misses        482      474       -8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@GefMar
GefMar marked this pull request as ready for review July 15, 2026 17:19
@GefMar
GefMar requested a review from s3rius July 15, 2026 17:19
Reject invalid prefetch values before receiver retries and retain delivery accounting until execution capacity is acquired.

Add deterministic coverage for CLI defaults, cancellation races, and listener lifecycle cleanup.

Refs #528
Comment on lines +443 to +445
"A Receiver listener lifecycle error was recorded before "
"the task group failed.",
exc_info=(type(error), error, error.__traceback__),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one can be simplified.

Suggested change
"A Receiver listener lifecycle error was recorded before "
"the task group failed.",
exc_info=(type(error), error, error.__traceback__),
"A Receiver listener lifecycle error was recorded before "
"the task group failed.\n%s" % error,
exc_info=True,

Comment thread taskiq/receiver/receiver.py Outdated
state.owns_delivery_slot = False
return late_delivery

async def _notify_prefetch_hook(self, hook_name: str) -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this function and whole on_prefetch_queue_add and on_prefetch_queue_remove. They are not part of middelware ABC and should not be used.

I actually missed that thing when opentelemetry support was added. Let's remove this functionality, since it's internal implementation details. I don't think those functions should have existed.

Comment on lines +469 to +470
self._record_listen_error(exc)
queue.put_nowait(_QueueSignal.DONE)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're running in a anyio group, I suppose you can simply do something like this:

Suggested change
self._record_listen_error(exc)
queue.put_nowait(_QueueSignal.DONE)
queue.put_nowait(_QueueSignal.DONE)
raise

finally:
queue.put_nowait(_QueueSignal.DONE)
finish_waiter.cancel()
await asyncio.gather(finish_waiter, return_exceptions=True)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
await asyncio.gather(finish_waiter, return_exceptions=True)
with contextlib.supress(asyncio.CanceledError):
await finish_waiter

Idk, IMO it's just more obvious what is going on with such code.

await self._settle_delivery_acquire(acquire_task)
raise

if finish_waiter in done or finish_event.is_set():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if finish_waiter in done or finish_event.is_set():
if finish_waiter.done() or finish_event.is_set():

I guess that'd work and will look a bit cleaner. Also, you won't need done.

) -> None:
"""Release capacity and instrumentation for abandoned queue entries."""
discarded_messages = 0
while True:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
while True:
while queue:

You can use this condition to simplify inner body of the loop. Because you won't need to check on QueueEmpty exception.

)
state.owns_delivery_slot = False
try:
await self._notify_prefetch_hook("on_prefetch_queue_add")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also be removed.

try:
await maybe_awaitable(hook())
except BaseException as exc:
if first_error is None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach ignores all the errors except the first one. I mean, it's fine, because you raise it anyway. But, I'd rather create an exception group.

I think it's safe to bump MPE to 3.11 and start using ExceptionGroup for such case. It'd be a good fit and reasonable upgrade.

Since python 3.10 will be completely discarded in 3 months.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

max-prefetch argument doesn't work as expected

2 participants