Skip to content

Follow up 'Ability to monitor Blazor Server circuit activity' #47328

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Components/Server/src/Circuits/CircuitHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,11 @@ public abstract class CircuitHandler
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
public virtual Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken) => Task.CompletedTask;

/// <summary>
/// Creates a handler that gets invoked when inbound activity on the circuit causes an asynchronous task to be dispatched on the server.
/// </summary>
/// <param name="next">The next handler to invoke.</param>
/// <returns>A handler function that returns a <see cref="Task"/> that completes when the activity has finished.</returns>
public virtual Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(Func<CircuitInboundActivityContext, Task> next) => next;
}
21 changes: 9 additions & 12 deletions src/Components/Server/src/Circuits/CircuitHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -606,24 +606,21 @@ internal async Task<TResult> HandleInboundActivityAsync<TResult>(Func<Task<TResu

private static Func<Func<Task>, Task> BuildInboundActivityDispatcher(IReadOnlyList<CircuitHandler> circuitHandlers, Circuit circuit)
{
Func<CircuitInboundActivityContext, Task>? result = null;

for (var i = circuitHandlers.Count - 1; i >= 0; i--)
if (circuitHandlers.Count == 0)
{
if (circuitHandlers[i] is IHandleCircuitActivity inboundActivityHandler)
{
var next = result ?? (static (context) => context.Handler());
result = (context) => inboundActivityHandler.HandleInboundActivityAsync(context, next);
}
// If there are no registered handlers, there is no need to allocate a context on each call.
return static handler => handler();
}

if (result is null)
var result = static (CircuitInboundActivityContext context) => context.Handler();

for (var i = circuitHandlers.Count - 1; i >= 0; i--)
{
// If there are no registered handlers, there is no need to allocate a context on each call.
return static (handler) => handler();
var next = result;
result = circuitHandlers[i].CreateInboundActivityHandler(next);
}

return (handler) => result(new(handler, circuit));
return handler => result(new(handler, circuit));
}

private void AssertInitialized()
Expand Down
18 changes: 0 additions & 18 deletions src/Components/Server/src/Circuits/IHandleCircuitActivity.cs

This file was deleted.

3 changes: 1 addition & 2 deletions src/Components/Server/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#nullable enable
Microsoft.AspNetCore.Components.Server.Circuits.CircuitInboundActivityContext
Microsoft.AspNetCore.Components.Server.Circuits.CircuitInboundActivityContext.Circuit.get -> Microsoft.AspNetCore.Components.Server.Circuits.Circuit!
Microsoft.AspNetCore.Components.Server.Circuits.IHandleCircuitActivity
Microsoft.AspNetCore.Components.Server.Circuits.IHandleCircuitActivity.HandleInboundActivityAsync(Microsoft.AspNetCore.Components.Server.Circuits.CircuitInboundActivityContext! context, System.Func<Microsoft.AspNetCore.Components.Server.Circuits.CircuitInboundActivityContext!, System.Threading.Tasks.Task!>! next) -> System.Threading.Tasks.Task!
virtual Microsoft.AspNetCore.Components.Server.Circuits.CircuitHandler.CreateInboundActivityHandler(System.Func<Microsoft.AspNetCore.Components.Server.Circuits.CircuitInboundActivityContext!, System.Threading.Tasks.Task!>! next) -> System.Func<Microsoft.AspNetCore.Components.Server.Circuits.CircuitInboundActivityContext!, System.Threading.Tasks.Task!>!
57 changes: 43 additions & 14 deletions src/Components/Server/test/Circuits/CircuitHostTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ public async Task InitializeAsync_InvokesHandlers()
var handler2 = new Mock<CircuitHandler>(MockBehavior.Strict);
var sequence = new MockSequence();

SetupMockInboundActivityHandlers(sequence, handler1, handler2);

handler1
.InSequence(sequence)
.Setup(h => h.OnCircuitOpenedAsync(It.IsAny<Circuit>(), cancellationToken))
Expand Down Expand Up @@ -242,6 +244,8 @@ public async Task InitializeAsync_ReportsOwnAsyncExceptions()
var tcs = new TaskCompletionSource();
var reportedErrors = new List<UnhandledExceptionEventArgs>();

SetupMockInboundActivityHandler(handler);

handler
.Setup(h => h.OnCircuitOpenedAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()))
.Returns(tcs.Task)
Expand Down Expand Up @@ -284,6 +288,8 @@ public async Task DisposeAsync_InvokesCircuitHandler()
var handler2 = new Mock<CircuitHandler>(MockBehavior.Strict);
var sequence = new MockSequence();

SetupMockInboundActivityHandlers(sequence, handler1, handler2);

