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:
@@ -7,7 +7,7 @@
|
||||
| Review date | 2026-05-22 |
|
||||
| Commit reviewed | `76d35d1` |
|
||||
| Status | Reviewed |
|
||||
| Open findings | 12 |
|
||||
| Open findings | 11 |
|
||||
|
||||
## Checklist coverage
|
||||
|
||||
@@ -33,13 +33,13 @@
|
||||
| Severity | High |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Location | `ModbusDriver.cs:92,99-122` |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** `_lastPublishedByRef` is a plain `Dictionary<string, object>` mutated inside `ShouldPublish`, which runs on the `PollGroupEngine.onChange` callback. `PollGroupEngine` runs one background `Task` per subscription (`PollGroupEngine.cs:64`), so a driver with two or more subscriptions invokes `onChange` — and therefore `ShouldPublish` — concurrently on separate threads. `ShouldPublish` does `TryGetValue` and indexer writes on the unsynchronized dictionary (`ModbusDriver.cs:108`, `112`, `120`). Concurrent reads/writes of a non-thread-safe `Dictionary` can corrupt internal state, drop entries, or throw `IndexOutOfRangeException`/`InvalidOperationException`, crashing the poll loop. The sibling cache `_lastWrittenByRef` is correctly guarded by `_lastWrittenLock` — only the deadband cache was left unprotected.
|
||||
|
||||
**Recommendation:** Guard `_lastPublishedByRef` with a dedicated lock around every access in `ShouldPublish`, or switch it to `ConcurrentDictionary<string, object>` and use `AddOrUpdate`/`TryGetValue`.
|
||||
|
||||
**Resolution:** _(open)_
|
||||
**Resolution:** Resolved 2026-05-22 — switched `_lastPublishedByRef` to `ConcurrentDictionary<string, object>` so the `TryGetValue`/indexer-write accesses in `ShouldPublish` are thread-safe under concurrent multi-subscription `onChange` callbacks; added a concurrent-deadband-subscription regression test.
|
||||
|
||||
### Driver.Modbus-002
|
||||
|
||||
|
||||
Reference in New Issue
Block a user