Skip to content

Fix unbounded completed task retention in queue consumers - #1454

Merged
danielgerlag merged 1 commit into
danielgerlag:masterfrom
dajiaohuang:fix/bound-queue-consumer-tasks-20261016
Oct 25, 2026
Merged

danielgerlag merged 1 commit into
danielgerlag:masterfrom
dajiaohuang:fix/bound-queue-consumer-tasks-20261016

Conversation

@dajiaohuang

Copy link
Copy Markdown
Contributor

Describe the change
Fixes #1448: prevent the queue consumers' completed-task list from growing with the total number of items processed by the host.

A dummy in-memory workflow run through AddWorkflow / IWorkflowHost.StartWorkflow retained 222 workflow-consumer tasks after 200 sequential one-step workflows (plus preceding input/output, event and retry workflows). With this change the same run retained one task. This measures retained task references, not process RSS or OS handle usage.

Describe your implementation or design
Prune completed tasks under the existing _runningTasksLock before tracking each newly dispatched task. In-flight tasks remain tracked for the existing shutdown wait. When dispatch is idle the final bounded batch can remain; retention no longer grows with lifetime throughput. No continuation per item, public API, cancellation semantics, wait-handle lifecycle or shutdown rewrite is introduced.

Tests

  • New regression processes 100 completed items between two deliberately unfinished items; asserts completed tasks are pruned and both unfinished tasks remain tracked. It fails against the base implementation and passes with the fix.
  • .NET 10: all 46 integration tests pass, including the existing StopAsync scenario and the new regression.
  • .NET 10: all 62 unit tests pass.
  • Local dummy public-API fixture: fluent step input/output, while/if, event wait/publish, same-process in-memory host restart/recovery, retry and JSON definition loading all pass; task-retention assertion fails before and passes after the change.
  • git diff --check passes.

Limits: .NET 6/8 test targets and external database/provider suites were not run locally; no cross-process durability, distributed behavior or performance certification is claimed.

Breaking change
None; only completed task references are removed from the private list.

@danielgerlag danielgerlag left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Looks good — this correctly bounds _runningTasks to in-flight (+ at most one just-completed) work under the existing lock, without changing dispatch, shutdown wait, or public API.

The regression test is solid: holds two items in flight, processes 100 completed ones, and asserts completed tasks are pruned while both unfinished tasks remain.

One optional nit (non-blocking): when the dispatcher is idle after the last batch, completed tasks aren't pruned until the next dequeue. That's fine for the #1448 leak (growth with throughput); only matters if someone inspects the list after idle. Could also prune once in the shutdown path before WhenAll, but not required.

Approving.

@danielgerlag
danielgerlag merged commit 4e64768 into danielgerlag:master Oct 25, 2026
1 check passed
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.

QueueConsumer._runningTasks retains completed tasks for the life of the host

2 participants