Don't release SemaphoreSlim when it is canceled (#12818)

* Don't release SemaphoreSlim when it is cancelled

* fixed tests

* Rebased

* Updated ref

* mark test as flaky
This commit is contained in:
Ajay Bhargav Baaskaran 2019-08-14 12:40:44 -07:00 committed by GitHub
parent 1f7d59d8f1
commit c605d6cf63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 75 additions and 176 deletions

View File

@ -97,6 +97,7 @@ namespace Microsoft.AspNetCore.Components
public abstract partial class Dispatcher public abstract partial class Dispatcher
{ {
protected Dispatcher() { } protected Dispatcher() { }
public void AssertAccess() { }
public abstract bool CheckAccess(); public abstract bool CheckAccess();
public static Microsoft.AspNetCore.Components.Dispatcher CreateDefault() { throw null; } public static Microsoft.AspNetCore.Components.Dispatcher CreateDefault() { throw null; }
public abstract System.Threading.Tasks.Task InvokeAsync(System.Action workItem); public abstract System.Threading.Tasks.Task InvokeAsync(System.Action workItem);

View File

@ -97,6 +97,7 @@ namespace Microsoft.AspNetCore.Components
public abstract partial class Dispatcher public abstract partial class Dispatcher
{ {
protected Dispatcher() { } protected Dispatcher() { }
public void AssertAccess() { }
public abstract bool CheckAccess(); public abstract bool CheckAccess();
public static Microsoft.AspNetCore.Components.Dispatcher CreateDefault() { throw null; } public static Microsoft.AspNetCore.Components.Dispatcher CreateDefault() { throw null; }
public abstract System.Threading.Tasks.Task InvokeAsync(System.Action workItem); public abstract System.Threading.Tasks.Task InvokeAsync(System.Action workItem);

View File

@ -23,6 +23,20 @@ namespace Microsoft.AspNetCore.Components
/// </summary> /// </summary>
internal event UnhandledExceptionEventHandler UnhandledException; internal event UnhandledExceptionEventHandler UnhandledException;
/// <summary>
/// Validates that the currently executing code is running inside the dispatcher.
/// </summary>
public void AssertAccess()
{
if (!CheckAccess())
{
throw new InvalidOperationException(
"The current thread is not associated with the Dispatcher. " +
"Use InvokeAsync() to switch execution to the Dispatcher when " +
"triggering rendering or component state.");
}
}
/// <summary> /// <summary>
/// Returns a value that determines whether using the dispatcher to invoke a work item is required /// Returns a value that determines whether using the dispatcher to invoke a work item is required
/// from the current context. /// from the current context.

View File

@ -209,7 +209,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// </returns> /// </returns>
public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo fieldInfo, EventArgs eventArgs) public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo fieldInfo, EventArgs eventArgs)
{ {
EnsureSynchronizationContext(); Dispatcher.AssertAccess();
if (!_eventBindings.TryGetValue(eventHandlerId, out var callback)) if (!_eventBindings.TryGetValue(eventHandlerId, out var callback))
{ {
@ -337,7 +337,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// <param name="renderFragment">A <see cref="RenderFragment"/> that will supply the updated UI contents.</param> /// <param name="renderFragment">A <see cref="RenderFragment"/> that will supply the updated UI contents.</param>
internal void AddToRenderQueue(int componentId, RenderFragment renderFragment) internal void AddToRenderQueue(int componentId, RenderFragment renderFragment)
{ {
EnsureSynchronizationContext(); Dispatcher.AssertAccess();
var componentState = GetOptionalComponentState(componentId); var componentState = GetOptionalComponentState(componentId);
if (componentState == null) if (componentState == null)
@ -374,21 +374,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
return eventHandlerId; return eventHandlerId;
} }
private void EnsureSynchronizationContext()
{
// Render operations are not thread-safe, so they need to be serialized by the dispatcher.
// Plus, any other logic that mutates state accessed during rendering also
// needs not to run concurrently with rendering so should be dispatched to
// the renderer's sync context.
if (!Dispatcher.CheckAccess())
{
throw new InvalidOperationException(
"The current thread is not associated with the Dispatcher. " +
"Use Invoke() or InvokeAsync() to switch execution to the Dispatcher when " +
"triggering rendering or modifying any state accessed during rendering.");
}
}
private ComponentState GetRequiredComponentState(int componentId) private ComponentState GetRequiredComponentState(int componentId)
=> _componentStateById.TryGetValue(componentId, out var componentState) => _componentStateById.TryGetValue(componentId, out var componentState)
? componentState ? componentState
@ -414,7 +399,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
private void ProcessRenderQueue() private void ProcessRenderQueue()
{ {
EnsureSynchronizationContext(); Dispatcher.AssertAccess();
if (_isBatchInProgress) if (_isBatchInProgress)
{ {

View File

@ -18,7 +18,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
internal class CircuitHost : IAsyncDisposable internal class CircuitHost : IAsyncDisposable
{ {
private readonly SemaphoreSlim HandlerLock = new SemaphoreSlim(1);
private readonly IServiceScope _scope; private readonly IServiceScope _scope;
private readonly CircuitOptions _options; private readonly CircuitOptions _options;
private readonly CircuitHandler[] _circuitHandlers; private readonly CircuitHandler[] _circuitHandlers;
@ -187,35 +186,28 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
Log.CircuitOpened(_logger, Circuit.Id); Log.CircuitOpened(_logger, Circuit.Id);
await HandlerLock.WaitAsync(cancellationToken); Renderer.Dispatcher.AssertAccess();
try List<Exception> exceptions = null;
for (var i = 0; i < _circuitHandlers.Length; i++)
{ {
List<Exception> exceptions = null; var circuitHandler = _circuitHandlers[i];
try
for (var i = 0; i < _circuitHandlers.Length; i++)
{ {
var circuitHandler = _circuitHandlers[i]; await circuitHandler.OnCircuitOpenedAsync(Circuit, cancellationToken);
try
{
await circuitHandler.OnCircuitOpenedAsync(Circuit, cancellationToken);
}
catch (Exception ex)
{
Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnCircuitOpenedAsync), ex);
exceptions ??= new List<Exception>();
exceptions.Add(ex);
}
} }
catch (Exception ex)
if (exceptions != null)
{ {
throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions); Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnCircuitOpenedAsync), ex);
exceptions ??= new List<Exception>();
exceptions.Add(ex);
} }
} }
finally
if (exceptions != null)
{ {
HandlerLock.Release(); throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions);
} }
} }
@ -223,35 +215,28 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
Log.ConnectionUp(_logger, Circuit.Id, Client.ConnectionId); Log.ConnectionUp(_logger, Circuit.Id, Client.ConnectionId);
await HandlerLock.WaitAsync(cancellationToken); Renderer.Dispatcher.AssertAccess();
List<Exception> exceptions = null;
try for (var i = 0; i < _circuitHandlers.Length; i++)
{ {
List<Exception> exceptions = null; var circuitHandler = _circuitHandlers[i];
try
for (var i = 0; i < _circuitHandlers.Length; i++)
{ {
var circuitHandler = _circuitHandlers[i]; await circuitHandler.OnConnectionUpAsync(Circuit, cancellationToken);
try
{
await circuitHandler.OnConnectionUpAsync(Circuit, cancellationToken);
}
catch (Exception ex)
{
Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnConnectionUpAsync), ex);
exceptions ??= new List<Exception>();
exceptions.Add(ex);
}
} }
catch (Exception ex)
if (exceptions != null)
{ {
throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions); Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnConnectionUpAsync), ex);
exceptions ??= new List<Exception>();
exceptions.Add(ex);
} }
} }
finally
if (exceptions != null)
{ {
HandlerLock.Release(); throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions);
} }
} }
@ -259,35 +244,28 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
Log.ConnectionDown(_logger, Circuit.Id, Client.ConnectionId); Log.ConnectionDown(_logger, Circuit.Id, Client.ConnectionId);
await HandlerLock.WaitAsync(cancellationToken); Renderer.Dispatcher.AssertAccess();
List<Exception> exceptions = null;
try for (var i = 0; i < _circuitHandlers.Length; i++)
{ {
List<Exception> exceptions = null; var circuitHandler = _circuitHandlers[i];
try
for (var i = 0; i < _circuitHandlers.Length; i++)
{ {
var circuitHandler = _circuitHandlers[i]; await circuitHandler.OnConnectionDownAsync(Circuit, cancellationToken);
try
{
await circuitHandler.OnConnectionDownAsync(Circuit, cancellationToken);
}
catch (Exception ex)
{
Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnConnectionDownAsync), ex);
exceptions ??= new List<Exception>();
exceptions.Add(ex);
}
} }
catch (Exception ex)
if (exceptions != null)
{ {
throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions); Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnConnectionDownAsync), ex);
exceptions ??= new List<Exception>();
exceptions.Add(ex);
} }
} }
finally
if (exceptions != null)
{ {
HandlerLock.Release(); throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions);
} }
} }
@ -295,35 +273,26 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
Log.CircuitClosed(_logger, Circuit.Id); Log.CircuitClosed(_logger, Circuit.Id);
await HandlerLock.WaitAsync(cancellationToken); List<Exception> exceptions = null;
try for (var i = 0; i < _circuitHandlers.Length; i++)
{ {
List<Exception> exceptions = null; var circuitHandler = _circuitHandlers[i];
try
for (var i = 0; i < _circuitHandlers.Length; i++)
{ {
var circuitHandler = _circuitHandlers[i]; await circuitHandler.OnCircuitClosedAsync(Circuit, cancellationToken);
try
{
await circuitHandler.OnCircuitClosedAsync(Circuit, cancellationToken);
}
catch (Exception ex)
{
Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnCircuitClosedAsync), ex);
exceptions ??= new List<Exception>();
exceptions.Add(ex);
}
} }
catch (Exception ex)
if (exceptions != null)
{ {
throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions); Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnCircuitClosedAsync), ex);
exceptions ??= new List<Exception>();
exceptions.Add(ex);
} }
} }
finally
if (exceptions != null)
{ {
HandlerLock.Release(); throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions);
} }
} }

