namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect;
///
/// Base type for every way an Agent /sample long poll can stop delivering chunks for a
/// reason that is not the caller cancelling it.
///
///
///
/// Why these are exceptions rather than a normal end of enumeration.
/// is contracted to yield indefinitely until
/// its token is cancelled, so a pump written to that contract has no reason to handle normal
/// completion — it would either drop the subscription silently or hot-loop reconnecting on
/// an enumerator that ends immediately. Returning normally would make "the data path
/// stopped" indistinguishable from "the data path is idle", which is the
/// quiet-successful-looking-termination shape that produced this repo's #485 defect. Every
/// non-cancellation end is therefore loud, typed, and carries the Agent URI.
///
///
/// Catch this base type to handle "the stream is over" uniformly; catch the two derived
/// types to distinguish a transient end (the Agent closed a connection — reconnect)
/// from a configuration error (the Agent will never stream to this request — retrying
/// is pointless until an operator changes something).
///
///
public abstract class MTConnectStreamException : Exception
{
/// Creates the exception with no message.
protected MTConnectStreamException()
{
}
/// Creates the exception with a message.
/// The operator-facing description.
protected MTConnectStreamException(string message)
: base(message)
{
}
/// Creates the exception with a message and an inner cause.
/// The operator-facing description.
/// The underlying cause.
protected MTConnectStreamException(string message, Exception innerException)
: base(message, innerException)
{
}
}
/// How an Agent /sample stream ended.
public enum MTConnectStreamEndReason
{
///
/// The transport closed mid-stream — the connection dropped, the Agent restarted, or a proxy
/// timed the connection out. Ordinary and transient: reconnect from the last
/// .
///
ConnectionClosed = 0,
///
/// The Agent wrote the closing --boundary-- delimiter, ending the multipart response
/// deliberately. Usually means the request was bounded (an Agent honouring a count
/// that exhausted, or an administrative stop) rather than a fault. Also reconnectable, but
/// worth distinguishing in logs: a stream that keeps closing cleanly points at the request's
/// parameters, not at the network.
///
ClosingBoundary = 1
}
///
/// The Agent's /sample stream ended without the caller cancelling it. See
/// for why this is not a normal end of enumeration.
///
public sealed class MTConnectStreamEndedException : MTConnectStreamException
{
/// Creates the exception for a stream that ended for .
/// How the stream ended.
/// The /sample request URI whose stream ended.
/// How many chunks were yielded before the end.
public MTConnectStreamEndedException(MTConnectStreamEndReason reason, string agentUri, long chunksDelivered)
: base($"MTConnect /sample stream from '{agentUri}' ended after {chunksDelivered} chunk(s) without the caller cancelling it ({Describe(reason)}). The stream is contracted to run until cancelled, so this is reported rather than returned; resume from the last chunk's nextSequence.")
{
Reason = reason;
AgentUri = agentUri;
ChunksDelivered = chunksDelivered;
}
/// Creates the exception with a message.
/// The operator-facing description.
public MTConnectStreamEndedException(string message)
: base(message)
{
AgentUri = string.Empty;
}
/// Creates the exception with a message and an inner cause.
/// The operator-facing description.
/// The underlying cause.
public MTConnectStreamEndedException(string message, Exception innerException)
: base(message, innerException)
{
AgentUri = string.Empty;
}
/// Creates the exception with no message.
public MTConnectStreamEndedException()
{
AgentUri = string.Empty;
}
/// How the stream ended.
public MTConnectStreamEndReason Reason { get; }
/// The /sample request URI whose stream ended.
public string AgentUri { get; }
/// How many chunks were yielded before the stream ended.
public long ChunksDelivered { get; }
private static string Describe(MTConnectStreamEndReason reason) =>
reason switch
{
MTConnectStreamEndReason.ClosingBoundary => "the Agent wrote the closing multipart boundary",
_ => "the connection closed mid-stream"
};
}
///
/// The Agent answered a /sample request with something that cannot be consumed as a
/// stream at all — a single non-multipart document, or a multipart/* response with no
/// boundary parameter to frame it by.
///
///
/// This is a configuration error, not a transient one, and is deliberately typed apart
/// from : reconnecting will produce the identical
/// response forever. The usual cause is an endpoint that is not an MTConnect Agent (a reverse
/// proxy, a load balancer's health page, or an Agent fronted by something that buffers and
/// collapses multipart/x-mixed-replace). Reported instead of yielding the one document
/// the Agent did return, because a single snapshot followed by a healthy-looking finished
/// stream is exactly how a dead data path disguises itself as a working one.
///
public sealed class MTConnectStreamNotSupportedException : MTConnectStreamException
{
/// Creates the exception with a message.
/// The operator-facing description.
public MTConnectStreamNotSupportedException(string message)
: base(message)
{
}
/// Creates the exception with a message and an inner cause.
/// The operator-facing description.
/// The underlying cause.
public MTConnectStreamNotSupportedException(string message, Exception innerException)
: base(message, innerException)
{
}
/// Creates the exception with no message.
public MTConnectStreamNotSupportedException()
{
}
}