fix(mtconnect): an Agent refusal of /current is a reply, not a comms fault
Health contract: an MTConnectError document (NO_DEVICE, INVALID_REQUEST, ...) is the Agent answering. The parsers now mark such an InvalidDataException (MTConnectXml.IsAgentRefusal - the type is sealed, so a Data marker, every other handler unchanged); ReadAsync reports the batch Bad but does not degrade the read path, advances LastSuccessfulRead, clears a prior read degradation and warns once per distinct refusal. An unparseable answer stays a comms fault. Refs #743 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -369,6 +369,9 @@ public sealed partial class MTConnectDriver
|
||||
/// <summary>Whether the <c>/current</c> read path is currently failing. See <see cref="PublishHealthy"/>.</summary>
|
||||
private volatile bool _readDegraded;
|
||||
|
||||
/// <summary>The last <c>/current</c> refusal text logged, so a persistent refusal warns once, not per read.</summary>
|
||||
private string? _lastReadRefusal;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the current-generation <c>/sample</c> pump has delivered since it started (a chunk, a heartbeat or
|
||||
/// a re-baseline) — the precondition, with <see cref="_streamDegraded"/> clear, for <see cref="ReadAsync"/>
|
||||
@@ -770,6 +773,23 @@ public sealed partial class MTConnectDriver
|
||||
// handled below as the Agent failure it is.
|
||||
throw;
|
||||
}
|
||||
catch (InvalidDataException ex) when (MTConnectXml.IsAgentRefusal(ex))
|
||||
{
|
||||
// The health contract: an MTConnectError document is the Agent REFUSING the request (a bad device
|
||||
// scope, an unsupported request) — it answered. The batch is Bad, but this is not a comms fault: the
|
||||
// read path is not degraded by it (that would reconnect-loop a driver over a config problem), the stamp
|
||||
// advances, and the Agent's text is logged once per distinct refusal.
|
||||
_readDegraded = false;
|
||||
PublishHealthy(DateTime.UtcNow);
|
||||
if (!string.Equals(Interlocked.Exchange(ref _lastReadRefusal, ex.Message), ex.Message, StringComparison.Ordinal))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"MTConnect driver {DriverInstanceId}: {AgentUri} refused /current — {AgentError}. Every read is reported Bad until the Agent accepts the request; the driver stays connected.",
|
||||
_driverInstanceId, AgentDisplay(options), ex.Message);
|
||||
}
|
||||
|
||||
return BadBatch(fullReferences.Count, BadCommunicationError);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DegradeAfterReadFailure(ex);
|
||||
|
||||
@@ -118,7 +118,7 @@ internal static class MTConnectProbeParser
|
||||
|
||||
if (!MTConnectXml.IsNamed(root, "MTConnectDevices"))
|
||||
{
|
||||
throw new InvalidDataException(MTConnectXml.DescribeUnexpectedRoot(root, "MTConnectDevices", Subject));
|
||||
throw MTConnectXml.UnexpectedRoot(root, "MTConnectDevices", Subject);
|
||||
}
|
||||
|
||||
var devicesContainer = MTConnectXml.ChildrenNamed(root, "Devices").FirstOrDefault()
|
||||
|
||||
@@ -159,3 +159,4 @@ public sealed class MTConnectStreamNotSupportedException : MTConnectStreamExcept
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -295,8 +295,7 @@ internal static class MTConnectStreamsParser
|
||||
// exactly this shape (Agents serve one under HTTP 200), and its <Error> elements — the
|
||||
// Agent's own account of what it objected to — are the most useful thing in the whole
|
||||
// response. Materialising the root is how the shared extraction can reach them.
|
||||
throw new InvalidDataException(
|
||||
MTConnectXml.DescribeUnexpectedRoot((XElement)XNode.ReadFrom(reader), "MTConnectStreams", Subject));
|
||||
throw MTConnectXml.UnexpectedRoot((XElement)XNode.ReadFrom(reader), "MTConnectStreams", Subject);
|
||||
}
|
||||
|
||||
var observations = new List<MTConnectObservation>();
|
||||
|
||||
@@ -190,7 +190,44 @@ internal static class MTConnectXml
|
||||
/// <param name="root">The document's actual root element.</param>
|
||||
/// <param name="expectedRootName">The root element name the caller required.</param>
|
||||
/// <param name="subject">How to describe the document in the error message.</param>
|
||||
/// <returns>The error message describing the unexpected root.</returns>
|
||||
/// <returns>
|
||||
/// The exception to throw. For an <c>MTConnectError</c> root (the Agent REFUSED the request) it is marked
|
||||
/// with <see cref="AgentRefusalDataKey"/> — see <see cref="IsAgentRefusal"/>.
|
||||
/// </returns>
|
||||
public static InvalidDataException UnexpectedRoot(XElement root, string expectedRootName, string subject)
|
||||
{
|
||||
var ex = new InvalidDataException(DescribeUnexpectedRoot(root, expectedRootName, subject));
|
||||
|
||||
// Marked (InvalidDataException is sealed, so not a subtype) so the read path can tell a refusal — the
|
||||
// Agent answered — from an answer it could not parse at all. Every other handler sees the same exception.
|
||||
if (IsNamed(root, "MTConnectError"))
|
||||
{
|
||||
ex.Data[AgentRefusalDataKey] = true;
|
||||
}
|
||||
|
||||
return ex;
|
||||
}
|
||||
|
||||
/// <summary><see cref="Exception.Data"/> key marking an <c>MTConnectError</c> answer (see <see cref="IsAgentRefusal"/>).</summary>
|
||||
internal const string AgentRefusalDataKey = "MTConnect.AgentRefusal";
|
||||
|
||||
/// <summary>
|
||||
/// Whether <paramref name="ex"/> is the Agent answering with an <c>MTConnectError</c> document (e.g.
|
||||
/// <c>NO_DEVICE</c>, <c>INVALID_REQUEST</c>) — a refusal, as opposed to an answer that could not be parsed.
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception a parse raised.</param>
|
||||
/// <returns><see langword="true"/> for a refusal.</returns>
|
||||
public static bool IsAgentRefusal(Exception ex) =>
|
||||
ex is InvalidDataException && ex.Data[AgentRefusalDataKey] is true;
|
||||
|
||||
/// <summary>
|
||||
/// The text of <see cref="UnexpectedRoot"/>: the unexpected root, plus the Agent's own error text for an
|
||||
/// <c>MTConnectError</c> document.
|
||||
/// </summary>
|
||||
/// <param name="root">The document's root element.</param>
|
||||
/// <param name="expectedRootName">The root the caller expected.</param>
|
||||
/// <param name="subject">How to describe the document.</param>
|
||||
/// <returns>The message.</returns>
|
||||
public static string DescribeUnexpectedRoot(XElement root, string expectedRootName, string subject)
|
||||
{
|
||||
var message =
|
||||
|
||||
Reference in New Issue
Block a user