fix(driver-modbus): resolve High code-review finding (Driver.Modbus-001)
_lastPublishedByRef was a plain Dictionary<string, object> mutated inside ShouldPublish, which runs on the PollGroupEngine onChange callback. The engine runs one background Task per subscription, so a driver with two or more subscriptions invokes ShouldPublish concurrently on separate threads. Concurrent TryGetValue/indexer writes on a non-thread-safe Dictionary can corrupt internal state, drop entries, or throw, crashing the poll loop. Switch _lastPublishedByRef to ConcurrentDictionary<string, object>; its TryGetValue and indexer-set operations are individually thread-safe, so the deadband cache is now correct under concurrent multi-subscription publishing, consistent with the lock-guarded sibling cache _lastWrittenByRef. Add an xUnit + Shouldly regression test that runs 24 deadband-configured single-tag subscriptions concurrently and asserts the poll loop survives without faulting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
@@ -89,7 +90,11 @@ public sealed class ModbusDriver
|
||||
// Last-published value per tag, keyed by FullReference. Used by ShouldPublish to apply
|
||||
// the deadband filter. Stored as object so all numeric types share one map; the comparison
|
||||
// does a typed cast inside.
|
||||
private readonly Dictionary<string, object> _lastPublishedByRef = new(StringComparer.OrdinalIgnoreCase);
|
||||
// Driver.Modbus-001: ShouldPublish runs on the PollGroupEngine onChange callback, which
|
||||
// executes on one background Task per subscription — so a multi-subscription driver mutates
|
||||
// this map concurrently from several threads. A plain Dictionary corrupts under concurrent
|
||||
// writes; ConcurrentDictionary makes every TryGetValue / indexer write thread-safe.
|
||||
private readonly ConcurrentDictionary<string, object> _lastPublishedByRef = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Last-written value per tag for the WriteOnChangeOnly suppression. Invalidated by reads
|
||||
// that return a different value (so an HMI-side change doesn't get masked).
|
||||
|
||||
Reference in New Issue
Block a user