-
Notifications
You must be signed in to change notification settings - Fork 47
Add ability to observe the number of connections halibut opens for polling connections #717
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
a0fcda4
d9b34ea
2aa8583
4d436f2
832f089
f3a43ba
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 |
|---|---|---|
| @@ -1,101 +1,122 @@ | ||
| using System; | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Runtime.CompilerServices; | ||
| using Halibut.Diagnostics; | ||
| using Halibut.Exceptions; | ||
| using Halibut.Transport.Observability; | ||
|
|
||
| namespace Halibut.Transport | ||
| { | ||
| public interface IActiveTcpConnectionsLimiter | ||
| { | ||
| IDisposable LeaseActiveTcpConnection(Uri subscriptionId); | ||
|
|
||
| IDisposable CreateUnlimitedLease(); | ||
| } | ||
|
|
||
| public class ActiveTcpConnectionsLimiter : IActiveTcpConnectionsLimiter | ||
| { | ||
| readonly HalibutTimeoutsAndLimits timeoutsAndLimits; | ||
| readonly IConnectionsObserver connectionsObserver; | ||
|
|
||
| Dictionary<Uri, StrongBox<int>> activeConnectionCountPerSubscriptionId = new(); | ||
|
|
||
| public ActiveTcpConnectionsLimiter(HalibutTimeoutsAndLimits timeoutsAndLimits) | ||
| public ActiveTcpConnectionsLimiter(HalibutTimeoutsAndLimits timeoutsAndLimits, IConnectionsObserver connectionsObserver) | ||
| { | ||
| this.timeoutsAndLimits = timeoutsAndLimits; | ||
| this.connectionsObserver = connectionsObserver; | ||
| } | ||
|
|
||
| public IDisposable LeaseActiveTcpConnection(Uri subscriptionId) | ||
| { | ||
| //if there is no limit, then we return a NoOp lease (which doesn't limit anything) | ||
| //if there is no limit, then we still count the connection (the observer is told about every | ||
| //connection either way), we just never reject it | ||
| if (!timeoutsAndLimits.MaximumActiveTcpConnectionsPerPollingSubscription.HasValue) | ||
| { | ||
| return CreateUnlimitedLease(); | ||
| return CreateUnlimitedLease(subscriptionId); | ||
| } | ||
|
|
||
| return new LimitingAuthorizedTcpConnectionLease(subscriptionId, activeConnectionCountPerSubscriptionId, timeoutsAndLimits.MaximumActiveTcpConnectionsPerPollingSubscription.Value); | ||
| } | ||
|
|
||
| public IDisposable CreateUnlimitedLease() | ||
| { | ||
| return new UnlimitedAuthorizedTcpConnectionLease(); | ||
| return new LimitingAuthorizedTcpConnectionLease(subscriptionId, activeConnectionCountPerSubscriptionId, timeoutsAndLimits.MaximumActiveTcpConnectionsPerPollingSubscription.Value, connectionsObserver); | ||
| } | ||
|
|
||
| class UnlimitedAuthorizedTcpConnectionLease : IDisposable | ||
| IDisposable CreateUnlimitedLease(Uri subscriptionId) | ||
| { | ||
| public void Dispose() | ||
| { | ||
| } | ||
| return new LimitingAuthorizedTcpConnectionLease(subscriptionId, activeConnectionCountPerSubscriptionId, int.MaxValue, connectionsObserver); | ||
| } | ||
|
|
||
| class LimitingAuthorizedTcpConnectionLease : IDisposable | ||
| { | ||
| readonly Uri subscriptionId; | ||
| readonly Dictionary<Uri, StrongBox<int>> activeConnectionCountPerSubscriptionId; | ||
| readonly IConnectionsObserver connectionsObserver; | ||
|
|
||
| public LimitingAuthorizedTcpConnectionLease(Uri subscriptionId, Dictionary<Uri, StrongBox<int>> activeConnectionCountPerSubscriptionId, int maximumAcceptedTcpConnectionsPerThumbprint) | ||
| public LimitingAuthorizedTcpConnectionLease(Uri subscriptionId, Dictionary<Uri, StrongBox<int>> activeConnectionCountPerSubscriptionId, int maximumAcceptedTcpConnectionsPerThumbprint, IConnectionsObserver connectionsObserver) | ||
| { | ||
| this.subscriptionId = subscriptionId; | ||
| this.activeConnectionCountPerSubscriptionId = activeConnectionCountPerSubscriptionId; | ||
| this.connectionsObserver = connectionsObserver; | ||
|
|
||
| var (previousCount, currentCount) = IncrementCount( maximumAcceptedTcpConnectionsPerThumbprint); | ||
|
|
||
| // Calling observer outside the lock might lead to surprising short term values but it will reduce the impact of a slow observer on the connection acceptance. | ||
| // E.g. (1, 2) might be processed before (0, 1) which will result in (-1, 1) -> (0, 1) values in the bucket. | ||
| // (-1, 1) should not really last for long as the connections are rather long-lived. | ||
| connectionsObserver.ConnectionsCountChangedFor(subscriptionId, previousCount, currentCount); | ||
| } | ||
|
|
||
| lock (this.activeConnectionCountPerSubscriptionId) | ||
| (int previousCount, int currentCount) IncrementCount(int maximumAcceptedTcpConnectionsPerThumbprint) | ||
| { | ||
| lock (activeConnectionCountPerSubscriptionId) | ||
| { | ||
| if (!this.activeConnectionCountPerSubscriptionId.TryGetValue(subscriptionId, out var count)) | ||
| if (!activeConnectionCountPerSubscriptionId.TryGetValue(subscriptionId, out var count)) | ||
| { | ||
| count = new StrongBox<int>(0); | ||
| this.activeConnectionCountPerSubscriptionId.Add(subscriptionId, count); | ||
| activeConnectionCountPerSubscriptionId.Add(subscriptionId, count); | ||
| } | ||
|
|
||
| count.Value++; | ||
| var previousCount = count.Value; | ||
|
|
||
| //validate the new count. If this throws an exception, it'll kill the connection | ||
| if (count.Value > maximumAcceptedTcpConnectionsPerThumbprint) | ||
| if (count.Value + 1 > maximumAcceptedTcpConnectionsPerThumbprint) | ||
| { | ||
| //decrement as this connection has been rejected | ||
| count.Value--; | ||
|
|
||
| //throw an exception, bailing on the connection | ||
| throw new ActiveTcpConnectionsExceededException(this.subscriptionId, $"Exceeded the maximum number ({maximumAcceptedTcpConnectionsPerThumbprint}) of active TCP connections for subscription {subscriptionId}"); | ||
| throw new ActiveTcpConnectionsExceededException(subscriptionId, $"Exceeded the maximum number ({maximumAcceptedTcpConnectionsPerThumbprint}) of active TCP connections for subscription {subscriptionId}"); | ||
| } | ||
|
|
||
| count.Value++; | ||
|
|
||
| return (previousCount, count.Value); | ||
| } | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| var counts = DecrementCount(); | ||
| if (counts == null) return; | ||
|
|
||
| var (previousCount, currentCount) = counts.Value; | ||
| connectionsObserver.ConnectionsCountChangedFor(subscriptionId, previousCount, currentCount); | ||
| } | ||
|
|
||
| (int previousCount, int currentCount)? DecrementCount() | ||
| { | ||
| lock (activeConnectionCountPerSubscriptionId) | ||
| { | ||
| if (activeConnectionCountPerSubscriptionId.TryGetValue(subscriptionId, out var count)) | ||
| { | ||
| //decrement the count of authorized connections | ||
| var previousCount = count.Value; | ||
| count.Value--; | ||
|
|
||
| // Remove the key from the dictionary if the value is 0 | ||
| if (count.Value == 0) | ||
| { | ||
| activeConnectionCountPerSubscriptionId.Remove(subscriptionId); | ||
| } | ||
|
|
||
| return (previousCount, count.Value); | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| using System; | ||
|
|
||
| namespace Halibut.Transport.Observability | ||
| { | ||
| public interface IConnectionsObserver | ||
|
|
@@ -6,7 +8,7 @@ public interface IConnectionsObserver | |
| /// The connection has been accepted and no bytes have been read from the wire. | ||
| /// | ||
| /// In this context server is anything that listens on a port. | ||
| /// | ||
| /// | ||
| /// This is called when any of the following occurs: | ||
| /// - When a "server" accepts a connection from a polling service (either websocket or regular) | ||
| /// - When a "server" accepts a connection from a listening client (so in this case the server is the service) | ||
|
|
@@ -16,8 +18,20 @@ public interface IConnectionsObserver | |
| /// <summary> | ||
| /// A previously accepted connection has been closed. | ||
| /// | ||
| /// For every call to ConnectionClosed() their can be at most one call to this method. | ||
| /// For every call to ConnectionClosed() their can be at most one call to this method. | ||
| /// </summary> | ||
| public void ConnectionClosed(bool authorized); | ||
|
|
||
| /// <summary> | ||
| /// A polling subscriber's connections' count has changed | ||
| /// </summary> | ||
| /// <param name="subscriptionId">The polling subscriber's subscription id.</param> | ||
| /// <param name="previousCount"> | ||
| /// The number of active TCP connections for this subscriptionId immediately before the change | ||
| /// </param> | ||
| /// <param name="currentCount"> | ||
| /// The number of active TCP connections for this subscriptionId immediately after the change | ||
| /// </param> | ||
| public void ConnectionsCountChangedFor(Uri subscriptionId, int previousCount, int currentCount); | ||
|
Contributor
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. I considrerd a pair of
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. Make it simpler for the caller is probably the best approach.
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. Looking at how this is used, what we have now is fine. |
||
| } | ||
| } | ||
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 looks like the information you want.
Maybe have a method that will give you back a copy of this dictionary upon request.
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.
That would create a tighter coupling between these two.
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 what you are doing with buckets is faster over processing the entire dict each time we want metrics so this is fine.
As for coupling we already have some form, I think the form that works the best for the telemetry we want to collect makes sense. I guess that comes with a "I would be comfortable changing the contract for telemetry callbacks if we had"