Skip to content

Commit 1cd162f

Browse files
committed
Task Completion Source & DefaultTimeout
1 parent 7785751 commit 1cd162f

File tree

2 files changed

+37
-48
lines changed

2 files changed

+37
-48
lines changed

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

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -219,16 +219,6 @@ private async Task CompletePipeAndDisposeStream(Exception? ex = null)
219219
Dispose(true);
220220
}
221221

222-
/// <summary>
223-
/// For testing purposes only.
224-
///
225-
/// Triggers the timeout on the next check.
226-
/// </summary>
227-
internal void InvalidateLastDataReceivedTimeForTimeout()
228-
{
229-
_lastDataReceivedTime = _lastDataReceivedTime.Subtract(_jsInteropDefaultCallTimeout);
230-
}
231-
232222
protected override void Dispose(bool disposing)
233223
{
234224
if (disposing)

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

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Threading;
88
using System.Threading.Tasks;
99
using Microsoft.AspNetCore.SignalR;
10+
using Microsoft.AspNetCore.Testing;
1011
using Microsoft.Extensions.Logging;
1112
using Microsoft.Extensions.Options;
1213
using Microsoft.JSInterop;
@@ -26,7 +27,7 @@ public async Task CreateRemoteJSDataStreamAsync_CreatesStream()
2627
var jsStreamReference = Mock.Of<IJSStreamReference>();
2728

2829
// Act
29-
var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(_jsRuntime, jsStreamReference, totalLength: 100, maximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), pauseIncomingBytesThreshold: 50, resumeIncomingBytesThreshold: 25, cancellationToken: CancellationToken.None);
30+
var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(_jsRuntime, jsStreamReference, totalLength: 100, maximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), pauseIncomingBytesThreshold: 50, resumeIncomingBytesThreshold: 25, cancellationToken: CancellationToken.None).DefaultTimeout();
3031

3132
// Assert
3233
Assert.NotNull(remoteJSDataStream);
@@ -40,7 +41,7 @@ public async Task ReceiveData_DoesNotFindStream()
4041
var unrecognizedGuid = 10;
4142

4243
// Act
43-
var success = await RemoteJSDataStream.ReceiveData(_jsRuntime, streamId: unrecognizedGuid, chunkId: 0, chunk, error: null);
44+
var success = await RemoteJSDataStream.ReceiveData(_jsRuntime, streamId: unrecognizedGuid, chunkId: 0, chunk, error: null).DefaultTimeout();
4445

4546
// Assert
4647
Assert.False(success);
@@ -60,17 +61,17 @@ public async Task ReceiveData_SuccessReadsBackStream()
6061
var sendDataTask = Task.Run(async () =>
6162
{
6263
// Act 1
63-
var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null);
64+
var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout();
6465
return success;
6566
});
6667

6768
// Act & Assert 2
6869
using var memoryStream = new MemoryStream();
69-
await remoteJSDataStream.CopyToAsync(memoryStream);
70+
await remoteJSDataStream.CopyToAsync(memoryStream).DefaultTimeout();
7071
Assert.Equal(chunk, memoryStream.ToArray());
7172

7273
// Act & Assert 3
73-
var sendDataCompleted = await sendDataTask;
74+
var sendDataCompleted = await sendDataTask.DefaultTimeout();
7475
Assert.True(sendDataCompleted);
7576
}
7677

@@ -88,17 +89,17 @@ public async Task ReceiveData_SuccessReadsBackPipeReader()
8889
var sendDataTask = Task.Run(async () =>
8990
{
9091
// Act 1
91-
var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null);
92+
var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout();
9293
return success;
9394
});
9495

9596
// Act & Assert 2
9697
using var memoryStream = new MemoryStream();
97-
await remoteJSDataStream.PipeReader.CopyToAsync(memoryStream);
98+
await remoteJSDataStream.PipeReader.CopyToAsync(memoryStream).DefaultTimeout();
9899
Assert.Equal(chunk, memoryStream.ToArray());
99100

100101
// Act & Assert 3
101-
var sendDataCompleted = await sendDataTask;
102+
var sendDataCompleted = await sendDataTask.DefaultTimeout();
102103
Assert.True(sendDataCompleted);
103104
}
104105

