-
Notifications
You must be signed in to change notification settings - Fork 10.3k
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
Ability to monitor Blazor Server circuit activity #46968
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The additional handlers I've added so far are:
- Unhandled exceptions
- Starting an invocation to .NET from JS
- Ending an invocation to JS from .NET
The start/end disparity for the interop method handlers was to keep all circuit handler invocation logic in CircuitHost
. This was sort of an arbitrary decision, so I'm definitely happy to reconsider it.
Some other potential CircuitHandler
events:
- A render completes
- The page's location changes
- Anything related to transferring streams/arrays between .NET and JS
Before we close on this, can we step back and work through the design justification? I know that #30287 says "callbacks when an event, JS interop call, or other activity" which in turn is copied from #27885, but we would be wise to spell out the customer scenarios this addresses. I don't think we went through any structured design process to get here (and apologies for that - we just filed vague issues and then ran with it). The one scenario I know for sure is "detecting idleness", however that could be covered by a much more general Let's make sure we don't try to come up with scenarios that happen to fit this design in a post-hoc way, but rather consider if there are viable options to minimize the API surface to keep options open. |
My original view on this scenario has always been to extend the current support that we have for circuithandlers to also support other interactions. The goal for me is that we have some sort of filter/middleware where people can perform centralized actions like:
I am fine if we add a single method "RegisterCircuitActivityEvent" to CircuitHandler that gets called to register a callback with the shape async (evt, circuit, handler) => {
Class.AsyncLocal = value;
await handler();
Class.AsyncLocal = null;
} Or in the future tackling other things like terminating inactive circuits. I don't want us to expose any of the internals of the hub method parameters or anything like that. |
In your scenario list, is item 3 a special case of item 2? Or is some distinct API required for item 3? I'm speculating that what's common to all cases is the idea of "inbound activity". None of the cases require differentiating what sort of inbound activity it is (and it would be ideal not to put that on the API), but it is meaningful to talk about the activity being inbound because people would not want to set up async local state around outbound activity. |
It's fair to say that they are the same, just adding concrete examples of specific problems.
|
OK great, that helps to clarify things a lot. So can we nail down a definite API proposal? The simplest reasonable starting point, which is almost exactly what @MackinnonBuck has so far: namespace Microsoft.AspNetCore.Components.Server.Circuits;
public abstract class CircuitHandler
{
+ public virtual Task OnInboundActivityAsync(Circuit circuit, CancellationToken cancellationToken);
} With the above known scenarios, can we get away with it being this simple? Questions:
|
I am fine with the simpler API, but the idea was that if you don't register anything, we skip it alltogether. Maybe we can use an additional interface to signal that.
It needs to wrap the call we make, otherwise AsyncLocals won't work. So it needs to be something like:
If we don't want to "close" over variables, we can pass in a handler(CircuitInvocationContext),CircuitInvocationContext) where |
Thanks, @javiercn and @SteveSandersonMS! The scenarios are clearer to me now. I've updated my prototype to introduce just a single new method: namespace Microsoft.AspNetCore.Components.Server.Circuits;
public abstract class CircuitHandler
{
+ public virtual Task HandleInboundActivityAsync(Circuit circuit, Func<Task> next);
} I named it This leads to another question - does this method even belong in
Some other questions I have:
Thoughts? |
Great point. Totally agreed!
Also agreed. I don't see any real drawback to it being here - it's the most natural place in the existing API structure.
Probably not at this point. Our known scenarios don't include that requirement, and the more detail we expose, the more our internal implementation choices become limited. I do concur with your suggestion about A CircuitEventContext read-only struct to replace the existing circuit argument. I think that's also roughly what Javier was suggesting. Example: public abstract class CircuitHandler
{
+ public virtual Task HandleInboundActivityAsync(CircuitInboundActivityContext context, Func<Task> next);
}
+// It's important to have at least one public field otherwise adding any other fields in the future would be breaking
+// See "Adding an instance field to a struct that has no nonpublic fields" in https://learn.microsoft.com/en-us/dotnet/core/compatibility/library-change-rules
+public readonly struct CircuitInboundActivityContext
+{
+ public Circuit Circuit { get; }
+} ... and then we're free to add other info to
Also agreed that it doesn't seem like we'd want to define all activity as being "inbound". We can document the kinds of things that trigger this callback. |
src/Components/Server/src/Circuits/ICircuitInboundEventHandler.cs
Outdated
Show resolved
Hide resolved
src/Components/Server/src/Circuits/ICircuitInboundEventHandler.cs
Outdated
Show resolved
Hide resolved
src/Components/Server/src/Circuits/CircuitInboundEventDelegate.cs
Outdated
Show resolved
Hide resolved
public class TestCircuitContextAccessor : CircuitHandler, ICircuitInboundEventHandler | ||
{ | ||
private readonly AsyncLocal<bool> _hasCircuitContext = new(); | ||
|
||
public bool HasCircuitContext => _hasCircuitContext.Value; | ||
|
||
public async Task HandleInboundEventAsync(CircuitInboundEventContext context, CircuitInboundEventDelegate next) | ||
{ | ||
_hasCircuitContext.Value = true; | ||
await next(context); | ||
_hasCircuitContext.Value = false; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will be interesting that we update the docs for HttpClientFactory with this approach, as well as offer other examples, like making values available to OwningComponentBase and other components.
@@ -324,7 +327,7 @@ public async Task OnRenderCompletedAsync(long renderId, string errorMessageOrNul | |||
|
|||
try | |||
{ | |||
_ = Renderer.OnRenderCompletedAsync(renderId, errorMessageOrNull); | |||
_ = HandleInboundActivityAsync(() => Renderer.OnRenderCompletedAsync(renderId, errorMessageOrNull)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The following is nitpicky so feel free to ignore it, especially if you think it makes the code unpleasant!
It may be possible to avoid the extra allocation caused by capturing these locals in the closure by redefining HandleInboundActivityAsync
as HandleInboundActivityAsync<T>(T data, Func<T, Task> callback)
, then passing in the otherwise-captured values as the T data
parameter (for example, the caller could pass a tuple containing whatever combination of values they want). The lambda can then be defined as static
.
Not sure if that's justified or would make the code hard to read.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think in order to make this work, we would have to include data
in the CircuitInboundActivityContext
class so it can be passed all the way through to the last handler (otherwise a capture would have to happen somewhere else). But then we would probably need to make CircuitInboundActivityContext
generic in order to avoid boxing the data
parameter. And then we would need to introduce a non-generic base type for CircuitInboundActivityContext
to avoid also making IHandleCircuitActivity.HandleInboundActivityAsync
generic.
Or maybe there's a way simpler way of doing this that I'm missing, but if it really does require that we do all this ^ then I think it might be best to leave as is. Is that okay?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am ok if we leave it as is for now, and file an issue to revisit. If in the case that nobody registers an IHandleCircuitActivity
implementation we don't allocate, I think it is ok to leave it like this for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
Ability to monitor Blazor Server circuit activity
Adds the ability to monitor circuit events such as unhandled exceptions and JS interop calls.
Description
This PR adds functionality to the existing
CircuitHandler
to also support non-lifecycle circuit events.Opening initially as a draft PR to gather feedback. I'll add new tests after we validate the overall direction.
Fixes #30287