fix(ablegacy): never operate on a handle evicted while waiting for its gate
Review r0 M3. The indirect sweep disposed stale resolutions without holding their gates, and any reader/writer that fetched a handle before waiting on its gate could then use an instance a sibling had evicted and disposed - a spurious comms fault and Degraded. The sweep now evicts under each key's gate, and read/write re-resolve the handle once the gate is held (reconnecting single-flight if it was evicted; a retired state never reconnects). Refs #738 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -680,6 +680,7 @@ public sealed partial class AbLegacyDriver : IDriver, IReadable, IWritable, ITag
|
||||
await opLock.WaitAsync(ct).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
runtime = await CurrentRuntimeUnderLockAsync(device, def, runtime, ct).ConfigureAwait(false);
|
||||
await runtime.ReadAsync(ct).ConfigureAwait(false);
|
||||
status = runtime.GetStatus();
|
||||
var parsed = AbLegacyAddress.TryParse(def.Address, device.Options.PlcFamily);
|
||||
@@ -942,6 +943,7 @@ public sealed partial class AbLegacyDriver : IDriver, IReadable, IWritable, ITag
|
||||
await opLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
runtime = await CurrentRuntimeUnderLockAsync(device, def, runtime, cancellationToken).ConfigureAwait(false);
|
||||
if (arrayValue is not null) runtime.EncodeArray(arrayFileType, arrayValue);
|
||||
else runtime.EncodeValue(def.DataType, parsed.BitIndex, w.Value);
|
||||
await runtime.WriteAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -1356,7 +1358,10 @@ public sealed partial class AbLegacyDriver : IDriver, IReadable, IWritable, ITag
|
||||
{
|
||||
if (!key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(key, resolvedName, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
EvictRuntime(device, key);
|
||||
// Under the key's own gate (review 07 r0 M3), so no reader is mid-operation on the handle we dispose.
|
||||
var gate = device.GetRuntimeLock(key);
|
||||
await gate.WaitAsync(ct).ConfigureAwait(false);
|
||||
try { EvictRuntime(device, key); } finally { gate.Release(); }
|
||||
device.ForgetGates(key);
|
||||
}
|
||||
}
|
||||
@@ -1601,6 +1606,23 @@ public sealed partial class AbLegacyDriver : IDriver, IReadable, IWritable, ITag
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-resolves a tag's runtime once its operation gate is HELD (review 07 r0 M3): the handle fetched before
|
||||
/// the wait may since have been evicted and disposed (a failed sibling call, or an indirect index sweep), and
|
||||
/// using it would fail spuriously as a comms fault. Returns the live cached handle, or connects a fresh one.
|
||||
/// </summary>
|
||||
private async Task<IAbLegacyTagRuntime> CurrentRuntimeUnderLockAsync(
|
||||
DeviceState device, AbLegacyTagDefinition def, IAbLegacyTagRuntime fetched, CancellationToken ct)
|
||||
{
|
||||
if (device.Runtimes.TryGetValue(def.Name, out var live))
|
||||
return live;
|
||||
// Evicted between the fetch and the lock: a native runtime refused at init is cached too, so absence means
|
||||
// it really was released. Connect again (single-flight per device).
|
||||
// A retired (torn-down) state never reconnects: the in-flight call finishes on what it has.
|
||||
if (device.IsRetired) return fetched;
|
||||
return await EnsureTagRuntimeAsync(device, def, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evict the runtime for <paramref name="tagName"/> from the device's cache and dispose it so
|
||||
/// the next read/write call re-creates and re-initializes a fresh libplctag handle. Called
|
||||
|
||||
Reference in New Issue
Block a user