Description
We have a Thread that consumes some data from the BlockingCollection, as some point in time, we will finish adding elements and we want to wait for the consumer to process all the data.
So the natural thing is to check if IsCompleted is true - as it indicate if the collection is empty - so all elements were processed.
At some cases processing the queue can be long operation, so we just want to cancel it - even if we didn't process all elements.
Turns out if you pass the CancellationToken to GetConsumingEnumerable and cancel it, even after IsCompleted is true, at some point the element will be "added back" and your collection will get back to the uncompleted sate.
Reproduction Steps
t - represent the consumer... not the correct implementation, just for the simplicity we ignore all errors and start reading over - so no need to creating new every loop
using System.Collections.Concurrent;
CancellationTokenSource cts = null;
BlockingCollection<object> collection = null;
var t = new Thread(() =>
{
while (true)
{
try
{
foreach (var _ in collection?.GetConsumingEnumerable(cts?.Token ?? default) ?? [])
{
}
}
catch (Exception e)
{
//
}
}
});
t.IsBackground = true;
t.Start();
// Arrange
for (var i = 0; i < 100; i++)
{
cts = new CancellationTokenSource();
collection = new BlockingCollection<object>(1000);
collection.Add(new object());
collection.CompleteAdding();
SpinWait spin = default;
while (!collection.IsCompleted)
{
spin.SpinOnce();
}
cts.Cancel();
var counter = 50;
spin.Reset();
while (collection.IsCompleted && counter > 0)
{
counter--;
spin.SpinOnce();
}
if (collection is { IsCompleted: false })
{
throw new Exception($"Error! Colection is not completed anymore! {i}-{counter}");
}
}
Console.WriteLine("All good!");
GC.KeepAlive(t);
Expected behavior
I know that reacting to faulty removal or cancellation token (as we don't want to process with the data) should NOT remove the element from the collection and keep it still uncompleted.
So I see two options:
- the
collection.IsCompleted will stay true even after we cancelled the token - that means that if we already removed element (and it's successful) and it's gonna be returned to the consumer, we will not mess up with the IsCompleted state.
- the
collection.IsCompleted should not be set to true if we are still processing element, and there is possibility that it will get back to the queue - only when we are returning to the consumer and we know that will be the last element, the IsCompleted will become true
Actual behavior
collection.IsCompleted will change to false even if it was set before to true.
Regression?
Don't think it's regression, as the same behavior was in previous versions.
It's caused by this line:
|
cancellationToken.ThrowIfCancellationRequested(); |
But cannot find the reference to the "bug #702328" - so don't know what was real reason to put that check there.
Known Workarounds
Two possibilities:
- put delay before setting the cancellation or setting the cancellation with the timeout (so it will not fire immediately)
- not use token in
GetConsumingEnumerable, do the check manually inside the loop.
Configuration
No response
Other information
No response
Description
We have a Thread that consumes some data from the BlockingCollection, as some point in time, we will finish adding elements and we want to wait for the consumer to process all the data.
So the natural thing is to check if
IsCompletedis true - as it indicate if the collection is empty - so all elements were processed.At some cases processing the queue can be long operation, so we just want to cancel it - even if we didn't process all elements.
Turns out if you pass the CancellationToken to
GetConsumingEnumerableand cancel it, even afterIsCompletedis true, at some point the element will be "added back" and your collection will get back to the uncompleted sate.Reproduction Steps
t- represent the consumer... not the correct implementation, just for the simplicity we ignore all errors and start reading over - so no need to creating new every loopExpected behavior
I know that reacting to faulty removal or cancellation token (as we don't want to process with the data) should NOT remove the element from the collection and keep it still uncompleted.
So I see two options:
collection.IsCompletedwill staytrueeven after we cancelled the token - that means that if we already removed element (and it's successful) and it's gonna be returned to the consumer, we will not mess up with theIsCompletedstate.collection.IsCompletedshould not be set totrueif we are still processing element, and there is possibility that it will get back to the queue - only when we are returning to the consumer and we know that will be the last element, theIsCompletedwill becometrueActual behavior
collection.IsCompletedwill change tofalseeven if it was set before totrue.Regression?
Don't think it's regression, as the same behavior was in previous versions.
It's caused by this line:
runtime/src/libraries/System.Collections.Concurrent/src/System/Collections/Concurrent/BlockingCollection.cs
Line 693 in a0fddda
But cannot find the reference to the "bug #702328" - so don't know what was real reason to put that check there.
Known Workarounds
Two possibilities:
GetConsumingEnumerable, do the check manually inside the loop.Configuration
No response
Other information
No response