handler1
.InSequence(sequence)
.Setup(h => h.OnConnectionDownAsync(It.IsAny<Circuit>(), cancellationToken))
Expand Down Expand Up @@ -327,29 +333,31 @@ public async Task HandleInboundActivityAsync_InvokesCircuitActivityHandlers()
var handler3 = new Mock<CircuitHandler>(MockBehavior.Strict);
var sequence = new MockSequence();

// We deliberately avoid making handler2 an inbound activity handler
var activityHandler1 = handler1.As<IHandleCircuitActivity>();
var activityHandler3 = handler3.As<IHandleCircuitActivity>();

var asyncLocal1 = new AsyncLocal<bool>();
var asyncLocal3 = new AsyncLocal<bool>();

activityHandler1
handler3
.InSequence(sequence)
.Setup(h => h.HandleInboundActivityAsync(It.IsAny<CircuitInboundActivityContext>(), It.IsAny<Func<CircuitInboundActivityContext, Task>>()))
.Returns(async (CircuitInboundActivityContext context, Func<CircuitInboundActivityContext, Task> next) =>
.Setup(h => h.CreateInboundActivityHandler(It.IsAny<Func<CircuitInboundActivityContext, Task>>()))
.Returns((Func<CircuitInboundActivityContext, Task> next) => async (CircuitInboundActivityContext context) =>
{
asyncLocal1.Value = true;
asyncLocal3.Value = true;
await next(context);
})
.Verifiable();

activityHandler3
handler2
.InSequence(sequence)
.Setup(h => h.CreateInboundActivityHandler(It.IsAny<Func<CircuitInboundActivityContext, Task>>()))
.Returns((Func<CircuitInboundActivityContext, Task> next) => next)
.Verifiable();

handler1
.InSequence(sequence)
.Setup(h => h.HandleInboundActivityAsync(It.IsAny<CircuitInboundActivityContext>(), It.IsAny<Func<CircuitInboundActivityContext, Task>>()))
.Returns(async (CircuitInboundActivityContext context, Func<CircuitInboundActivityContext, Task> next) =>
.Setup(h => h.CreateInboundActivityHandler(It.IsAny<Func<CircuitInboundActivityContext, Task>>()))
.Returns((Func<CircuitInboundActivityContext, Task> next) => async (CircuitInboundActivityContext context) =>
{
asyncLocal3.Value = true;
asyncLocal1.Value = true;
await next(context);
})
.Verifiable();
Expand All @@ -367,8 +375,9 @@ await circuitHost.HandleInboundActivityAsync(() =>
});

// Assert
activityHandler1.VerifyAll();
activityHandler3.VerifyAll();
handler1.VerifyAll();
handler2.VerifyAll();
handler3.VerifyAll();

Assert.False(asyncLocal1.Value);
Assert.False(asyncLocal3.Value);
Expand Down Expand Up @@ -404,6 +413,26 @@ private static TestRemoteRenderer GetRemoteRenderer()
Mock.Of<IClientProxy>());
}

private static void SetupMockInboundActivityHandlers(MockSequence sequence, params Mock<CircuitHandler>[] circuitHandlers)
{
for (var i = circuitHandlers.Length - 1; i >= 0; i--)
{
circuitHandlers[i]
.InSequence(sequence)
.Setup(h => h.CreateInboundActivityHandler(It.IsAny<Func<CircuitInboundActivityContext, Task>>()))
.Returns((Func<CircuitInboundActivityContext, Task> next) => next)
.Verifiable();
}
}

private static void SetupMockInboundActivityHandler(Mock<CircuitHandler> circuitHandler)
{
circuitHandler
.Setup(h => h.CreateInboundActivityHandler(It.IsAny<Func<CircuitInboundActivityContext, Task>>()))
.Returns((Func<CircuitInboundActivityContext, Task> next) => next)
.Verifiable();
}

private class TestRemoteRenderer : RemoteRenderer
{
public TestRemoteRenderer(IServiceProvider serviceProvider, IClientProxy client)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@

namespace TestServer;

public class TestCircuitContextAccessor : CircuitHandler, IHandleCircuitActivity
public class TestCircuitContextAccessor : CircuitHandler
{
private readonly AsyncLocal<bool> _hasCircuitContext = new();

public bool HasCircuitContext => _hasCircuitContext.Value;

public async Task HandleInboundActivityAsync(CircuitInboundActivityContext context, Func<CircuitInboundActivityContext, Task> next)
{
_hasCircuitContext.Value = true;
await next(context);
_hasCircuitContext.Value = false;
}
public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
Func<CircuitInboundActivityContext, Task> next)
=> async (context) =>
{
_hasCircuitContext.Value = true;
await next(context);
_hasCircuitContext.Value = false;
};
}