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));
}