-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Add ability to set max calls across all sessions #23282
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
Changes from all commits
eed31d7
8e2c96b
2092b01
02c83a0
b130ff5
af22c64
7aa7565
7cd171a
38d6f82
7e065b7
97686aa
1ce7a8d
ba2ec6d
6745a75
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 |
|---|---|---|
|
|
@@ -146,28 +146,30 @@ public override bool IsDisposed | |
| private string _sendViaReceiverEntityPath; | ||
|
|
||
| private readonly object _syncLock = new(); | ||
| private readonly TimeSpan _operationTimeout; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="AmqpConnectionScope"/> class. | ||
| /// </summary> | ||
| /// | ||
| /// <param name="serviceEndpoint">Endpoint for the Service Bus service to which the scope is associated.</param> | ||
| /// <param name="credential">The credential to use for authorization with the Service Bus service.</param> | ||
| /// <param name="transport">The transport to use for communication.</param> | ||
| /// <param name="proxy">The proxy, if any, to use for communication.</param> | ||
| /// <param name="useSingleSession">If true, all links will use a single session.</param> | ||
| /// | ||
| /// <param name="operationTimeout">The timeout for operations associated with the connection.</param> | ||
| public AmqpConnectionScope( | ||
| Uri serviceEndpoint, | ||
| ServiceBusTokenCredential credential, | ||
| ServiceBusTransportType transport, | ||
| IWebProxy proxy, | ||
| bool useSingleSession) | ||
| bool useSingleSession, | ||
| TimeSpan operationTimeout) | ||
| { | ||
| Argument.AssertNotNull(serviceEndpoint, nameof(serviceEndpoint)); | ||
| Argument.AssertNotNull(credential, nameof(credential)); | ||
| ValidateTransport(transport); | ||
|
|
||
| _operationTimeout = operationTimeout; | ||
| ServiceEndpoint = serviceEndpoint; | ||
| Transport = transport; | ||
| Proxy = proxy; | ||
|
|
@@ -506,6 +508,7 @@ protected virtual async Task<RequestResponseAmqpLink> CreateManagementLinkAsync( | |
| var linkSettings = new AmqpLinkSettings(); | ||
| linkSettings.AddProperty(AmqpClientConstants.TimeoutName, (uint)timeout.CalculateRemaining(stopWatch.GetElapsedTime()).TotalMilliseconds); | ||
| linkSettings.AddProperty(AmqpClientConstants.EntityTypeName, AmqpClientConstants.EntityTypeManagement); | ||
| linkSettings.OperationTimeout = _operationTimeout; | ||
| entityPath += '/' + AmqpClientConstants.ManagementAddress; | ||
|
|
||
| // Perform the initial authorization for the link. | ||
|
|
@@ -639,7 +642,8 @@ protected virtual async Task<ReceivingAmqpLink> CreateReceivingLinkAsync( | |
| AutoSendFlow = prefetchCount > 0, | ||
| SettleType = (receiveMode == ServiceBusReceiveMode.PeekLock) ? SettleMode.SettleOnDispose : SettleMode.SettleOnSend, | ||
| Source = new Source { Address = endpoint.AbsolutePath, FilterSet = filters }, | ||
| Target = new Target { Address = Guid.NewGuid().ToString() } | ||
| Target = new Target { Address = Guid.NewGuid().ToString() }, | ||
| OperationTimeout = _operationTimeout | ||
|
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. 👍 |
||
| }; | ||
|
|
||
| var link = new ReceivingAmqpLink(linkSettings); | ||
|
|
@@ -777,7 +781,8 @@ protected virtual async Task<SendingAmqpLink> CreateSendingLinkAsync( | |
| Role = false, | ||
| InitialDeliveryCount = 0, | ||
| Source = new Source { Address = Guid.NewGuid().ToString() }, | ||
| Target = new Target { Address = destinationEndpoint.AbsolutePath } | ||
| Target = new Target { Address = destinationEndpoint.AbsolutePath }, | ||
| OperationTimeout = _operationTimeout | ||
| }; | ||
|
|
||
| linkSettings.AddProperty(AmqpClientConstants.TimeoutName, (uint)timeout.CalculateRemaining(stopWatch.GetElapsedTime()).TotalMilliseconds); | ||
|
|
@@ -1046,40 +1051,9 @@ protected virtual async Task OpenAmqpObjectAsync( | |
| string entityPath = default, | ||
| bool isProcessor = default) | ||
| { | ||
| CancellationTokenRegistration registration; | ||
| try | ||
| { | ||
| var openObjectCompletionSource = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); | ||
| // Only allow cancelling in-flight opens when it is from a processor. | ||
| // This would occur when the processor is stopped or closed by the user. | ||
| if (isProcessor) | ||
| { | ||
| // use a static delegate with tuple state to avoid allocating a closure | ||
| registration = cancellationToken.Register(static state => | ||
| { | ||
| var (tcs, target) = ((TaskCompletionSource<object>, AmqpObject))state; | ||
| if (tcs.TrySetCanceled()) | ||
| { | ||
| target.SafeClose(); | ||
| } | ||
| }, (openObjectCompletionSource, target), useSynchronizationContext: false); | ||
| } | ||
|
|
||
| static async Task Open(AmqpObject target, TimeSpan timeout, TaskCompletionSource<object> openObjectCompletionSource) | ||
| { | ||
| try | ||
| { | ||
| await target.OpenAsync(timeout).ConfigureAwait(false); | ||
| openObjectCompletionSource.TrySetResult(null); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| openObjectCompletionSource.TrySetException(ex); | ||
| } | ||
| } | ||
|
|
||
| _ = Open(target, timeout, openObjectCompletionSource); | ||
| await openObjectCompletionSource.Task.ConfigureAwait(false); | ||
| await target.OpenAsync(cancellationToken).ConfigureAwait(false); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
|
|
@@ -1110,13 +1084,6 @@ static async Task Open(AmqpObject target, TimeSpan timeout, TaskCompletionSource | |
| throw; | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| if (isProcessor) | ||
| { | ||
| registration.Dispose(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,10 +46,10 @@ public class ServiceBusProcessor : IAsyncDisposable | |
| /// The primitive for ensuring that the service is not overloaded with | ||
| /// accept session requests. | ||
| /// </summary> | ||
| private SemaphoreSlim MaxConcurrentAcceptSessionsSemaphore { get; } | ||
| private readonly SemaphoreSlim _maxConcurrentAcceptSessionsSemaphore = new(0, int.MaxValue); | ||
|
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. I'm guessing we don't want to recreate this when concurrency is changed?
Member
Author
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. Correct, we use the same approach as for the other semaphore - release/WaitAsync to change available threads |
||
|
|
||
| /// <summary>The primitive for synchronizing access during start and close operations.</summary> | ||
| private readonly SemaphoreSlim _processingStartStopSemaphore = new SemaphoreSlim(1, 1); | ||
| private readonly SemaphoreSlim _processingStartStopSemaphore = new(1, 1); | ||
|
|
||
| private CancellationTokenSource RunningTaskTokenSource { get; set; } | ||
|
|
||
|
|
@@ -123,6 +123,8 @@ public class ServiceBusProcessor : IAsyncDisposable | |
| internal int MaxConcurrentCallsPerSession => _maxConcurrentCallsPerSession; | ||
| private volatile int _maxConcurrentCallsPerSession; | ||
|
|
||
| private int _currentAcceptSessions; | ||
|
|
||
| internal TimeSpan? MaxReceiveWaitTime { get; } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -188,7 +190,6 @@ public virtual bool IsClosed | |
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="ServiceBusProcessor"/> class. | ||
| /// </summary> | ||
| /// | ||
| /// <param name="connection">The <see cref="ServiceBusConnection" /> connection to use for communication with the Service Bus service.</param> | ||
| /// <param name="entityPath">The queue name or subscription path to process messages from.</param> | ||
| /// <param name="isSessionEntity">Whether or not the processor is associated with a session entity.</param> | ||
|
|
@@ -199,6 +200,7 @@ public virtual bool IsClosed | |
| /// Only applies if isSessionEntity is true.</param> | ||
| /// <param name="maxConcurrentCallsPerSession">The max number of concurrent calls per session. | ||
| /// Only applies if isSessionEntity is true.</param> | ||
| /// <param name="maxConcurrentCallsAcrossAllSessions">The max number of concurrent calls across all sessions.</param> | ||
| /// <param name="sessionProcessor">If this is for a session processor, will contain the session processor instance.</param> | ||
| internal ServiceBusProcessor( | ||
| ServiceBusConnection connection, | ||
|
|
@@ -208,6 +210,7 @@ internal ServiceBusProcessor( | |
| string[] sessionIds = default, | ||
| int maxConcurrentSessions = default, | ||
| int maxConcurrentCallsPerSession = default, | ||
| int? maxConcurrentCallsAcrossAllSessions = default, | ||
| ServiceBusSessionProcessor sessionProcessor = default) | ||
| { | ||
| Argument.AssertNotNullOrWhiteSpace(entityPath, nameof(entityPath)); | ||
|
|
@@ -232,16 +235,9 @@ internal ServiceBusProcessor( | |
|
|
||
| if (isSessionEntity) | ||
| { | ||
| _maxConcurrentCalls = _sessionIds.Length > 0 | ||
| ? Math.Min(_sessionIds.Length, _maxConcurrentSessions) | ||
| : _maxConcurrentSessions * _maxConcurrentCallsPerSession; | ||
| SetMaxConcurrentCallsAcrossSessions(maxConcurrentCallsAcrossAllSessions); | ||
| } | ||
|
|
||
| var maxAcceptSessions = Math.Min(_maxConcurrentCalls, 2 * Environment.ProcessorCount); | ||
| MaxConcurrentAcceptSessionsSemaphore = new SemaphoreSlim( | ||
| maxAcceptSessions, | ||
| maxAcceptSessions); | ||
|
|
||
| AutoCompleteMessages = Options.AutoCompleteMessages; | ||
|
|
||
| IsSessionProcessor = isSessionEntity; | ||
|
|
@@ -607,7 +603,7 @@ private void ReconcileReceiverManagers(int maxConcurrentSessions) | |
| new SessionReceiverManager( | ||
| _sessionProcessor, | ||
| sessionId, | ||
| MaxConcurrentAcceptSessionsSemaphore, | ||
| _maxConcurrentAcceptSessionsSemaphore, | ||
| _scopeFactory, | ||
| KeepOpenOnReceiveTimeout)); | ||
| } | ||
|
|
@@ -634,7 +630,7 @@ private void ReconcileReceiverManagers(int maxConcurrentSessions) | |
| new SessionReceiverManager( | ||
| _sessionProcessor, | ||
| null, | ||
| MaxConcurrentAcceptSessionsSemaphore, | ||
| _maxConcurrentAcceptSessionsSemaphore, | ||
| _scopeFactory, | ||
| KeepOpenOnReceiveTimeout)); | ||
| } | ||
|
|
@@ -960,22 +956,45 @@ public void UpdateConcurrency(int maxConcurrentCalls) | |
| } | ||
| } | ||
|
|
||
| internal void UpdateConcurrency(int maxConcurrentSessions, int maxConcurrentCallsPerSession) | ||
| internal void UpdateConcurrency(int maxConcurrentSessions, int maxConcurrentCallsPerSession, int? maxConcurrentCallsAcrossAllSessions = default) | ||
| { | ||
| Argument.AssertAtLeast(maxConcurrentSessions, 1, nameof(maxConcurrentSessions)); | ||
| Argument.AssertAtLeast(maxConcurrentCallsPerSession, 1, nameof(maxConcurrentCallsPerSession)); | ||
|
|
||
| if (maxConcurrentCallsAcrossAllSessions.HasValue) | ||
| { | ||
| Argument.AssertAtLeast(maxConcurrentCallsAcrossAllSessions.Value, 1, nameof(maxConcurrentCallsAcrossAllSessions)); | ||
| } | ||
|
|
||
| lock (_maxConcurrencySyncLock) | ||
| { | ||
| _maxConcurrentCalls = _sessionIds.Length > 0 | ||
| ? Math.Min(_sessionIds.Length, maxConcurrentSessions) | ||
| : maxConcurrentSessions * maxConcurrentCallsPerSession; | ||
| _maxConcurrentSessions = maxConcurrentSessions; | ||
| _maxConcurrentCallsPerSession = maxConcurrentCallsPerSession; | ||
|
|
||
| SetMaxConcurrentCallsAcrossSessions(maxConcurrentCallsAcrossAllSessions); | ||
| WakeLoop(); | ||
| } | ||
| } | ||
|
|
||
| private void SetMaxConcurrentCallsAcrossSessions(int? maxConcurrentCallsAcrossAllSessions) | ||
| { | ||
| int calculatedMaxConcurrentCalls = _sessionIds.Length > 0 | ||
| ? Math.Min(_sessionIds.Length, _maxConcurrentSessions) | ||
| : _maxConcurrentSessions * _maxConcurrentCallsPerSession; | ||
|
JoshLove-msft marked this conversation as resolved.
|
||
| if (maxConcurrentCallsAcrossAllSessions.HasValue) | ||
| { | ||
| if (maxConcurrentCallsAcrossAllSessions.Value > calculatedMaxConcurrentCalls) | ||
| { | ||
| throw new ArgumentOutOfRangeException(Resources.InvalidMaxConcurrentCalls); | ||
| } | ||
| _maxConcurrentCalls = maxConcurrentCallsAcrossAllSessions.Value; | ||
| } | ||
| else | ||
| { | ||
| _maxConcurrentCalls = calculatedMaxConcurrentCalls; | ||
| } | ||
| } | ||
|
|
||
| private void WakeLoop() | ||
| { | ||
| // wake up the handler loop | ||
|
|
@@ -1031,6 +1050,25 @@ private async Task ReconcileConcurrencyAsync() | |
| } | ||
| } | ||
|
|
||
| if (IsSessionProcessor) | ||
| { | ||
| int maxAcceptSessions = Math.Min(maxConcurrentCalls, 2 * Environment.ProcessorCount); | ||
| int diffAcceptSessions = maxAcceptSessions - _currentAcceptSessions; | ||
| if (diffAcceptSessions > 0) | ||
| { | ||
| _maxConcurrentAcceptSessionsSemaphore.Release(diffAcceptSessions); | ||
| } | ||
| else | ||
| { | ||
| int diffAcceptLimit = Math.Abs(diffAcceptSessions); | ||
| for (int i = 0; i < diffAcceptLimit; i++) | ||
| { | ||
| await _maxConcurrentAcceptSessionsSemaphore.WaitAsync().ConfigureAwait(false); | ||
| } | ||
| } | ||
| _currentAcceptSessions = maxAcceptSessions; | ||
| } | ||
|
|
||
| ReconcileReceiverManagers(maxConcurrentSessions); | ||
|
|
||
| _currentConcurrentCalls = maxConcurrentCalls; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.