Skip to content

Commit fc3d449

Browse files
committed
Fixed zero timeout compatibility with IsTimedOut property
1 parent 9fa39c7 commit fc3d449

File tree

3 files changed

+26
-6
lines changed

3 files changed

+26
-6
lines changed

src/DotNext.Tests/Threading/CancellationTokenMultiplexerTests.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,15 @@ public static void ExtraListOverflow()
7272
True(scope.Token.IsCancellationRequested);
7373
}
7474

75-
[Fact]
76-
public static async Task TimeOut()
75+
[Theory]
76+
[InlineData(0)]
77+
[InlineData(1)]
78+
public static async Task TimeOut(int timeout)
7779
{
7880
using var cts = new CancellationTokenSource();
7981
var multiplexer = new CancellationTokenMultiplexer();
8082

81-
await using var scope = multiplexer.Combine(TimeSpan.FromMilliseconds(1), [cts.Token]);
83+
await using var scope = multiplexer.Combine(TimeSpan.FromMilliseconds(timeout), [cts.Token]);
8284
await scope.Token.WaitAsync();
8385

8486
Equal(scope.Token, scope.CancellationOrigin);
@@ -94,7 +96,7 @@ public static async Task LazyTimeout()
9496
await using var scope = multiplexer.CombineAndSetTimeoutLater([]);
9597
False(scope.Token.IsCancellationRequested);
9698

97-
scope.Timeout = TimeSpan.FromMilliseconds(1);
99+
scope.Timeout = TimeSpan.FromMilliseconds(0);
98100
await scope.Token.WaitAsync();
99101

100102
Equal(scope.Token, scope.CancellationOrigin);

src/DotNext.Threading/Threading/CancellationTokenMultiplexer.Scope.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ private static CancellationToken GetToken(MultiplexerOrToken value)
6666
/// <summary>
6767
/// Gets a value indicating that the multiplexed token is cancelled by the timeout.
6868
/// </summary>
69-
public bool IsTimedOut => source?.IsRootCause ?? false;
69+
public bool IsTimedOut => source?.IsRootCause ?? GetToken(multiplexerOrToken) == TimedOutToken;
7070

7171
internal void SetTimeout(TimeSpan value) => source?.CancelAfter(value);
7272

@@ -158,4 +158,22 @@ public TimeSpan Timeout
158158
/// <inheritdoc/>
159159
public ValueTask DisposeAsync() => scope.DisposeAsync();
160160
}
161+
162+
private static CancellationToken TimedOutToken => TimedOutTokenSource.Token;
163+
}
164+
165+
// This source represents a canceled token that is canceled by zero timeout.
166+
// It's not possible to use new CancellationToken(canceled: true) because the multiplexer
167+
// cannot distinguish between the canceled token passed by the user code and the token that represents the timeout.
168+
// This class is not accessible by the user code, and its token cannot be passed to the multiplexer directly.
169+
file static class TimedOutTokenSource
170+
{
171+
public static readonly CancellationToken Token;
172+
173+
static TimedOutTokenSource()
174+
{
175+
using var source = new CancellationTokenSource();
176+
Token = source.Token;
177+
source.Cancel();
178+
}
161179
}

src/DotNext.Threading/Threading/CancellationTokenMultiplexer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public Scope Combine(ReadOnlySpan<CancellationToken> tokens) // TODO: use params
7575
/// <exception cref="ArgumentOutOfRangeException"><paramref name="timeout"/> is negative or too large.</exception>
7676
public Scope Combine(TimeSpan timeout, ReadOnlySpan<CancellationToken> tokens) => timeout.Ticks switch
7777
{
78-
0L => new(new CancellationToken(canceled: true)),
78+
0L => new(TimedOutToken),
7979
Timeout.InfiniteTicks => Combine(tokens),
8080
< 0L or > Timeout.MaxTimeoutParameterTicks => throw new ArgumentOutOfRangeException(nameof(timeout)),
8181
_ => new(this, timeout, tokens)

0 commit comments

Comments
 (0)