-
Notifications
You must be signed in to change notification settings - Fork 24.8k
Added improvement in documentation for parallel streaming invocations limit #37194
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
1109b77
507d76d
89e978e
41b68c6
7908545
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -251,6 +251,125 @@ public class ChatHub : Hub | |
| > [!NOTE] | ||
| > This feature makes use of <xref:Microsoft.Extensions.DependencyInjection.IServiceProviderIsService>, which is optionally implemented by DI implementations. If the app's DI container doesn't support this feature, injecting services into hub methods isn't supported. | ||
|
|
||
| ## Limit per-connection streaming invocations | ||
|
|
||
| `HubOptions.MaximumParallelInvocationsPerClient` controls non-streaming hub invocations only. It does not apply to streaming hub invocations. Streaming invocations are expected to be long-running and can run concurrently. To enforce a per-connection limit for streaming invocations, use a hub filter to track active stream-returning hub methods. | ||
|
|
||
| ```csharp | ||
| using System.Collections.Concurrent; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we also need: |
||
|
|
||
| public class StreamConcurrencyFilter : IHubFilter | ||
| { | ||
| private readonly ConcurrentDictionary<string, int> _activeStreams = new(); | ||
| private readonly int _maxConcurrentStreams; | ||
|
|
||
| public StreamConcurrencyFilter(int maxConcurrentStreams = 1) | ||
| { | ||
| _maxConcurrentStreams = maxConcurrentStreams; | ||
| } | ||
|
|
||
| public async ValueTask<object?> InvokeMethodAsync( | ||
| HubInvocationContext invocationContext, | ||
| Func<HubInvocationContext, ValueTask<object?>> next) | ||
| { | ||
| if (!IsStreamingInvocation(invocationContext.HubMethod.ReturnType)) | ||
| { | ||
| return await next(invocationContext); | ||
| } | ||
|
|
||
| var connectionId = invocationContext.Context.ConnectionId; | ||
| if (connectionId is null) | ||
| { | ||
| return await next(invocationContext); | ||
| } | ||
|
|
||
| var activeStreams = _activeStreams.AddOrUpdate( | ||
| connectionId, | ||
| 1, | ||
| (_, current) => current + 1); | ||
|
|
||
| if (activeStreams > _maxConcurrentStreams) | ||
| { | ||
| Decrement(connectionId); | ||
| throw new HubException($"The connection is limited to {_maxConcurrentStreams} concurrent streaming invocations."); | ||
| } | ||
|
|
||
| try | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had to look at this a few times, but I think the limit would never be honored. The count is decremented the moment the stream starts delivering, not when the stream actually finishes. So it seems for example, we could start 100 concurrent streams even though there is a limit of 1, because the counter never stays elevated long enough to block as intended, so no actual concurrency control. @BrennanConroy, what would be ther correct pattern to suggest and document here? Also, should this be using
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @BrennanConroy, could you weigh in on the correct pattern here for above? It is not clear to me what it should be.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My initial thought would have been code in the Hub itself to handle this: (pseudo code) e.g. The hub filter is an interesting idea, unfortunately you would need to use some reflection to achieve the same behavior: Reflection IHubFilter Pseudo Code |
||
| { | ||
| return await next(invocationContext); | ||
| } | ||
| finally | ||
| { | ||
| Decrement(connectionId); | ||
| } | ||
| } | ||
|
|
||
| private static bool IsStreamingInvocation(Type returnType) | ||
| { | ||
| if (returnType is null) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| if (returnType.IsGenericType) | ||
| { | ||
| var genericDefinition = returnType.GetGenericTypeDefinition(); | ||
| if (genericDefinition == typeof(IAsyncEnumerable<>) || | ||
| genericDefinition == typeof(ChannelReader<>)) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| if (genericDefinition == typeof(Task<>)) | ||
| { | ||
| return IsStreamingInvocation(returnType.GetGenericArguments()[0]); | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| private void Decrement(string connectionId) | ||
| { | ||
| while (true) | ||
| { | ||
| if (!_activeStreams.TryGetValue(connectionId, out var current)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (current <= 1) | ||
| { | ||
| if (_activeStreams.TryRemove(connectionId, out _)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| continue; | ||
| } | ||
|
|
||
| if (_activeStreams.TryUpdate(connectionId, current - 1, current)) | ||
| { | ||
| return; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Register the filter as a singleton so the active-stream count is shared across hub invocations for the same connection: | ||
|
|
||
| ```csharp | ||
| builder.Services.AddSingleton(new StreamConcurrencyFilter(maxConcurrentStreams: 2)); | ||
| builder.Services.AddSignalR(options => | ||
| { | ||
| options.AddFilter<StreamConcurrencyFilter>(); | ||
| }); | ||
| ``` | ||
|
|
||
| > [!NOTE] | ||
| > This filter applies only to methods that return `IAsyncEnumerable<T>` or `ChannelReader<T>` and does not change the behavior of non-streaming hub methods. | ||
|
|
||
| ### Keyed services support in Dependency Injection | ||
|
|
||
| *Keyed services* refers to a mechanism for registering and retrieving Dependency Injection (DI) services using keys. A service is associated with a key by calling <xref:Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddKeyedSingleton%2A> (or `AddKeyedScoped` or `AddKeyedTransient`) to register it. Access a registered service by specifying the key with the [`[FromKeyedServices]`](xref:Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribute) attribute. The following code shows how to use keyed services: | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
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.
This new section would need to move to after the sub section Keyed Services support in Dependancy Injection.
"Keyed services support" is conceptually a subtopic of "Inject services into a hub"
So the organizaiton would like like this:
## Inject services into a hub(existing content)
### Keyed services support in Dependency Injection(existing content)
## Limit per-connection streaming invocations(new content)
## Handle events for a connection