Skip to content

Commit 844215d

Browse files
committed
Suggestions from code review
1 parent 708c0fd commit 844215d

File tree

2 files changed

+87
-45
lines changed

2 files changed

+87
-45
lines changed

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

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -181,18 +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-
using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_streamCancellationToken, cancellationToken))
185-
{
186-
return await _pipeReaderStream.ReadAsync(buffer.AsMemory(offset, count), linkedCts.Token);
187-
}
184+
using var linkedCts = ValueLinkedCancellationTokenSource.Create(_streamCancellationToken, cancellationToken);
185+
return await _pipeReaderStream.ReadAsync(buffer.AsMemory(offset, count), linkedCts.Token);
188186
}
189187

190188
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
191189
{
192-
using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_streamCancellationToken, cancellationToken))
193-
{
194-
return await _pipeReaderStream.ReadAsync(buffer, linkedCts.Token);
195-
}
190+
using var linkedCts = ValueLinkedCancellationTokenSource.Create(_streamCancellationToken, cancellationToken);
191+
return await _pipeReaderStream.ReadAsync(buffer, linkedCts.Token);
196192
}
197193

198194
private async Task ThrowOnTimeout()
@@ -233,4 +229,45 @@ protected override void Dispose(bool disposing)
233229

234230
_disposed = true;
235231
}
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+
}
236273
}

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

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -287,54 +287,59 @@ public async Task ReceiveData_ReceivesDataThenTimesout_StreamDisposed()
287287
Assert.False(success);
288288
}
289289

290-
[Fact]
291-
public async Task ReadAsync_ByteArray_DisposesCancellationTokenSource()
290+
[Theory]
291+
[InlineData(false)]
292+
[InlineData(true)]
293+
public void ValueLinkedCts_Works_WhenOneTokenCannotBeCanceled(bool isToken1Cancelable)
292294
{
293-
// Arrange
294-
var jsStreamReference = Mock.Of<IJSStreamReference>();
295-
var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(_jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None).DefaultTimeout();
296-
var buffer = new byte[100];
297-
var chunk = new byte[100];
298-
new Random().NextBytes(chunk);
299-
300-
// Act
301295
var cts = new CancellationTokenSource();
302-
var sendDataTask = Task.Run(async () =>
303-
{
304-
await RemoteJSDataStream.ReceiveData(_jsRuntime, GetStreamId(remoteJSDataStream, _jsRuntime), chunkId: 0, chunk, error: null);
305-
});
296+
var token1 = isToken1Cancelable ? cts.Token : CancellationToken.None;
297+
var token2 = isToken1Cancelable ? CancellationToken.None : cts.Token;
306298

307-
await sendDataTask;
308-
var result = await remoteJSDataStream.ReadAsync(buffer, 0, buffer.Length, cts.Token);
299+
using var linkedCts = RemoteJSDataStream.ValueLinkedCancellationTokenSource.Create(token1, token2);
309300

310-
// Assert
311-
Assert.True(cts.Token.CanBeCanceled);
312-
Assert.False(cts.IsCancellationRequested);
301+
Assert.False(linkedCts.HasLinkedCancellationTokenSource);
302+
Assert.False(linkedCts.Token.IsCancellationRequested);
303+
304+
cts.Cancel();
305+
306+
Assert.True(linkedCts.Token.IsCancellationRequested);
313307
}
314308

315309
[Fact]
316-
public async Task ReadAsync_Memory_DisposesCancellationTokenSource()
310+
public void ValueLinkedCts_Works_WhenBothTokensCannotBeCanceled()
317311
{
318-
// Arrange
319-
var jsStreamReference = Mock.Of<IJSStreamReference>();
320-
var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(_jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None).DefaultTimeout();
321-
var buffer = new Memory<byte>(new byte[100]);
322-
var chunk = new byte[100];
323-
new Random().NextBytes(chunk);
312+
using var linkedCts = RemoteJSDataStream.ValueLinkedCancellationTokenSource.Create(
313+
CancellationToken.None,
314+
CancellationToken.None);
324315

325-
// Act
326-
var cts = new CancellationTokenSource();
327-
var sendDataTask = Task.Run(async () =>
328-
{
329-
await RemoteJSDataStream.ReceiveData(_jsRuntime, GetStreamId(remoteJSDataStream, _jsRuntime), chunkId: 0, chunk, error: null);
330-
});
316+
Assert.False(linkedCts.HasLinkedCancellationTokenSource);
317+
Assert.False(linkedCts.Token.IsCancellationRequested);
318+
}
331319

332-
await sendDataTask;
333-
var result = await remoteJSDataStream.ReadAsync(buffer, cts.Token);
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);
334329

335-
// Assert
336-
Assert.True(cts.Token.CanBeCanceled);
337-
Assert.False(cts.IsCancellationRequested);
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);
338343
}
339344

340345
private static async Task<RemoteJSDataStream> CreateRemoteJSDataStreamAsync(TestRemoteJSRuntime jsRuntime = null)

0 commit comments

Comments
 (0)