namespace ZB.MOM.WW.OtOpcUa.Runtime.Drivers; /// /// Every bound on an operator write's path, derived from ONE configured value: the write timeout /// (DriverWrites:Timeout). lmxopcua#777. /// /// /// /// A write passes three bounds on its way to the device, and they used to be configured independently: /// the gateway's Ask of (10 s), the host's Ask of the /// child (a fixed 8 s), and the child's own write timeout plus the /// queued-write budget (5 s + up to 2 s, and 8 s in the queue). An outer Ask that fires first reports the /// write failed and the node manager reverts the node, while the child is still driving the device and the /// device may still take the value — the client is told "failed" for a write that applied. Raising the /// write timeout above ~6 s silently produced exactly that. /// /// /// Here every bound is derived from , innermost first, so each outer bound covers /// the worst case of everything inside it: /// /// — the write's cancellation token fires. /// = timeout + — a write that ignored its /// token is abandoned (answers BadTimeout). /// = — the longest a write may wait for a /// slot (or for an earlier write to the same tag) before it is failed instead of started. /// = queue + run — the child answers every write within this of its /// arrival. /// = reply + , and /// = host Ask + margin. /// /// The Asks are safety nets for an actor that never replies, not a second write timeout. /// /// /// The Phase 6.1 resilience tier's per-capability Write timeout (DriverResilienceOptions) runs /// INSIDE : the actor's token is the outer cap, so a tier Write timeout above /// never takes effect. The Host warns about that at startup (tier defaults) and at /// each driver spawn (per-instance ResilienceConfig overrides). /// /// public sealed record DriverWriteBounds { /// The configuration section the write timeout is bound from. public const string SectionName = "DriverWrites"; /// /// Default write timeout. 10 s — the longest resilience-tier Write timeout (Tier C), so no tier default /// is capped below its own policy. Tier A/B writes are cancelled by their tighter tier timeout first. /// public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10); /// Largest accepted write timeout; a larger configured value is clamped (with a startup warning). public static readonly TimeSpan MaxTimeout = TimeSpan.FromMinutes(5); /// /// Headroom each Ask keeps over the bound it wraps, for mailbox and dispatch latency between the reply being /// sent and the Ask observing it. /// public static readonly TimeSpan ReplyMargin = TimeSpan.FromSeconds(1); /// The write bounds derived from . public static DriverWriteBounds Default { get; } = new(DefaultTimeout); /// Creates the bounds for a write timeout. /// The write timeout; must be positive. /// The timeout is zero or negative. public DriverWriteBounds(TimeSpan timeout) { ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(timeout, TimeSpan.Zero); Timeout = timeout; } /// When a write's cancellation token is cancelled (the write answers "write timeout"). public TimeSpan Timeout { get; } /// /// How long after a write that has not returned is abandoned as token-ignoring: half /// the timeout, at most 2 s. Long enough for a driver that honours its token to answer the cancellation /// itself. /// public TimeSpan AbandonGrace => AbandonGraceFor(Timeout); /// The latest a running write is answered after it starts: + . public TimeSpan RunBound => Timeout + AbandonGrace; /// /// The longest a write may wait for a slot before it is failed BadTimeout instead of being written: /// one , i.e. long enough for an earlier write to the same tag to finish. /// public TimeSpan QueueBudget => RunBound; /// The latest the driver child answers a write after it arrives: + . public TimeSpan ReplyBound => QueueBudget + RunBound; /// 's Ask of the child: + . public TimeSpan HostAsk => ReplyBound + ReplyMargin; /// 's Ask of the host: + . public TimeSpan GatewayAsk => HostAsk + ReplyMargin; /// See . /// The write timeout in force. /// The grace between cancelling the write's token and abandoning the write. public static TimeSpan AbandonGraceFor(TimeSpan timeout) => TimeSpan.FromTicks(Math.Min(timeout.Ticks / 2, TimeSpan.FromSeconds(2).Ticks)); }