Skip to content

Track the actual item count in BlockingCollection - #133637

Open
aw0lid wants to merge 1 commit into
dotnet:mainfrom
aw0lid:fix/blockingcollection-iscompleted-cancellation
Open

Track the actual item count in BlockingCollection#133637
aw0lid wants to merge 1 commit into
dotnet:mainfrom
aw0lid:fix/blockingcollection-iscompleted-cancellation

Conversation

@aw0lid

@aw0lid aw0lid commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Description

BlockingCollection.IsCompleted currently uses _occupiedNodes.CurrentCount to determine whether the collection is empty.

However, _occupiedNodes is also used to synchronize consumer operations. This can cause IsCompleted to temporarily report true even though the collection still contains an item.

For example:

  1. A BlockingCollection contains a single item.
  2. CompleteAdding() is called, so IsCompleted is initially false.
  3. A consumer starts a Take operation and successfully waits on _occupiedNodes.
  4. _occupiedNodes.CurrentCount becomes 0, causing IsCompleted to return true even though the item has not yet been removed from the collection.
  5. The consumer operation is canceled before the item is removed.
  6. _occupiedNodes.CurrentCount is restored to 1.
  7. IsCompleted then returns to false.

As a result, IsCompleted can transition from false to true and back to false even though the item was never removed from the collection.

Fix

Introduce a separate _actualCount field to track the number of items successfully added to and removed from the collection.

_actualCount is initialized from the existing collection count and updated atomically after each successful add or remove operation, specifically in TryAddWithNoTimeValidation and TryTakeWithNoTimeValidation.

Count and IsCompleted now use _actualCount instead of _occupiedNodes.CurrentCount.

This separates the synchronization state of _occupiedNodes from the actual item count and prevents consumer cancellation from changing the state reported by IsCompleted.

Tests

Added a regression test that verifies that IsCompleted remains false when:

  • the collection contains a single item,
  • CompleteAdding() has been called,
  • a consumer waits to take the item using a cancellation token,
  • the operation is canceled before the item is removed.

The test then removes the remaining item and verifies that IsCompleted becomes true.

Closes #109217

@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Sep 10, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-collections
See info in area-owners.md if you want to be subscribed.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

{
current = Volatile.Read(ref _actualCount);
}
while (Interlocked.CompareExchange(ref _actualCount, current + 1, current) != current);

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.

Interlocked.Add can't be used due to build for older targets?

int current;
do
{
current = Volatile.Read(ref _actualCount);

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.

You can drop the volatile read, because below the interlocked operation provides the barrier.

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

Labels

area-System.Collections community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BlockingCollection can get back to uncompleted state even after IsCompleted was true

2 participants