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() { } }