feat: update RoutingFeedbackManager to use ConcurrentDictionary for update timers to ensure thread safety

This commit is contained in:
Neil Dorin 2026-07-14 21:47:52 -06:00
parent 079f2beb8b
commit f05a25c3e2

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Timers; using System.Timers;
@ -22,9 +23,13 @@ namespace PepperDash.Essentials.Core.Routing
private Dictionary<string, HashSet<RoutingInputPort>> midpointToSinkInputsMap; private Dictionary<string, HashSet<RoutingInputPort>> midpointToSinkInputsMap;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
private readonly Dictionary<string, Timer> updateTimers = new Dictionary<string, Timer>(); private readonly ConcurrentDictionary<string, Timer> updateTimers = new ConcurrentDictionary<string, Timer>();
/// <summary> /// <summary>
/// Debounce delay in milliseconds /// Debounce delay in milliseconds
@ -314,10 +319,9 @@ namespace PepperDash.Essentials.Core.Routing
} }
finally finally
{ {
if (updateTimers.ContainsKey(key)) if (updateTimers.TryRemove(key, out var timerToDispose))
{ {
updateTimers[key]?.Dispose(); timerToDispose?.Dispose();
updateTimers.Remove(key);
} }
} }
}; };