fix(twincat): replay failure marks the registration dead, publishes Bad quality, degrades health (05/STAB-16)
This commit is contained in:
@@ -192,7 +192,7 @@
|
||||
{
|
||||
"id": "B6.2",
|
||||
"subject": "B6: NativeRegistration HasLiveHandle/MarkHandleDead/OnUnavailable + replay-catch Bad-quality emit + health degrade \u2014 STAB-16",
|
||||
"status": "pending",
|
||||
"status": "completed",
|
||||
"blockedBy": [
|
||||
"B6.1"
|
||||
]
|
||||
|
||||
@@ -493,9 +493,14 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
var reg = new NativeRegistration(
|
||||
Interlocked.Increment(ref _nextRegId), device, reference, symbolName,
|
||||
def.DataType, bitIndex, publishingInterval,
|
||||
(_, value) => OnDataChange?.Invoke(this,
|
||||
onChange: (_, value) => OnDataChange?.Invoke(this,
|
||||
new DataChangeEventArgs(handle, reference, new DataValueSnapshot(
|
||||
value, TwinCATStatusMapper.Good, DateTime.UtcNow, DateTime.UtcNow))));
|
||||
value, TwinCATStatusMapper.Good, DateTime.UtcNow, DateTime.UtcNow))),
|
||||
// 05/STAB-16: when the handle goes dead (a failed replay) push a Bad snapshot so
|
||||
// subscribers see the tag go Bad instead of a frozen-Good-stale value.
|
||||
onUnavailable: () => OnDataChange?.Invoke(this,
|
||||
new DataChangeEventArgs(handle, reference, new DataValueSnapshot(
|
||||
null, TwinCATStatusMapper.BadCommunicationError, null, DateTime.UtcNow))));
|
||||
|
||||
await RegisterNotificationAsync(device, reg, cancellationToken).ConfigureAwait(false);
|
||||
registrations.Add(reg);
|
||||
@@ -593,8 +598,22 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
/// <summary>Value-change callback (closes over the owning subscription handle + reference).</summary>
|
||||
public Action<string, object?> OnChange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Bad-quality callback (05/STAB-16). Invoked when this registration's handle goes dead
|
||||
/// (a failed replay) so subscribers observe the tag go Bad instead of frozen-Good-stale —
|
||||
/// the <see cref="OnChange"/> shape has no quality channel. Closes over the same
|
||||
/// (handle, reference) pair as <see cref="OnChange"/>.
|
||||
/// </summary>
|
||||
public Action OnUnavailable { get; }
|
||||
|
||||
private ITwinCATNotificationHandle? _handle;
|
||||
|
||||
/// <summary>
|
||||
/// True while this registration holds a live notification handle. False after a failed
|
||||
/// replay marks it dead (05/STAB-16) — the probe-tick retry re-registers such handles.
|
||||
/// </summary>
|
||||
public bool HasLiveHandle => Volatile.Read(ref _handle) is not null;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="NativeRegistration"/> class.</summary>
|
||||
/// <param name="id">Globally-unique registration id (registry key on the device).</param>
|
||||
/// <param name="device">The device this notification is registered against.</param>
|
||||
@@ -604,8 +623,10 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
/// <param name="bitIndex">Bit index for a BOOL-within-word symbol; null otherwise.</param>
|
||||
/// <param name="interval">Requested notification interval.</param>
|
||||
/// <param name="onChange">Value-change callback (closes over the owning subscription handle + reference).</param>
|
||||
/// <param name="onUnavailable">Bad-quality callback fired when the handle goes dead (05/STAB-16).</param>
|
||||
public NativeRegistration(long id, DeviceState device, string reference, string symbolName,
|
||||
TwinCATDataType dataType, int? bitIndex, TimeSpan interval, Action<string, object?> onChange)
|
||||
TwinCATDataType dataType, int? bitIndex, TimeSpan interval, Action<string, object?> onChange,
|
||||
Action onUnavailable)
|
||||
{
|
||||
Id = id;
|
||||
Device = device;
|
||||
@@ -615,6 +636,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
BitIndex = bitIndex;
|
||||
Interval = interval;
|
||||
OnChange = onChange;
|
||||
OnUnavailable = onUnavailable;
|
||||
}
|
||||
|
||||
/// <summary>Installs a freshly-registered handle and returns the previous one (null on first set).</summary>
|
||||
@@ -623,6 +645,15 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
public ITwinCATNotificationHandle? SwapHandle(ITwinCATNotificationHandle newHandle) =>
|
||||
Interlocked.Exchange(ref _handle, newHandle);
|
||||
|
||||
/// <summary>
|
||||
/// Atomically clears the live handle and returns the previous one (05/STAB-16). The caller
|
||||
/// disposes the returned (already-dead) handle best-effort. After this
|
||||
/// <see cref="HasLiveHandle"/> is false until a retry re-registers.
|
||||
/// </summary>
|
||||
/// <returns>The previous handle, or null.</returns>
|
||||
public ITwinCATNotificationHandle? MarkHandleDead() =>
|
||||
Interlocked.Exchange(ref _handle, null);
|
||||
|
||||
/// <summary>Removes this registration from its device and disposes its live handle.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
@@ -851,6 +882,16 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
"TwinCAT driver '{DriverInstanceId}' failed to replay native notification " +
|
||||
"'{Symbol}' on device '{HostAddress}' after reconnect",
|
||||
_driverInstanceId, reg.SymbolName, device.Options.HostAddress);
|
||||
|
||||
// 05/STAB-16: the intent has no live handle now — mark it dead so the probe-tick
|
||||
// retry re-registers it, surface Bad quality to subscribers (the OnChange shape has
|
||||
// no quality channel), and degrade health preserving the last successful read.
|
||||
var dead = reg.MarkHandleDead();
|
||||
try { dead?.Dispose(); } catch { /* already-dead handle from the disposed client */ }
|
||||
try { reg.OnUnavailable(); } catch { /* never let a subscriber callback abort recovery of the rest */ }
|
||||
if (_health.State != DriverState.Faulted)
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead,
|
||||
$"native-notification replay failed for '{reg.SymbolName}' on {device.Options.HostAddress}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user