@@ -111,12 +112,12 @@ public async Task ReceiveData_WithError()
111112
var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
112113

113114
// Act & Assert 1
114-
var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk: null, error: "some error");
115+
var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk: null, error: "some error").DefaultTimeout();
115116
Assert.False(success);
116117

117118
// Act & Assert 2
118119
using var mem = new MemoryStream();
119-
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await remoteJSDataStream.CopyToAsync(mem));
120+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
120121
Assert.Equal("An error occurred while reading the remote stream: some error", ex.Message);
121122
}
122123

@@ -130,12 +131,12 @@ public async Task ReceiveData_WithZeroLengthChunk()
130131
var chunk = Array.Empty<byte>();
131132

132133
// Act & Assert 1
133-
var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null));
134+
var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout());
134135
Assert.Equal("The incoming data chunk cannot be empty.", ex.Message);
135136

136137
// Act & Assert 2
137138
using var mem = new MemoryStream();
138-
ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await remoteJSDataStream.CopyToAsync(mem));
139+
ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
139140
Assert.Equal("The incoming data chunk cannot be empty.", ex.Message);
140141
}
141142

@@ -150,12 +151,12 @@ public async Task ReceiveData_ProvidedWithMoreBytesThanRemaining()
150151
var chunk = new byte[110]; // 100 byte totalLength for stream
151152

152153
// Act & Assert 1
153-
var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null));
154+
var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout());
154155
Assert.Equal("The incoming data stream declared a length 100, but 110 bytes were sent.", ex.Message);
155156

156157
// Act & Assert 2
157158
using var mem = new MemoryStream();
158-
ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await remoteJSDataStream.CopyToAsync(mem));
159+
ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
159160
Assert.Equal("The incoming data stream declared a length 100, but 110 bytes were sent.", ex.Message);
160161
}
161162

@@ -174,26 +175,25 @@ public async Task ReceiveData_ProvidedWithOutOfOrderChunk_SimulatesSignalRDiscon
174175
{
175176
await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: i, chunk, error: null);
176177
}
177-
var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 7, chunk, error: null));
178+
var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 7, chunk, error: null).DefaultTimeout());
178179
Assert.Equal("Out of sequence chunk received, expected 5, but received 7.", ex.Message);
179180

180181
// Act & Assert 2
181182
using var mem = new MemoryStream();
182-
ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await remoteJSDataStream.CopyToAsync(mem));
183+
ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
183184
Assert.Equal("Out of sequence chunk received, expected 5, but received 7.", ex.Message);
184185
}
185186

186187
[Fact]
187188
public async Task ReceiveData_NoDataProvidedBeforeTimeout_StreamDisposed()
188189
{
189190
// Arrange
191+
var unhandledExceptionRaisedTask = new TaskCompletionSource<bool>();
190192
var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of<ILogger<RemoteJSRuntime>>());
191-
var timeoutExceptionRaisedSemaphore = new SemaphoreSlim(initialCount: 0, maxCount: 1);
192193
jsRuntime.UnhandledException += (_, ex) =>
193194
{
194-
Assert.Equal("Did not receive any data in the alloted time.", ex.Message);
195-
Assert.IsType<TimeoutException>(ex);
196-
timeoutExceptionRaisedSemaphore.Release();
195+
Assert.Equal("Did not receive any data in the allotted time.", ex.Message);
196+
unhandledExceptionRaisedTask.SetResult(ex is TimeoutException);
197197
};
198198

199199
var jsStreamReference = Mock.Of<IJSStreamReference>();
@@ -202,41 +202,40 @@ public async Task ReceiveData_NoDataProvidedBeforeTimeout_StreamDisposed()
202202
jsStreamReference,
203203
totalLength: 15,
204204
maximumIncomingBytes: 10_000,
205-
jsInteropDefaultCallTimeout: TimeSpan.FromSeconds(10), // Note we're using a 10 second timeout for this test
205+
jsInteropDefaultCallTimeout: TimeSpan.FromSeconds(1),
206206
pauseIncomingBytesThreshold: 50,
207207
resumeIncomingBytesThreshold: 25,
208208
cancellationToken: CancellationToken.None);
209209
var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
210210
var chunk = new byte[] { 3, 5, 7 };
211211

