Merge pull request #1353 from PepperDash/mc-subscription-concurrency

fix: make subscriber functionality thread-safe
This commit is contained in:
Nick Genovese
2025-11-03 17:26:31 -04:00
committed by GitHub

View File

@@ -31,7 +31,12 @@ namespace PepperDash.Essentials.AppServer.Messengers
/// <remarks> /// <remarks>
/// Unsoliciited feedback from a device in a messenger will ONLY be sent to devices in this subscription list. When a client disconnects, it's ID will be removed from the collection. /// Unsoliciited feedback from a device in a messenger will ONLY be sent to devices in this subscription list. When a client disconnects, it's ID will be removed from the collection.
/// </remarks> /// </remarks>
protected HashSet<string> SubscriberIds = new HashSet<string>(); private readonly HashSet<string> subscriberIds = new HashSet<string>();
/// <summary>
/// Lock object for thread-safe access to SubscriberIds
/// </summary>
private readonly object _subscriberLock = new object();
private readonly List<string> _deviceInterfaces; private readonly List<string> _deviceInterfaces;
@@ -193,14 +198,15 @@ namespace PepperDash.Essentials.AppServer.Messengers
return; return;
} }
if (SubscriberIds.Any(id => id == clientId)) lock (_subscriberLock)
{ {
this.LogVerbose("Client {clientId} already subscribed", clientId); if (!subscriberIds.Add(clientId))
return; {
this.LogVerbose("Client {clientId} already subscribed", clientId);
return;
}
} }
SubscriberIds.Add(clientId);
this.LogDebug("Client {clientId} subscribed", clientId); this.LogDebug("Client {clientId} subscribed", clientId);
} }
@@ -216,14 +222,22 @@ namespace PepperDash.Essentials.AppServer.Messengers
return; return;
} }
if (!SubscriberIds.Any(i => i == clientId)) bool wasSubscribed;
lock (_subscriberLock)
{
wasSubscribed = subscriberIds.Contains(clientId);
if (wasSubscribed)
{
subscriberIds.Remove(clientId);
}
}
if (!wasSubscribed)
{ {
this.LogVerbose("Client with ID {clientId} is not subscribed", clientId); this.LogVerbose("Client with ID {clientId} is not subscribed", clientId);
return; return;
} }
SubscriberIds.RemoveWhere((i) => i == clientId);
this.LogInformation("Client with ID {clientId} unsubscribed", clientId); this.LogInformation("Client with ID {clientId} unsubscribed", clientId);
} }
@@ -312,7 +326,14 @@ namespace PepperDash.Essentials.AppServer.Messengers
// If client is null or empty, this message is unsolicited feedback. Iterate through the subscriber list and send to all interested parties // If client is null or empty, this message is unsolicited feedback. Iterate through the subscriber list and send to all interested parties
if (string.IsNullOrEmpty(clientId)) if (string.IsNullOrEmpty(clientId))
{ {
foreach (var client in SubscriberIds) // Create a snapshot of subscribers to avoid collection modification during iteration
List<string> subscriberSnapshot;
lock (_subscriberLock)
{
subscriberSnapshot = new List<string>(subscriberIds);
}
foreach (var client in subscriberSnapshot)
{ {
AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = client, Content = content }); AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = client, Content = content });
} }