fix(actor): drain Actor::tasks JoinSet in run_async#96
Merged
dignifiedquire merged 1 commit intoMay 8, 2026
Merged
Conversation
The three streaming-reply actions push tasks into `Actor::tasks` via `spawn_local`, but `run_async` had no `join_next` arm, so completed task headers accumulated for the actor's lifetime and only got released by `abort_all()` at shutdown. Add the missing arm, matching the drain pattern already used by `LiveActor` and `GossipActor`.
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
Actor::tasksis aJoinSet<()>that the three streaming-reply actions (Action::ListAuthors,Action::ListReplicas,ReplicaAction::GetMany) push tasks into viaspawn_local, butrun_async'stokio::select!had nojoin_nextarm. Completed task headers accumulate for the lifetime of the actor and only get released whenabort_all()runs at shutdown.The fix adds the missing arm:
This matches the canonical drain pattern used everywhere else in the crate:
LiveActor::run_innerdrains three JoinSets in the sameselect!:running_sync_connect,running_sync_accept, anddownload_tasks(src/engine/live.rs).GossipActor::progressdrainsactive_taskswith the sameis_cancelled()skip onJoinError(src/engine/gossip.rs).Actor::taskswas the onlyJoinSetin the crate not following the pattern. There is also a pre-existing TODO insrc/engine/live.rsthat calls this out:The three
spawn_localsites are kept as-is.iter_to_irpcis a sync-iter → async-mpsc bridge that can stall on a slow consumer, so inlining it would block the action queue (notably theMAX_COMMIT_DELAYtimeout flush). The fix lives entirely in theselect!.A regression test is added next to the existing
open_closetest insrc/actor.rs::tests. It fires 1000 calls of each streaming-reply shape (3000 streamer tasks total), drains every reply stream to completion, then asserts thatActor::tasksshrinks back to a small bound. Without the new arm the residual is ~3000; with it the count drops to near zero within tens of milliseconds. A#[cfg(test)]-gatedAction::DebugTasksLenandSyncHandle::debug_tasks_lenprovide the introspection.Breaking Changes
None.
Notes & open questions
The test introspection is
#[cfg(test)]-only. Happy to instead expose a publicwait_idle()-style helper if you'd prefer that shape.Change checklist