212212
// Act & Assert 1
213-
// Trigger timeout and ensure unhandled exception raised to crush circuit
214-
remoteJSDataStream.InvalidateLastDataReceivedTimeForTimeout();
215-
await timeoutExceptionRaisedSemaphore.WaitAsync();
213+
// Ensure unhandled exception raised to crush circuit
214+
var unhandledExceptionResult = await unhandledExceptionRaisedTask.Task.DefaultTimeout();
215+
Assert.True(unhandledExceptionResult);
216216

217217
// Act & Assert 2
218218
// Confirm exception also raised on pipe reader
219219
using var mem = new MemoryStream();
220-
var ex = await Assert.ThrowsAsync<TimeoutException>(async () => await remoteJSDataStream.CopyToAsync(mem));
220+
var ex = await Assert.ThrowsAsync<TimeoutException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
221221
Assert.Equal("Did not receive any data in the allotted time.", ex.Message);
222222

223223
// Act & Assert 3
224224
// Ensures stream is disposed after the timeout and any additional chunks aren't accepted
225-
var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null);
225+
var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout();
226226
Assert.False(success);
227227
}
228228

229229
[Fact]
230230
public async Task ReceiveData_ReceivesDataThenTimesout_StreamDisposed()
231231
{
232232
// Arrange
233+
var unhandledExceptionRaisedTask = new TaskCompletionSource<bool>();
233234
var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of<ILogger<RemoteJSRuntime>>());
234-
var timeoutExceptionRaisedSemaphore = new SemaphoreSlim(initialCount: 0, maxCount: 1);
235235
jsRuntime.UnhandledException += (_, ex) =>
236236
{
237237
Assert.Equal("Did not receive any data in the allotted time.", ex.Message);
238-
Assert.IsType<TimeoutException>(ex);
239-
timeoutExceptionRaisedSemaphore.Release();
238+
unhandledExceptionRaisedTask.SetResult(ex is TimeoutException);
240239
};
241240

242241
var jsStreamReference = Mock.Of<IJSStreamReference>();
@@ -245,35 +244,35 @@ public async Task ReceiveData_ReceivesDataThenTimesout_StreamDisposed()
245244
jsStreamReference,
246245
totalLength: 15,
247246
maximumIncomingBytes: 10_000,
248-
jsInteropDefaultCallTimeout: TimeSpan.FromSeconds(30), // Note we're using a 30 second timeout for this test
247+
jsInteropDefaultCallTimeout: TimeSpan.FromSeconds(3),
249248
pauseIncomingBytesThreshold: 50,
250249
resumeIncomingBytesThreshold: 25,
251250
cancellationToken: CancellationToken.None);
252251
var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
253252
var chunk = new byte[] { 3, 5, 7 };
254253

255254
// Act & Assert 1
256-
var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null);
255+
var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout();
257256
Assert.True(success);
258257

259258
// Act & Assert 2
260-
success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 1, chunk, error: null);
259+
success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 1, chunk, error: null).DefaultTimeout();
261260
Assert.True(success);
262261

263262
// Act & Assert 3
264-
// Trigger timeout and ensure unhandled exception raised to crush circuit
265-
remoteJSDataStream.InvalidateLastDataReceivedTimeForTimeout();
266-
await timeoutExceptionRaisedSemaphore.WaitAsync();
263+
// Ensure unhandled exception raised to crush circuit
264+
var unhandledExceptionResult = await unhandledExceptionRaisedTask.Task.DefaultTimeout();
265+
Assert.True(unhandledExceptionResult);
267266

268267
// Act & Assert 4
269268
// Confirm exception also raised on pipe reader
270269
using var mem = new MemoryStream();
271-
var ex = await Assert.ThrowsAsync<TimeoutException>(async () => await remoteJSDataStream.CopyToAsync(mem));
272-
Assert.Equal("Did not receive any data in the alloted time.", ex.Message);
270+
var ex = await Assert.ThrowsAsync<TimeoutException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
271+
Assert.Equal("Did not receive any data in the allotted time.", ex.Message);
273272

274273
// Act & Assert 5
275274
// Ensures stream is disposed after the timeout and any additional chunks aren't accepted
276-
success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 2, chunk, error: null);
275+
success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 2, chunk, error: null).DefaultTimeout();
277276
Assert.False(success);
278277
}
279278

0 commit comments

Comments
 (0)