fix(abcip): serialize Read/GetStatus/Decode + Encode/Write/GetStatus per runtime (05/STAB-4)

This commit is contained in:
Joseph Doherty
2026-07-13 11:42:51 -04:00
parent e48609b000
commit 00dcb547e6
2 changed files with 88 additions and 51 deletions
@@ -42,7 +42,7 @@
{ {
"id": "B2.3", "id": "B2.3",
"subject": "B2: wrap ReadSingleAsync/ReadGroupAsync/WriteAsync critical sections in the per-runtime lock; AbCip suite green", "subject": "B2: wrap ReadSingleAsync/ReadGroupAsync/WriteAsync critical sections in the per-runtime lock; AbCip suite green",
"status": "pending", "status": "completed",
"blockedBy": [ "blockedBy": [
"B2.2" "B2.2"
] ]
@@ -542,6 +542,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
try try
{ {
var runtime = await EnsureTagRuntimeAsync(device, def, ct).ConfigureAwait(false); var runtime = await EnsureTagRuntimeAsync(device, def, ct).ConfigureAwait(false);
// Serialise Read → GetStatus → Decode against the shared runtime — the cached libplctag
// Tag handle is not concurrency-safe and GetStatus returns the LAST op's status (05/STAB-4).
var opLock = device.GetRuntimeLock(def.Name);
await opLock.WaitAsync(ct).ConfigureAwait(false);
try
{
await runtime.ReadAsync(ct).ConfigureAwait(false); await runtime.ReadAsync(ct).ConfigureAwait(false);
var status = runtime.GetStatus(); var status = runtime.GetStatus();
@@ -574,6 +581,11 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
results[fb.OriginalIndex] = new DataValueSnapshot(value, AbCipStatusMapper.Good, now, now); results[fb.OriginalIndex] = new DataValueSnapshot(value, AbCipStatusMapper.Good, now, now);
_health = new DriverHealth(DriverState.Healthy, now, null); _health = new DriverHealth(DriverState.Healthy, now, null);
} }
finally
{
opLock.Release();
}
}
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
throw; throw;
@@ -612,6 +624,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
try try
{ {
var runtime = await EnsureTagRuntimeAsync(device, parent, ct).ConfigureAwait(false); var runtime = await EnsureTagRuntimeAsync(device, parent, ct).ConfigureAwait(false);
// Serialise the whole-UDT Read → GetStatus → per-member DecodeValueAt against the shared
// parent runtime — the members decode from that one handle's buffer (05/STAB-4).
var opLock = device.GetRuntimeLock(parent.Name);
await opLock.WaitAsync(ct).ConfigureAwait(false);
try
{
await runtime.ReadAsync(ct).ConfigureAwait(false); await runtime.ReadAsync(ct).ConfigureAwait(false);
var status = runtime.GetStatus(); var status = runtime.GetStatus();
@@ -636,6 +655,11 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
} }
_health = new DriverHealth(DriverState.Healthy, now, null); _health = new DriverHealth(DriverState.Healthy, now, null);
} }
finally
{
opLock.Release();
}
}
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
throw; throw;
@@ -706,10 +730,23 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
} }
var runtime = await EnsureTagRuntimeAsync(device, def, cancellationToken).ConfigureAwait(false); var runtime = await EnsureTagRuntimeAsync(device, def, cancellationToken).ConfigureAwait(false);
// Serialise Encode → Write → GetStatus against the shared runtime — the same cached
// Tag handle may be in use by a concurrent ReadAsync or poll loop (05/STAB-4).
var opLock = device.GetRuntimeLock(def.Name);
await opLock.WaitAsync(cancellationToken).ConfigureAwait(false);
int status;
try
{
runtime.EncodeValue(def.DataType, parsedPath?.BitIndex, w.Value); runtime.EncodeValue(def.DataType, parsedPath?.BitIndex, w.Value);
await runtime.WriteAsync(cancellationToken).ConfigureAwait(false); await runtime.WriteAsync(cancellationToken).ConfigureAwait(false);
status = runtime.GetStatus();
}
finally
{
opLock.Release();
}
var status = runtime.GetStatus();
if (status != 0) if (status != 0)
{ {
EvictRuntime(device, def.Name); EvictRuntime(device, def.Name);