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",
|
"id": "B6.2",
|
||||||
"subject": "B6: NativeRegistration HasLiveHandle/MarkHandleDead/OnUnavailable + replay-catch Bad-quality emit + health degrade \u2014 STAB-16",
|
"subject": "B6: NativeRegistration HasLiveHandle/MarkHandleDead/OnUnavailable + replay-catch Bad-quality emit + health degrade \u2014 STAB-16",
|
||||||
"status": "pending",
|
"status": "completed",
|
||||||
"blockedBy": [
|
"blockedBy": [
|
||||||
"B6.1"
|
"B6.1"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -493,9 +493,14 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
|||||||
var reg = new NativeRegistration(
|
var reg = new NativeRegistration(
|
||||||
Interlocked.Increment(ref _nextRegId), device, reference, symbolName,
|
Interlocked.Increment(ref _nextRegId), device, reference, symbolName,
|
||||||
def.DataType, bitIndex, publishingInterval,
|
def.DataType, bitIndex, publishingInterval,
|
||||||
(_, value) => OnDataChange?.Invoke(this,
|
onChange: (_, value) => OnDataChange?.Invoke(this,
|
||||||
new DataChangeEventArgs(handle, reference, new DataValueSnapshot(
|
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);
|
await RegisterNotificationAsync(device, reg, cancellationToken).ConfigureAwait(false);
|
||||||
registrations.Add(reg);
|
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>
|
/// <summary>Value-change callback (closes over the owning subscription handle + reference).</summary>
|
||||||
public Action<string, object?> OnChange { get; }
|
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;
|
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>
|
/// <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="id">Globally-unique registration id (registry key on the device).</param>
|
||||||
/// <param name="device">The device this notification is registered against.</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="bitIndex">Bit index for a BOOL-within-word symbol; null otherwise.</param>
|
||||||
/// <param name="interval">Requested notification interval.</param>
|
/// <param name="interval">Requested notification interval.</param>
|
||||||
/// <param name="onChange">Value-change callback (closes over the owning subscription handle + reference).</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,
|
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;
|
Id = id;
|
||||||
Device = device;
|
Device = device;
|
||||||
@@ -615,6 +636,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
|||||||
BitIndex = bitIndex;
|
BitIndex = bitIndex;
|
||||||
Interval = interval;
|
Interval = interval;
|
||||||
OnChange = onChange;
|
OnChange = onChange;
|
||||||
|
OnUnavailable = onUnavailable;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Installs a freshly-registered handle and returns the previous one (null on first set).</summary>
|
/// <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) =>
|
public ITwinCATNotificationHandle? SwapHandle(ITwinCATNotificationHandle newHandle) =>
|
||||||
Interlocked.Exchange(ref _handle, 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>
|
/// <summary>Removes this registration from its device and disposes its live handle.</summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
@@ -851,6 +882,16 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
|||||||
"TwinCAT driver '{DriverInstanceId}' failed to replay native notification " +
|
"TwinCAT driver '{DriverInstanceId}' failed to replay native notification " +
|
||||||
"'{Symbol}' on device '{HostAddress}' after reconnect",
|
"'{Symbol}' on device '{HostAddress}' after reconnect",
|
||||||
_driverInstanceId, reg.SymbolName, device.Options.HostAddress);
|
_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