fix(dcl): capture adapter into locals before background tasks — no off-thread reads of the mutable _adapter field (S7)
This commit is contained in:
@@ -678,6 +678,13 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
// Capture the current adapter generation so callbacks
|
||||
// from this adapter can be distinguished from a later (post-failover) adapter.
|
||||
var generation = _adapterGeneration;
|
||||
// Capture the adapter reference on the actor thread too (S7). The background task
|
||||
// below iterates multiple tags with an await between each SubscribeAsync — a failover
|
||||
// that swaps the mutable _adapter field mid-loop would otherwise make later iterations
|
||||
// (and the SeedTagsAsync read) run against the NEW adapter while carrying the OLD
|
||||
// generation. The in-flight subscribe must finish against the adapter that started it;
|
||||
// the generation guard drops any value the swapped-out adapter later produces.
|
||||
var adapter = _adapter;
|
||||
|
||||
// Partition tags on the actor thread into "this
|
||||
// request will issue _adapter.SubscribeAsync" vs. "already subscribed (by us
|
||||
@@ -718,7 +725,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
{
|
||||
try
|
||||
{
|
||||
var subId = await _adapter.SubscribeAsync(tagPath, (path, value) =>
|
||||
var subId = await adapter.SubscribeAsync(tagPath, (path, value) =>
|
||||
{
|
||||
self.Tell(new TagValueReceived(path, value, generation));
|
||||
});
|
||||
@@ -752,7 +759,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
// SeedTagsAsync retries the still-empty reads so a
|
||||
// seed that races the just-created advise (returns VT_EMPTY) is not silently
|
||||
// dropped, and logs any tag that never yields a value.
|
||||
var seedValues = await SeedTagsAsync(_adapter, tagsToSeed);
|
||||
var seedValues = await SeedTagsAsync(adapter, tagsToSeed);
|
||||
|
||||
return new SubscribeCompleted(request, sender, results, seedValues);
|
||||
}).PipeTo(self);
|
||||
@@ -1119,8 +1126,12 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
// failed WriteTagResponse so the failure is returned synchronously.
|
||||
var cts = new CancellationTokenSource(_options.WriteTimeout);
|
||||
|
||||
// Capture the adapter on the actor thread (S7) so the write completes against the
|
||||
// adapter that started it even if a failover swaps the _adapter field mid-flight.
|
||||
var adapter = _adapter;
|
||||
|
||||
// Write through DCL to device, failure returned synchronously
|
||||
_adapter.WriteAsync(request.TagPath, request.Value, cts.Token).ContinueWith(t =>
|
||||
adapter.WriteAsync(request.TagPath, request.Value, cts.Token).ContinueWith(t =>
|
||||
{
|
||||
cts.Dispose();
|
||||
if (t.IsCompletedSuccessfully)
|
||||
@@ -1158,11 +1169,16 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
var sender = Sender;
|
||||
var cts = new CancellationTokenSource(_options.WriteTimeout);
|
||||
|
||||
// Capture the adapter on the actor thread (S7). The trigger write below runs AFTER
|
||||
// the batch await, so re-reading the _adapter field there could hit a post-failover
|
||||
// adapter; both legs must use the adapter that started this batch.
|
||||
var adapter = _adapter;
|
||||
|
||||
async Task<WriteTagBatchResponse> RunAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var results = await _adapter.WriteBatchAsync(
|
||||
var results = await adapter.WriteBatchAsync(
|
||||
new Dictionary<string, object?>(request.Values), cts.Token);
|
||||
var failed = results.Values.Where(r => !r.Success).Select(r => r.ErrorMessage).ToList();
|
||||
if (failed.Count > 0)
|
||||
@@ -1172,7 +1188,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
|
||||
if (!string.IsNullOrEmpty(request.TriggerTagPath))
|
||||
{
|
||||
var tr = await _adapter.WriteAsync(request.TriggerTagPath, request.TriggerValue, cts.Token);
|
||||
var tr = await adapter.WriteAsync(request.TriggerTagPath, request.TriggerValue, cts.Token);
|
||||
if (!tr.Success)
|
||||
return new WriteTagBatchResponse(
|
||||
request.CorrelationId, false,
|
||||
@@ -1591,9 +1607,13 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
_healthCollector.UpdateTagQuality(_connectionName, _tagsGoodQuality, _tagsBadQuality, _tagsUncertainQuality);
|
||||
|
||||
var generation = _adapterGeneration;
|
||||
// Capture the adapter up front (S7), symmetric with reseedAdapter below. This loop
|
||||
// runs on the actor thread so a mid-loop swap can't happen, but reading the local
|
||||
// keeps every adapter access in this method tied to one generation.
|
||||
var subscribeAdapter = _adapter;
|
||||
foreach (var tagPath in allTags)
|
||||
{
|
||||
_adapter.SubscribeAsync(tagPath, (path, value) =>
|
||||
subscribeAdapter.SubscribeAsync(tagPath, (path, value) =>
|
||||
{
|
||||
self.Tell(new TagValueReceived(path, value, generation));
|
||||
}).ContinueWith(t =>
|
||||
|
||||
Reference in New Issue
Block a user