Skip to content

Commit 458aef3

Browse files
github-actions[bot]alex-jitbitBrennanConroy
authored
[release/8.0] SignalR performance: track groups per connection, remove on disconnect (#53862)
* SignalR: track groups per connection, remove on disconnect, fixes #48249 Instead of iterating over ALL the groups, which is slow and even introduces a DDoS vector, we remove from groups that are specific to this connection * Removed unused method * Apply suggestions from code review Co-authored-by: Brennan <[email protected]> * More code review suggestions with a minor fix * Use lock instead of ToArray (the latter iterates anyway) * Made the HashSet an internal property on HubConnectionContext * Removed unneeded "using" * Removed ignore-case from group tracking * Addressed race condition from code review --------- Co-authored-by: alex-jitbit <[email protected]> Co-authored-by: Brennan <[email protected]>
1 parent aad6550 commit 458aef3

File tree

3 files changed

+35
-13
lines changed

3 files changed

+35
-13
lines changed

src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,18 @@ public override Task AddToGroupAsync(string connectionId, string groupName, Canc
4242
return Task.CompletedTask;
4343
}
4444

45-
_groups.Add(connection, groupName);
45+
// Track groups in the connection object
46+
lock (connection.GroupNames)
47+
{
48+
if (!connection.GroupNames.Add(groupName))
49+
{
50+
// Connection already in group
51+
return Task.CompletedTask;
52+
}
53+
54+
_groups.Add(connection, groupName);
55+
}
56+
4657
// Connection disconnected while adding to group, remove it in case the Add was called after OnDisconnectedAsync removed items from the group
4758
if (connection.ConnectionAborted.IsCancellationRequested)
4859
{
@@ -64,7 +75,17 @@ public override Task RemoveFromGroupAsync(string connectionId, string groupName,
6475
return Task.CompletedTask;
6576
}
6677

67-
_groups.Remove(connectionId, groupName);
78+
// Remove from previously saved groups
79+
lock (connection.GroupNames)
80+
{
81+
if (!connection.GroupNames.Remove(groupName))
82+
{
83+
// Connection not in group
84+
return Task.CompletedTask;
85+
}
86+
87+
_groups.Remove(connectionId, groupName);
88+
}
6889

6990
return Task.CompletedTask;
7091
}
@@ -277,8 +298,16 @@ public override Task OnConnectedAsync(HubConnectionContext connection)
277298
/// <inheritdoc />
278299
public override Task OnDisconnectedAsync(HubConnectionContext connection)
279300
{
301+
lock (connection.GroupNames)
302+
{
303+
// Remove from tracked groups one by one
304+
foreach (var groupName in connection.GroupNames)
305+
{
306+
_groups.Remove(connection.ConnectionId, groupName);
307+
}
308+
}
309+
280310
_connections.Remove(connection);
281-
_groups.RemoveDisconnectedConnection(connection.ConnectionId);
282311

283312
return Task.CompletedTask;
284313
}

src/SignalR/server/Core/src/HubConnectionContext.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ public partial class HubConnectionContext
5656
[MemberNotNullWhen(true, nameof(_messageBuffer))]
5757
internal bool UsingStatefulReconnect() => _useStatefulReconnect;
5858

59+
// Tracks groups that the connection has been added to
60+
internal HashSet<string> GroupNames { get; } = new HashSet<string>();
61+
5962
/// <summary>
6063
/// Initializes a new instance of the <see cref="HubConnectionContext"/> class.
6164
/// </summary>

src/SignalR/server/Core/src/Internal/HubGroupList.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Collections;
55
using System.Collections.Concurrent;
6-
using System.Linq;
76

87
namespace Microsoft.AspNetCore.SignalR.Internal;
98

@@ -43,15 +42,6 @@ public void Remove(string connectionId, string groupName)
4342
}
4443
}
4544

46-
public void RemoveDisconnectedConnection(string connectionId)
47-
{
48-
var groupNames = _groups.Where(x => x.Value.ContainsKey(connectionId)).Select(x => x.Key);
49-
foreach (var groupName in groupNames)
50-
{
51-
Remove(connectionId, groupName);
52-
}
53-
}
54-
5545
public int Count => _groups.Count;
5646

5747
public IEnumerator<ConcurrentDictionary<string, HubConnectionContext>> GetEnumerator()

0 commit comments

Comments
 (0)