View File

@ -270,46 +270,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId, out _)); Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId, out _));
} }
[Fact]
public async Task Connect_WhileDisconnectIsInProgress_SeriallyExecutesCircuitHandlers()
{
// Arrange
var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory();
var registry = new TestCircuitRegistry(circuitIdFactory);
registry.BeforeDisconnect = new ManualResetEventSlim();
// This verifies that connection up \ down events on a circuit handler are always invoked serially.
var circuitHandler = new SerialCircuitHandler();
var tcs = new TaskCompletionSource<int>();
var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId(), handlers: new[] { circuitHandler });
registry.Register(circuitHost);
var client = Mock.Of<IClientProxy>();
var newId = "new-connection";
// Act
var disconnect = Task.Run(() =>
{
var task = registry.DisconnectAsync(circuitHost, circuitHost.Client.ConnectionId);
tcs.SetResult(0);
return task;
});
var connect = Task.Run(async () =>
{
registry.BeforeDisconnect.Set();
await tcs.Task;
await registry.ConnectAsync(circuitHost.CircuitId, client, newId, default);
});
await Task.WhenAll(disconnect, connect);
// Assert
Assert.Single(registry.ConnectedCircuits.Values);
Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId, out _));
Assert.True(circuitHandler.OnConnectionDownExecuted, "OnConnectionDownAsync should have been executed.");
Assert.True(circuitHandler.OnConnectionUpExecuted, "OnConnectionUpAsync should have been executed.");
}
[Fact] [Fact]
public async Task DisconnectWhenAConnectIsInProgress() public async Task DisconnectWhenAConnectIsInProgress()
{ {
@ -444,37 +404,5 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
NullLogger<CircuitRegistry>.Instance, NullLogger<CircuitRegistry>.Instance,
factory ?? TestCircuitIdFactory.CreateTestFactory()); factory ?? TestCircuitIdFactory.CreateTestFactory());
} }
private class SerialCircuitHandler : CircuitHandler
{
private readonly SemaphoreSlim _sempahore = new SemaphoreSlim(1);
public bool OnConnectionUpExecuted { get; private set; }
public bool OnConnectionDownExecuted { get; private set; }
public override async Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
{
Assert.True(await _sempahore.WaitAsync(0), "This should be serialized and consequently without contention");
await Task.Delay(10);
Assert.False(OnConnectionUpExecuted);
Assert.True(OnConnectionDownExecuted);
OnConnectionUpExecuted = true;
_sempahore.Release();
}
public override async Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken)
{
Assert.True(await _sempahore.WaitAsync(0), "This should be serialized and consequently without contention");
await Task.Delay(10);
Assert.False(OnConnectionUpExecuted);
Assert.False(OnConnectionDownExecuted);
OnConnectionDownExecuted = true;
_sempahore.Release();
}
}
} }
} }

View File

@ -264,6 +264,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
} }
[Fact] [Fact]
[Flaky("https://github.com/aspnet/AspNetCore/issues/13086", FlakyOn.AzP.Windows)]
public async Task ContinuesWorkingAfterInvalidAsyncReturnCallback() public async Task ContinuesWorkingAfterInvalidAsyncReturnCallback()
{ {
// Arrange // Arrange

View File

@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
appElement.FindElement(By.Id("run-without-dispatch")).Click(); appElement.FindElement(By.Id("run-without-dispatch")).Click();
Browser.Contains( Browser.Contains(
$"{typeof(InvalidOperationException).FullName}: The current thread is not associated with the Dispatcher. Use Invoke() or InvokeAsync() to switch execution to the Dispatcher when triggering rendering or modifying any state accessed during rendering.", $"{typeof(InvalidOperationException).FullName}: The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.",
() => result.Text); () => result.Text);
} }
} }