Sweep untracked tasks and close thread_loop on ServiceThread shutdown#79
Open
wbarnha wants to merge 1 commit into
Open
Sweep untracked tasks and close thread_loop on ServiceThread shutdown#79wbarnha wants to merge 1 commit into
wbarnha wants to merge 1 commit into
Conversation
Fixes #54. ServiceThread._shutdown_thread() only cancels/awaits futures registered via this Service's own add_future()/@Service.task tracking. A driver library running inside the thread (e.g. a Kafka client's internal consumer poll/heartbeat loop) can schedule tasks directly on thread_loop, bypassing that tracking entirely. Left unswept, those tasks were simply abandoned the moment _serve() returned and thread_loop stopped running -- producing "coroutine ... was never awaited" / "Task was destroyed but it is pending" warnings at some later, unpredictable point (e.g. interpreter shutdown), since the abandoned task/coroutine object is only reported once garbage collected. mode.Worker already has an equivalent safety net for the main loop: _gather_all() sweeps every task on self.loop via all_tasks(), tracked or not, before closing it. ServiceThread had no equivalent for thread_loop, and never explicitly closed thread_loop at all. Add ServiceThread._gather_remaining_tasks(), called at the end of _serve()'s finally block (after _shutdown_thread(), while thread_loop is still running): cancel and await every task still on thread_loop, tracked or not, mirroring Worker._gather_all(). Also close thread_loop explicitly once _start_thread() returns -- but only when ServiceThread created it itself (thread_loop=None, the default); a caller who passed their own thread_loop= may expect to keep managing its lifecycle, so that case is left untouched. Verified with a minimal repro (no aiokafka/Kafka broker required): a subclass that schedules an untracked asyncio.ensure_future(...) task in on_thread_started() no longer leaves it pending after stop() -- confirmed by checking the task's own .done()/.cancelled() state directly, which is deterministic (a warnings-capture-based version of the same test was tried first and found unreliable, since the GC-triggered warning can fire outside any reasonably-sized capture window). Full suite passes on Python 3.11 and 3.13, ruff clean, --random-order stable across repeated runs, and a full faust-streaming/faust cross-check (tests/unit + tests/functional, 2128 passed/32 skipped/0 failed, unchanged from before this change) confirms no regression in the AIOKafkaConsumerThread-based driver that originally reported this issue. Assisted-by: Claude Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HgnKFtXZbCjXNoVNWa5JLd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixes #54.
ServiceThread._shutdown_thread()only cancels/awaits futures registered via this Service's ownadd_future()/@Service.tasktracking. A driver library running inside the thread (e.g. a Kafka client's internal consumer poll/heartbeat loop) can schedule tasks directly onthread_loop, bypassing that tracking entirely. Left unswept, those tasks were simply abandoned the moment_serve()returned andthread_loopstopped running — producingcoroutine ... was never awaited/Task was destroyed but it is pendingwarnings at some later, unpredictable point (e.g. interpreter shutdown), since the abandoned task/coroutine object is only reported once garbage collected.mode.Workeralready has an equivalent safety net for the main loop:_gather_all()sweeps every task onself.loopviaall_tasks(), tracked or not, before closing it.ServiceThreadhad no equivalent forthread_loop, and never explicitly closedthread_loopat all.Changes
ServiceThread._gather_remaining_tasks(), called at the end of_serve()'sfinallyblock (after_shutdown_thread(), whilethread_loopis still running): cancel and await every task still onthread_loop, tracked or not, mirroringWorker._gather_all().thread_loopexplicitly once_start_thread()returns — but only whenServiceThreadcreated it itself (thread_loop=None, the default). A caller who passed their ownthread_loop=may expect to keep managing its lifecycle, so that case is left untouched.Verification
asyncio.ensure_future(...)task inon_thread_started()no longer leaves it pending afterstop()— confirmed by checking the task's own.done()/.cancelled()state directly. (A warnings-capture-based version of this test was tried first and found unreliable, since the GC-triggered warning can fire outside any reasonably-sized capture window — the direct task-state assertion is what actually proves the fix.)--random-orderruns (relevant given this touches concurrency-sensitive shutdown code).faust-streaming/faustcross-check (tests/unit+tests/functional, 2128 passed / 32 skipped / 0 failed, unchanged from before this change) — confirms no regression in theAIOKafkaConsumerThread-based driver that originally reported this issue.🤖 Generated with Claude Code
https://claude.ai/code/session_01HgnKFtXZbCjXNoVNWa5JLd
Generated by Claude Code