fix(opcuaclient): writes feed health; a good keep-alive clears a stale Degraded
Health contract: a write response (per-node refusals included) is the server answering, so it restores Healthy and advances LastSuccessfulRead; a write with no reply (transport failure / timeout) writes Degraded with the stamp preserved; a caller-cancelled write touches nothing. A good keep-alive with no outage or dead subscription latched now clears a Degraded left by a failed read/write instead of only moving the stamp (a subscription-only driver otherwise stayed Degraded until its next re-initialise). All writes are generation-stamped. Refs #743 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -1437,6 +1437,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
_ = RequireSession();
|
||||
var results = new WriteResult[writes.Count];
|
||||
|
||||
var generation = Volatile.Read(ref _lifecycleGeneration);
|
||||
var session = Session;
|
||||
if (session is null)
|
||||
{
|
||||
@@ -1483,8 +1484,10 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// the severity bits set is a Bad.
|
||||
results[r] = new WriteResult(codes[w].Code);
|
||||
}
|
||||
// A response — refusals included (BadNotWritable, BadTypeMismatch, ...) — is the server answering.
|
||||
RecordDeviceAnswered(generation, Clock.GetUtcNow().UtcDateTime);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
// Timeout / cancellation after the wire request may have been dispatched.
|
||||
// Writes are non-idempotent — BadTimeout ("outcome unknown, do not blindly
|
||||
@@ -1494,12 +1497,15 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
const uint StatusBadTimeout = 0x800A0000u;
|
||||
for (var w = 0; w < indexMap.Count; w++)
|
||||
results[indexMap[w]] = new WriteResult(StatusBadTimeout);
|
||||
// No reply arrived. Only a timeout is a comms fault — a caller that cancelled says nothing about upstream.
|
||||
if (!cancellationToken.IsCancellationRequested) RecordCommsFault(generation, $"write: {ex.Message}");
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Pre-wire transport failure — the write definitely did not reach the server.
|
||||
for (var w = 0; w < indexMap.Count; w++)
|
||||
results[indexMap[w]] = new WriteResult(StatusBadCommunicationError);
|
||||
RecordCommsFault(generation, $"write: {ex.Message}");
|
||||
}
|
||||
return results;
|
||||
}
|
||||
@@ -3125,7 +3131,15 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateHealth(generation, h => h.State is DriverState.Healthy or DriverState.Degraded ? h with { LastSuccessfulRead = now } : null);
|
||||
// A good keep-alive is the server answering. With no outage and no dead subscription latched, a Degraded
|
||||
// here can only be a failed read/write the server has since contradicted, so it clears too — otherwise a
|
||||
// subscription-only driver (which never reads again) stayed Degraded until its next re-initialise.
|
||||
UpdateHealth(generation, h => h.State switch
|
||||
{
|
||||
DriverState.Healthy => h with { LastSuccessfulRead = now },
|
||||
DriverState.Degraded => new DriverHealth(DriverState.Healthy, now, null),
|
||||
_ => null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user