Skip to content

Commit 6023624

Browse files
Ensuring cancellation of tokens when using GetLinkedCancellationToken(a,b) (#55968)
Co-authored-by: Mackinnon Buck <mackinnon.buck@gmail.com>
1 parent d8acb6d commit 6023624

File tree

2 files changed

+100
-18
lines changed

2 files changed

+100
-18
lines changed

src/Components/Server/src/Circuits/RemoteJSDataStream.cs

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -181,28 +181,14 @@ public override void Write(byte[] buffer, int offset, int count)
181181

182182
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
183183
{
184-
var linkedCancellationToken = GetLinkedCancellationToken(_streamCancellationToken, cancellationToken);
185-
return await _pipeReaderStream.ReadAsync(buffer.AsMemory(offset, count), linkedCancellationToken);
184+
using var linkedCts = ValueLinkedCancellationTokenSource.Create(_streamCancellationToken, cancellationToken);
185+
return await _pipeReaderStream.ReadAsync(buffer.AsMemory(offset, count), linkedCts.Token);
186186
}
187187

188188
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
189189
{
190-
var linkedCancellationToken = GetLinkedCancellationToken(_streamCancellationToken, cancellationToken);
191-
return await _pipeReaderStream.ReadAsync(buffer, linkedCancellationToken);
192-
}
193-
194-
private static CancellationToken GetLinkedCancellationToken(CancellationToken a, CancellationToken b)
195-
{
196-
if (a.CanBeCanceled && b.CanBeCanceled)
197-
{
198-
return CancellationTokenSource.CreateLinkedTokenSource(a, b).Token;
199-
}
200-
else if (a.CanBeCanceled)
201-
{
202-
return a;
203-
}
204-
205-
return b;
190+
using var linkedCts = ValueLinkedCancellationTokenSource.Create(_streamCancellationToken, cancellationToken);
191+
return await _pipeReaderStream.ReadAsync(buffer, linkedCts.Token);
206192
}
207193

208194
private async Task ThrowOnTimeout()
@@ -243,4 +229,45 @@ protected override void Dispose(bool disposing)
243229

244230
_disposed = true;
245231
}
232+
233+
// A helper for creating and disposing linked CancellationTokenSources
234+
// without allocating, when possible.
235+
// Internal for testing.
236+
internal readonly struct ValueLinkedCancellationTokenSource : IDisposable
237+
{
238+
private readonly CancellationTokenSource? _linkedCts;
239+
240+
public readonly CancellationToken Token;
241+
242+
// For testing.
243+
internal bool HasLinkedCancellationTokenSource => _linkedCts is not null;
244+
245+
public static ValueLinkedCancellationTokenSource Create(
246+
CancellationToken token1, CancellationToken token2)
247+
{
248+
if (!token1.CanBeCanceled)
249+
{
250+
return new(linkedCts: null, token2);
251+
}
252+
253+
if (!token2.CanBeCanceled)
254+
{
255+
return new(linkedCts: null, token1);
256+
}
257+
258+
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(token1, token2);
259+
return new(linkedCts, linkedCts.Token);
260+
}
261+
262+
private ValueLinkedCancellationTokenSource(CancellationTokenSource? linkedCts, CancellationToken token)
263+
{
264+
_linkedCts = linkedCts;
265+
Token = token;
266+
}
267+
268+
public void Dispose()
269+
{
270+
_linkedCts?.Dispose();
271+
}
272+
}
246273
}

src/Components/Server/test/Circuits/RemoteJSDataStreamTest.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,61 @@ public async Task ReceiveData_ReceivesDataThenTimesout_StreamDisposed()
287287
Assert.False(success);
288288
}
289289

290+
[Theory]
291+
[InlineData(false)]
292+
[InlineData(true)]
293+
public void ValueLinkedCts_Works_WhenOneTokenCannotBeCanceled(bool isToken1Cancelable)
294+
{
295+
var cts = new CancellationTokenSource();
296+
var token1 = isToken1Cancelable ? cts.Token : CancellationToken.None;
297+
var token2 = isToken1Cancelable ? CancellationToken.None : cts.Token;
298+
299+
using var linkedCts = RemoteJSDataStream.ValueLinkedCancellationTokenSource.Create(token1, token2);
300+
301+
Assert.False(linkedCts.HasLinkedCancellationTokenSource);
302+
Assert.False(linkedCts.Token.IsCancellationRequested);
303+
304+
cts.Cancel();
305+
306+
Assert.True(linkedCts.Token.IsCancellationRequested);
307+
}
308+
309+
[Fact]
310+
public void ValueLinkedCts_Works_WhenBothTokensCannotBeCanceled()
311+
{
312+
using var linkedCts = RemoteJSDataStream.ValueLinkedCancellationTokenSource.Create(
313+
CancellationToken.None,
314+
CancellationToken.None);
315+
316+
Assert.False(linkedCts.HasLinkedCancellationTokenSource);
317+
Assert.False(linkedCts.Token.IsCancellationRequested);
318+
}
319+
320+
[Theory]
321+
[InlineData(false, true)]
322+
[InlineData(true, false)]
323+
[InlineData(true, true)]
324+
public void ValueLinkedCts_Works_WhenBothTokensCanBeCanceled(bool shouldCancelToken1, bool shouldCancelToken2)
325+
{
326+
var cts1 = new CancellationTokenSource();
327+
var cts2 = new CancellationTokenSource();
328+
using var linkedCts = RemoteJSDataStream.ValueLinkedCancellationTokenSource.Create(cts1.Token, cts2.Token);
329+
330+
Assert.True(linkedCts.HasLinkedCancellationTokenSource);
331+
Assert.False(linkedCts.Token.IsCancellationRequested);
332+
333+
if (shouldCancelToken1)
334+
{
335+
cts1.Cancel();
336+
}
337+
if (shouldCancelToken2)
338+
{
339+
cts2.Cancel();
340+
}
341+
342+
Assert.True(linkedCts.Token.IsCancellationRequested);
343+
}
344+
290345
private static async Task<RemoteJSDataStream> CreateRemoteJSDataStreamAsync(TestRemoteJSRuntime jsRuntime = null)
291346
{
292347
var jsStreamReference = Mock.Of<IJSStreamReference>();

0 commit comments

Comments
 (0)