fix(data-connection): resolve DataConnectionLayer-001 — off-thread actor state mutation
HandleSubscribe spawned a Task.Run that mutated DataConnectionActor private state (_subscriptionIds, _subscriptionsByInstance, _totalSubscribed, _resolvedTags, _unresolvedTags) from a thread-pool thread, racing the actor's own message loop — a data race on non-thread-safe Dictionary/HashSet and non-atomic counters. Restructured HandleSubscribe to follow the actor's existing PipeTo(Self) pattern: the background task now performs only adapter I/O and pipes a SubscribeCompleted message to Self; all subscription-state mutation happens in the new HandleSubscribeCompleted handler on the actor thread (wired into the Connected, Connecting and Reconnecting states). Adds DCL001_ConcurrentSubscribes_DoNotCorruptSubscriptionCounters (30x30 concurrent subscribes) which fails against the pre-fix code and passes after.
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
| Last reviewed | 2026-05-16 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `9c60592` |
|
||||
| Open findings | 13 |
|
||||
| Open findings | 12 |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -53,7 +53,7 @@ tag-resolution retry, disconnect/re-subscribe, and concurrency around `HandleSub
|
||||
|--|--|
|
||||
| Severity | Critical |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:473-538` |
|
||||
|
||||
**Description**
|
||||
@@ -82,7 +82,18 @@ handler too.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
Resolved 2026-05-16. `HandleSubscribe` was restructured to follow the actor's own
|
||||
`PipeTo(Self)` pattern (the one already used by `HandleRetryTagResolution`): the
|
||||
background `Task.Run` now performs only adapter I/O (`SubscribeAsync`/`ReadAsync`),
|
||||
collects per-tag outcomes into an immutable `SubscribeCompleted` message, and pipes
|
||||
that to `Self`. All mutation of `_subscriptionIds`, `_subscriptionsByInstance`,
|
||||
`_totalSubscribed`, `_resolvedTags` and `_unresolvedTags` now happens in the new
|
||||
`HandleSubscribeCompleted` handler on the actor thread; it is wired into the
|
||||
Connected, Connecting and Reconnecting states so an in-flight subscribe is applied
|
||||
regardless of state transitions. Regression test
|
||||
`DCL001_ConcurrentSubscribes_DoNotCorruptSubscriptionCounters` (30×30 concurrent
|
||||
subscribes) fails against the pre-fix code and passes after. Fixed by the commit
|
||||
whose message references `DataConnectionLayer-001`.
|
||||
|
||||
### DataConnectionLayer-002 — `Restart` supervision discards all subscription state on connection-actor crash
|
||||
|
||||
|
||||
Reference in New Issue
Block a user