From f05a25c3e24d27628daed857c98f698dccd85eb4 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 14 Jul 2026 21:47:52 -0600 Subject: [PATCH] feat: update RoutingFeedbackManager to use ConcurrentDictionary for update timers to ensure thread safety --- .../Routing/RoutingFeedbackManager.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index 4819de30..0ef974b9 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Timers; @@ -22,9 +23,13 @@ namespace PepperDash.Essentials.Core.Routing private Dictionary> midpointToSinkInputsMap; /// - /// Debounce timers for each sink device to prevent rapid successive updates + /// Debounce timers for each sink device to prevent rapid successive updates. Must be a + /// ConcurrentDictionary - it's mutated both from whatever thread calls UpdateDestination and + /// from each Timer's Elapsed callback (which fires on a threadpool timer thread), so a plain + /// Dictionary here would have its internal state corrupted by the concurrent Insert/Remove + /// calls, surfacing as an IndexOutOfRangeException from deep inside Dictionary internals. /// - private readonly Dictionary updateTimers = new Dictionary(); + private readonly ConcurrentDictionary updateTimers = new ConcurrentDictionary(); /// /// Debounce delay in milliseconds @@ -314,10 +319,9 @@ namespace PepperDash.Essentials.Core.Routing } finally { - if (updateTimers.ContainsKey(key)) + if (updateTimers.TryRemove(key, out var timerToDispose)) { - updateTimers[key]?.Dispose(); - updateTimers.Remove(key); + timerToDispose?.Dispose(); } } };