Implement server-side sync context
This is a synchronization context we can use for server side blazor to support a single logical thread of execution. This is optimized for scalability and non-blocking behavior.
This commit is contained in:
parent
8724b84a14
commit
e0168eb0c8
17
Blazor.sln
17
Blazor.sln
|
|
@ -95,11 +95,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.Analyzers.Test", "test\Microsoft.AspNetCore.Blazor.Analyzers.Test\Microsoft.AspNetCore.Blazor.Analyzers.Test.csproj", "{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.JSInterop", "src\Microsoft.JSInterop\Microsoft.JSInterop.csproj", "{C866B19D-AFFF-45B7-8DAB-71805F39D516}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop", "src\Microsoft.JSInterop\Microsoft.JSInterop.csproj", "{C866B19D-AFFF-45B7-8DAB-71805F39D516}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.JSInterop.Test", "test\Microsoft.JSInterop.Test\Microsoft.JSInterop.Test.csproj", "{BA1CE1FD-89D8-423F-A21B-6B212674EB39}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop.Test", "test\Microsoft.JSInterop.Test\Microsoft.JSInterop.Test.csproj", "{BA1CE1FD-89D8-423F-A21B-6B212674EB39}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mono.WebAssembly.Interop", "src\Mono.WebAssembly.Interop\Mono.WebAssembly.Interop.csproj", "{C56873E6-8F49-476E-AF51-B5D187832CF5}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.WebAssembly.Interop", "src\Mono.WebAssembly.Interop\Mono.WebAssembly.Interop.csproj", "{C56873E6-8F49-476E-AF51-B5D187832CF5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspnetCore.Blazor.Server.Test", "test\Microsoft.AspnetCore.Blazor.Server.Test\Microsoft.AspnetCore.Blazor.Server.Test.csproj", "{142AA6BC-5110-486B-A34D-6878E5E2CE95}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
|
@ -368,6 +370,14 @@ Global
|
|||
{C56873E6-8F49-476E-AF51-B5D187832CF5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C56873E6-8F49-476E-AF51-B5D187832CF5}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C56873E6-8F49-476E-AF51-B5D187832CF5}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{142AA6BC-5110-486B-A34D-6878E5E2CE95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{142AA6BC-5110-486B-A34D-6878E5E2CE95}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{142AA6BC-5110-486B-A34D-6878E5E2CE95}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{142AA6BC-5110-486B-A34D-6878E5E2CE95}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{142AA6BC-5110-486B-A34D-6878E5E2CE95}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{142AA6BC-5110-486B-A34D-6878E5E2CE95}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{142AA6BC-5110-486B-A34D-6878E5E2CE95}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{142AA6BC-5110-486B-A34D-6878E5E2CE95}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -413,6 +423,7 @@ Global
|
|||
{C866B19D-AFFF-45B7-8DAB-71805F39D516} = {B867E038-B3CE-43E3-9292-61568C46CDEB}
|
||||
{BA1CE1FD-89D8-423F-A21B-6B212674EB39} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
|
||||
{C56873E6-8F49-476E-AF51-B5D187832CF5} = {7B5CAAB1-A3EB-44F7-87E3-A13ED89FC17D}
|
||||
{142AA6BC-5110-486B-A34D-6878E5E2CE95} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,269 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Server.Circuits
|
||||
{
|
||||
[DebuggerDisplay("{_state,nq}")]
|
||||
internal class CircuitSynchronizationContext : SynchronizationContext
|
||||
{
|
||||
private static readonly ContextCallback ExecutionContextThunk = (object state) =>
|
||||
{
|
||||
var item = (WorkItem)state;
|
||||
item.SynchronizationContext.ExecuteSynchronously(null, item.Callback, item.State);
|
||||
};
|
||||
|
||||
private static readonly Action<Task, object> BackgroundWorkThunk = (Task task, object state) =>
|
||||
{
|
||||
var item = (WorkItem)state;
|
||||
item.SynchronizationContext.ExecuteBackground(item);
|
||||
};
|
||||
|
||||
private readonly State _state;
|
||||
|
||||
public event UnhandledExceptionEventHandler UnhandledException;
|
||||
|
||||
public CircuitSynchronizationContext()
|
||||
: this(new State())
|
||||
{
|
||||
}
|
||||
|
||||
private CircuitSynchronizationContext(State state)
|
||||
{
|
||||
_state = state;
|
||||
}
|
||||
|
||||
public Task Invoke(Action action)
|
||||
{
|
||||
var completion = new TaskCompletionSource<object>();
|
||||
Post(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
completion.SetResult(null);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
completion.SetException(exception);
|
||||
}
|
||||
}, null);
|
||||
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
public Task InvokeAsync(Func<Task> asyncAction)
|
||||
{
|
||||
var completion = new TaskCompletionSource<object>();
|
||||
Post(async (_) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await asyncAction();
|
||||
completion.SetResult(null);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
completion.SetException(exception);
|
||||
}
|
||||
}, null);
|
||||
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
public Task<TResult> Invoke<TResult>(Func<TResult> function)
|
||||
{
|
||||
var completion = new TaskCompletionSource<TResult>();
|
||||
Post(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = function();
|
||||
completion.SetResult(result);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
completion.SetException(exception);
|
||||
}
|
||||
}, null);
|
||||
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
public Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> asyncFunction)
|
||||
{
|
||||
var completion = new TaskCompletionSource<TResult>();
|
||||
Post(async (_) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await asyncFunction();
|
||||
completion.SetResult(result);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
completion.SetException(exception);
|
||||
}
|
||||
}, null);
|
||||
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
// asynchronously runs the callback
|
||||
public override void Post(SendOrPostCallback d, object state)
|
||||
{
|
||||
TaskCompletionSource<object> completion;
|
||||
lock (_state.Lock)
|
||||
{
|
||||
if (!_state.Task.IsCompleted)
|
||||
{
|
||||
_state.Task = Enqueue(_state.Task, d, state);
|
||||
return;
|
||||
}
|
||||
|
||||
// We can execute this synchronously because nothing is currently running
|
||||
// or queued.
|
||||
completion = new TaskCompletionSource<object>();
|
||||
_state.Task = completion.Task;
|
||||
}
|
||||
|
||||
ExecuteSynchronously(completion, d, state);
|
||||
}
|
||||
|
||||
// synchronously runs the callback
|
||||
public override void Send(SendOrPostCallback d, object state)
|
||||
{
|
||||
Task antecedant;
|
||||
var completion = new TaskCompletionSource<object>();
|
||||
|
||||
lock (_state.Lock)
|
||||
{
|
||||
antecedant = _state.Task;
|
||||
_state.Task = completion.Task;
|
||||
}
|
||||
|
||||
// We have to block. That's the contract of Send - we don't expect this to be used
|
||||
// in many scenarios in Blazor.
|
||||
//
|
||||
// Using Wait here is ok because the antecedant task will never throw.
|
||||
antecedant.Wait();
|
||||
|
||||
ExecuteSynchronously(completion, d, state);
|
||||
}
|
||||
|
||||
// shallow copy
|
||||
public override SynchronizationContext CreateCopy()
|
||||
{
|
||||
return new CircuitSynchronizationContext(_state);
|
||||
}
|
||||
|
||||
private Task Enqueue(Task antecedant, SendOrPostCallback d, object state)
|
||||
{
|
||||
// If we get here is means that a callback is being queued while something is currently executing
|
||||
// in this context. Let's instead add it to the queue and yield.
|
||||
//
|
||||
// We use our own queue here to maintain the execution order of the callbacks scheduled here. Also
|
||||
// we need a queue rather than just scheduling an item in the thread pool - those items would immediately
|
||||
// block and hurt scalability.
|
||||
//
|
||||
// We need to capture the execution context so we can restore it later. This code is similar to
|
||||
// the call path of ThreadPool.QueueUserWorkItem and System.Threading.QueueUserWorkItemCallback.
|
||||
ExecutionContext executionContext = null;
|
||||
if (!ExecutionContext.IsFlowSuppressed())
|
||||
{
|
||||
executionContext = ExecutionContext.Capture();
|
||||
}
|
||||
|
||||
return antecedant.ContinueWith(BackgroundWorkThunk, new WorkItem()
|
||||
{
|
||||
SynchronizationContext = this,
|
||||
ExecutionContext = executionContext,
|
||||
Callback = d,
|
||||
State = state,
|
||||
}, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Current);
|
||||
}
|
||||
|
||||
private void ExecuteSynchronously(
|
||||
TaskCompletionSource<object> completion,
|
||||
SendOrPostCallback d,
|
||||
object state)
|
||||
{
|
||||
var original = Current;
|
||||
try
|
||||
{
|
||||
SetSynchronizationContext(this);
|
||||
_state.IsBusy = true;
|
||||
|
||||
d(state);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_state.IsBusy = false;
|
||||
SetSynchronizationContext(original);
|
||||
|
||||
completion?.SetResult(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteBackground(WorkItem item)
|
||||
{
|
||||
if (item.ExecutionContext == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
ExecuteSynchronously(null, item.Callback, item.State);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DispatchException(ex);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Perf - using a static thunk here to avoid a delegate allocation.
|
||||
try
|
||||
{
|
||||
ExecutionContext.Run(item.ExecutionContext, ExecutionContextThunk, item);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DispatchException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void DispatchException(Exception ex)
|
||||
{
|
||||
var handler = UnhandledException;
|
||||
if (handler != null)
|
||||
{
|
||||
handler(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
|
||||
}
|
||||
}
|
||||
|
||||
private class State
|
||||
{
|
||||
public bool IsBusy; // Just for debugging
|
||||
public object Lock = new object();
|
||||
public Task Task = Task.CompletedTask;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{{ Busy: {IsBusy}, Pending Task: {Task} }}";
|
||||
}
|
||||
}
|
||||
|
||||
private class WorkItem
|
||||
{
|
||||
public CircuitSynchronizationContext SynchronizationContext;
|
||||
public ExecutionContext ExecutionContext;
|
||||
public SendOrPostCallback Callback;
|
||||
public object State;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Cli")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Server.Test")]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,683 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Blazor.Server.Circuits;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspnetCore.Blazor.Server
|
||||
{
|
||||
public class CircuitSynchronizationContextTest
|
||||
{
|
||||
// Nothing should exceed the timeout in a successful run of the the tests, this is just here to catch
|
||||
// failures.
|
||||
public TimeSpan Timeout = Debugger.IsAttached ? System.Threading.Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10);
|
||||
|
||||
[Fact]
|
||||
public void Post_CanRunSynchronously_WhenNotBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
Thread capturedThread = null;
|
||||
|
||||
// Act
|
||||
context.Post((_) =>
|
||||
{
|
||||
capturedThread = Thread.CurrentThread;
|
||||
}, null);
|
||||
|
||||
// Assert
|
||||
Assert.Same(thread, capturedThread);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_CanRunSynchronously_WhenNotBusy_Exception()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidTimeZoneException>(() => context.Post((_) =>
|
||||
{
|
||||
throw new InvalidTimeZoneException();
|
||||
}, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Post_CanRunAsynchronously_WhenBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
Thread capturedThread = null;
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
var e2 = new ManualResetEventSlim();
|
||||
var e3 = new ManualResetEventSlim();
|
||||
|
||||
var task = Task.Run(() =>
|
||||
{
|
||||
context.Send((_) =>
|
||||
{
|
||||
e1.Set();
|
||||
Assert.True(e2.Wait(Timeout), "timeout");
|
||||
}, null);
|
||||
});
|
||||
|
||||
Assert.True(e1.Wait(Timeout), "timeout");
|
||||
|
||||
// Act
|
||||
context.Post((_) =>
|
||||
{
|
||||
capturedThread = Thread.CurrentThread;
|
||||
|
||||
e3.Set();
|
||||
}, null);
|
||||
|
||||
// Assert
|
||||
Assert.False(e2.IsSet);
|
||||
e2.Set(); // Unblock the first item
|
||||
await task;
|
||||
|
||||
Assert.True(e3.Wait(Timeout), "timeout");
|
||||
Assert.NotSame(thread, capturedThread);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Post_CanRunAsynchronously_CaptureExecutionContext()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
|
||||
// CultureInfo uses the execution context.
|
||||
CultureInfo.CurrentCulture = new CultureInfo("en-GB");
|
||||
CultureInfo capturedCulture = null;
|
||||
|
||||
SynchronizationContext capturedContext = null;
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
var e2 = new ManualResetEventSlim();
|
||||
var e3 = new ManualResetEventSlim();
|
||||
|
||||
var task = Task.Run(() =>
|
||||
{
|
||||
context.Send((_) =>
|
||||
{
|
||||
e1.Set();
|
||||
Assert.True(e2.Wait(Timeout), "timeout");
|
||||
}, null);
|
||||
});
|
||||
|
||||
Assert.True(e1.Wait(Timeout), "timeout");
|
||||
|
||||
// Act
|
||||
SynchronizationContext original = SynchronizationContext.Current;
|
||||
|
||||
try
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(context);
|
||||
context.Post((_) =>
|
||||
{
|
||||
capturedCulture = CultureInfo.CurrentCulture;
|
||||
capturedContext = SynchronizationContext.Current;
|
||||
e3.Set();
|
||||
}, null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(original);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.False(e2.IsSet);
|
||||
e2.Set(); // Unblock the first item
|
||||
await task;
|
||||
|
||||
Assert.True(e3.Wait(Timeout), "timeout");
|
||||
Assert.Same(CultureInfo.CurrentCulture, capturedCulture);
|
||||
Assert.Same(context, capturedContext);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Post_CanRunAsynchronously_WhenBusy_Exception()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
|
||||
Exception exception = null;
|
||||
context.UnhandledException += (sender, e) =>
|
||||
{
|
||||
exception = (InvalidTimeZoneException)e.ExceptionObject;
|
||||
};
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
var e2 = new ManualResetEventSlim();
|
||||
|
||||
var task = Task.Run(() =>
|
||||
{
|
||||
context.Send((_) =>
|
||||
{
|
||||
e1.Set();
|
||||
Assert.True(e2.Wait(Timeout), "timeout");
|
||||
}, null);
|
||||
});
|
||||
|
||||
Assert.True(e1.Wait(Timeout), "timeout");
|
||||
|
||||
// Act
|
||||
context.Post((_) =>
|
||||
{
|
||||
throw new InvalidTimeZoneException();
|
||||
}, null);
|
||||
|
||||
// Assert
|
||||
Assert.False(e2.IsSet);
|
||||
e2.Set(); // Unblock the first item
|
||||
await task;
|
||||
|
||||
// Use another item to 'push through' the throwing one
|
||||
context.Send((_) => { }, null);
|
||||
Assert.NotNull(exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Post_BackgroundWorkItem_CanProcessMoreItemsInline()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
Thread capturedThread = null;
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
var e2 = new ManualResetEventSlim();
|
||||
var e3 = new ManualResetEventSlim();
|
||||
var e4 = new ManualResetEventSlim();
|
||||
var e5 = new ManualResetEventSlim();
|
||||
var e6 = new ManualResetEventSlim();
|
||||
|
||||
// Force task2 to execute in the background
|
||||
var task1 = Task.Run(() => context.Send((_) =>
|
||||
{
|
||||
e1.Set();
|
||||
Assert.True(e2.Wait(Timeout), "timeout");
|
||||
}, null));
|
||||
|
||||
Assert.True(e1.Wait(Timeout), "timeout");
|
||||
|
||||
var task2 = Task.Run(() =>
|
||||
{
|
||||
context.Send((_) =>
|
||||
{
|
||||
e3.Set();
|
||||
Assert.True(e4.Wait(Timeout), "timeout");
|
||||
capturedThread = Thread.CurrentThread;
|
||||
}, null);
|
||||
});
|
||||
|
||||
e2.Set();
|
||||
await task1;
|
||||
|
||||
Assert.True(e3.Wait(Timeout), "timeout");
|
||||
|
||||
// Act
|
||||
//
|
||||
// Now task2 is 'running' in the sync context. Schedule more work items - they will be
|
||||
// run immediately after the second item
|
||||
context.Post((_) =>
|
||||
{
|
||||
e5.Set();
|
||||
Assert.Same(Thread.CurrentThread, capturedThread);
|
||||
}, null);
|
||||
context.Post((_) =>
|
||||
{
|
||||
e6.Set();
|
||||
Assert.Same(Thread.CurrentThread, capturedThread);
|
||||
}, null);
|
||||
|
||||
|
||||
// Assert
|
||||
e4.Set();
|
||||
await task2;
|
||||
|
||||
Assert.True(e5.Wait(Timeout), "timeout");
|
||||
Assert.True(e6.Wait(Timeout), "timeout");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Post_CapturesContext()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
|
||||
// CultureInfo uses the execution context.
|
||||
CultureInfo.CurrentCulture = new CultureInfo("en-GB");
|
||||
CultureInfo capturedCulture = null;
|
||||
|
||||
SynchronizationContext capturedContext = null;
|
||||
|
||||
// Act
|
||||
context.Post(async (_) =>
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
capturedCulture = CultureInfo.CurrentCulture;
|
||||
capturedContext = SynchronizationContext.Current;
|
||||
e1.Set();
|
||||
}, null);
|
||||
|
||||
// Assert
|
||||
Assert.True(e1.Wait(Timeout), "timeout");
|
||||
Assert.Same(CultureInfo.CurrentCulture, capturedCulture);
|
||||
Assert.Same(context, capturedContext);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Send_CanRunSynchonously()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
Thread capturedThread = null;
|
||||
|
||||
// Act
|
||||
context.Send((_) =>
|
||||
{
|
||||
capturedThread = Thread.CurrentThread;
|
||||
}, null);
|
||||
|
||||
// Assert
|
||||
Assert.Same(thread, capturedThread);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Send_CanRunSynchronously_Exception()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidTimeZoneException>(() => context.Send((_) =>
|
||||
{
|
||||
throw new InvalidTimeZoneException();
|
||||
}, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Send_BlocksWhenOtherWorkRunning()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
var e2 = new ManualResetEventSlim();
|
||||
var e3 = new ManualResetEventSlim();
|
||||
var e4 = new ManualResetEventSlim();
|
||||
|
||||
// Force task2 to execute in the background
|
||||
var task1 = Task.Run(() =>
|
||||
{
|
||||
context.Send((_) =>
|
||||
{
|
||||
e1.Set();
|
||||
Assert.True(e2.Wait(Timeout), "timeout");
|
||||
}, null);
|
||||
});
|
||||
|
||||
Assert.True(e1.Wait(Timeout), "timeout");
|
||||
|
||||
// Act
|
||||
//
|
||||
// Dispatch this on the background thread because otherwise it would block.
|
||||
var task2 = Task.Run(() =>
|
||||
{
|
||||
e3.Set();
|
||||
context.Send((_) =>
|
||||
{
|
||||
e4.Set();
|
||||
}, null);
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.True(e3.Wait(Timeout), "timeout");
|
||||
Assert.True(e3.IsSet);
|
||||
|
||||
// Unblock the first item
|
||||
e2.Set();
|
||||
await task1;
|
||||
|
||||
await task2;
|
||||
Assert.True(e4.IsSet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Send_CapturesContext()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
|
||||
// CultureInfo uses the execution context.
|
||||
CultureInfo.CurrentCulture = new CultureInfo("en-GB");
|
||||
CultureInfo capturedCulture = null;
|
||||
|
||||
SynchronizationContext capturedContext = null;
|
||||
|
||||
// Act
|
||||
context.Send(async (_) =>
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
capturedCulture = CultureInfo.CurrentCulture;
|
||||
capturedContext = SynchronizationContext.Current;
|
||||
|
||||
e1.Set();
|
||||
}, null);
|
||||
|
||||
// Assert
|
||||
Assert.True(e1.Wait(Timeout), "timeout");
|
||||
Assert.Same(CultureInfo.CurrentCulture, capturedCulture);
|
||||
Assert.Same(context, capturedContext);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invoke_Void_CanRunSynchronously_WhenNotBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
Thread capturedThread = null;
|
||||
|
||||
// Act
|
||||
var task = context.Invoke(() =>
|
||||
{
|
||||
capturedThread = Thread.CurrentThread;
|
||||
});
|
||||
|
||||
// Assert
|
||||
await task;
|
||||
Assert.Same(thread, capturedThread);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invoke_Void_CanRunAsynchronously_WhenBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
Thread capturedThread = null;
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
var e2 = new ManualResetEventSlim();
|
||||
var e3 = new ManualResetEventSlim();
|
||||
|
||||
var task1 = Task.Run(() =>
|
||||
{
|
||||
context.Send((_) =>
|
||||
{
|
||||
e1.Set();
|
||||
Assert.True(e2.Wait(Timeout), "timeout");
|
||||
}, null);
|
||||
});
|
||||
|
||||
Assert.True(e1.Wait(Timeout), "timeout");
|
||||
|
||||
var task2 = context.Invoke(() =>
|
||||
{
|
||||
capturedThread = Thread.CurrentThread;
|
||||
|
||||
e3.Set();
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.False(e2.IsSet);
|
||||
e2.Set(); // Unblock the first item
|
||||
await task1;
|
||||
|
||||
Assert.True(e3.Wait(Timeout), "timeout");
|
||||
await task2;
|
||||
Assert.NotSame(thread, capturedThread);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invoke_Void_CanRethrowExceptions()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
|
||||
// Act
|
||||
var task = context.Invoke(() =>
|
||||
{
|
||||
throw new InvalidTimeZoneException();
|
||||
});
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<InvalidTimeZoneException>(async () => await task);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invoke_T_CanRunSynchronously_WhenNotBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
|
||||
// Act
|
||||
var task = context.Invoke(() =>
|
||||
{
|
||||
return Thread.CurrentThread;
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.Same(thread, await task);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invoke_T_CanRunAsynchronously_WhenBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
var e2 = new ManualResetEventSlim();
|
||||
var e3 = new ManualResetEventSlim();
|
||||
|
||||
var task1 = Task.Run(() =>
|
||||
{
|
||||
context.Send((_) =>
|
||||
{
|
||||
e1.Set();
|
||||
Assert.True(e2.Wait(Timeout), "timeout");
|
||||
}, null);
|
||||
});
|
||||
|
||||
Assert.True(e1.Wait(Timeout), "timeout");
|
||||
|
||||
var task2 = context.Invoke(() =>
|
||||
{
|
||||
e3.Set();
|
||||
|
||||
return Thread.CurrentThread;
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.False(e2.IsSet);
|
||||
e2.Set(); // Unblock the first item
|
||||
await task1;
|
||||
|
||||
Assert.True(e3.Wait(Timeout), "timeout");
|
||||
Assert.NotSame(thread, await task2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invoke_T_CanRethrowExceptions()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
|
||||
// Act
|
||||
var task = context.Invoke<string>(() =>
|
||||
{
|
||||
throw new InvalidTimeZoneException();
|
||||
});
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<InvalidTimeZoneException>(async () => await task);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_Void_CanRunSynchronously_WhenNotBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
Thread capturedThread = null;
|
||||
|
||||
// Act
|
||||
var task = context.Invoke(() =>
|
||||
{
|
||||
capturedThread = Thread.CurrentThread;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
// Assert
|
||||
await task;
|
||||
Assert.Same(thread, capturedThread);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_Void_CanRunAsynchronously_WhenBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
Thread capturedThread = null;
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
var e2 = new ManualResetEventSlim();
|
||||
var e3 = new ManualResetEventSlim();
|
||||
|
||||
var task1 = Task.Run(() =>
|
||||
{
|
||||
context.Send((_) =>
|
||||
{
|
||||
e1.Set();
|
||||
Assert.True(e2.Wait(Timeout), "timeout");
|
||||
}, null);
|
||||
});
|
||||
|
||||
Assert.True(e1.Wait(Timeout), "timeout");
|
||||
|
||||
var task2 = context.InvokeAsync(() =>
|
||||
{
|
||||
capturedThread = Thread.CurrentThread;
|
||||
|
||||
e3.Set();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.False(e2.IsSet);
|
||||
e2.Set(); // Unblock the first item
|
||||
await task1;
|
||||
|
||||
Assert.True(e3.Wait(Timeout), "timeout");
|
||||
await task2;
|
||||
Assert.NotSame(thread, capturedThread);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_Void_CanRethrowExceptions()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
|
||||
// Act
|
||||
var task = context.InvokeAsync(() =>
|
||||
{
|
||||
throw new InvalidTimeZoneException();
|
||||
});
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<InvalidTimeZoneException>(async () => await task);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_T_CanRunSynchronously_WhenNotBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
|
||||
// Act
|
||||
var task = context.InvokeAsync(() =>
|
||||
{
|
||||
return Task.FromResult(Thread.CurrentThread);
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.Same(thread, await task);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_T_CanRunAsynchronously_WhenBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
var e2 = new ManualResetEventSlim();
|
||||
var e3 = new ManualResetEventSlim();
|
||||
|
||||
var task1 = Task.Run(() =>
|
||||
{
|
||||
context.Send((_) =>
|
||||
{
|
||||
e1.Set();
|
||||
Assert.True(e2.Wait(Timeout), "timeout");
|
||||
}, null);
|
||||
});
|
||||
|
||||
Assert.True(e1.Wait(Timeout), "timeout");
|
||||
|
||||
var task2 = context.InvokeAsync(() =>
|
||||
{
|
||||
e3.Set();
|
||||
|
||||
return Task.FromResult(Thread.CurrentThread);
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.False(e2.IsSet);
|
||||
e2.Set(); // Unblock the first item
|
||||
await task1;
|
||||
|
||||
Assert.True(e3.Wait(Timeout), "timeout");
|
||||
Assert.NotSame(thread, await task2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_T_CanRethrowExceptions()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
|
||||
// Act
|
||||
var task = context.InvokeAsync<string>(() =>
|
||||
{
|
||||
throw new InvalidTimeZoneException();
|
||||
});
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<InvalidTimeZoneException>(async () => await task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Blazor.Server\Microsoft.AspNetCore.Blazor.Server.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
Reference in New Issue