using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.Resilience; /// /// Per-tier × per-capability resilience policy configuration for a driver instance. /// Bound from DriverInstance.ResilienceConfig JSON (nullable column; null = tier defaults). /// Per docs/v2/plan.md. /// public sealed record DriverResilienceOptions { /// Tier the owning driver type is registered as; drives the default map. public required DriverTier Tier { get; init; } /// /// Per-capability policy overrides. Capabilities absent from this map fall back to /// for the configured . /// public IReadOnlyDictionary CapabilityPolicies { get; init; } = new Dictionary(); // RecycleIntervalSeconds is GONE (Gitea #522). It configured a scheduled recycle of a Tier C // driver's out-of-process Host — a process that no longer exists anywhere: Galaxy reaches MXAccess // over gRPC to the external mxaccessgw sidecar (PR 7.2 retired the in-process Host/Proxy/Shared // projects) and FOCAS has run in-process since its managed wire client landed 2026-04-24. The // scheduler it fed was constructed only in tests, and IDriverSupervisor — the thing that would // have performed the recycle — had zero implementations. The parser nevertheless validated the // knob, so an operator could author a recycle interval through the AdminUI and get nothing. // // The JSON key is not an error if present: DriverResilienceOptionsParser ignores unknown keys, so // a ResilienceConfig blob still carrying "recycleIntervalSeconds" parses cleanly and the value is // simply dropped, which is what it already did in effect. /// /// Look up the effective policy for a capability, falling back to tier defaults when no /// override is configured. Never returns null. /// /// The driver capability to resolve the policy for. /// The effective CapabilityPolicy for the specified capability. /// /// Thrown when neither the override map nor the tier defaults carry an entry for the /// requested capability. The TierDefaults_Cover_EveryCapability invariant test /// in DriverResilienceOptionsTests guarantees every defined enum value is present /// in each tier's table, so this only fires when a caller passes an out-of-range value /// or someone adds a member without updating /// . The message names the missing capability and tier. /// public CapabilityPolicy Resolve(DriverCapability capability) { if (CapabilityPolicies.TryGetValue(capability, out var policy)) return policy; var defaults = GetTierDefaults(Tier); if (defaults.TryGetValue(capability, out var fallback)) return fallback; throw new KeyNotFoundException( $"No policy defined for capability '{capability}' under tier '{Tier}'. " + $"This indicates a {nameof(DriverCapability)} enum value missing from {nameof(GetTierDefaults)} — " + "add the capability to every tier's default table."); } /// /// Per-tier per-capability default policy table, per the Phase 6.1 /// Stream A.2 specification. The parser enforces no-retry on /// and /// as an invariant (R2-02/S-8) — a JSON retryCount override on those capabilities is forced back /// to 0; per-tag opt-in via is the future relaxation. /// /// The driver tier to get defaults for. /// The default policy dictionary for the specified tier. public static IReadOnlyDictionary GetTierDefaults(DriverTier tier) => tier switch { DriverTier.A => new Dictionary { [DriverCapability.Read] = new(TimeoutSeconds: 2, RetryCount: 3, BreakerFailureThreshold: 5), [DriverCapability.Write] = new(TimeoutSeconds: 2, RetryCount: 0, BreakerFailureThreshold: 5), [DriverCapability.Discover] = new(TimeoutSeconds: 30, RetryCount: 2, BreakerFailureThreshold: 3), [DriverCapability.Subscribe] = new(TimeoutSeconds: 5, RetryCount: 3, BreakerFailureThreshold: 5), [DriverCapability.Probe] = new(TimeoutSeconds: 2, RetryCount: 3, BreakerFailureThreshold: 5), [DriverCapability.AlarmSubscribe] = new(TimeoutSeconds: 5, RetryCount: 3, BreakerFailureThreshold: 5), [DriverCapability.AlarmAcknowledge] = new(TimeoutSeconds: 5, RetryCount: 0, BreakerFailureThreshold: 5), [DriverCapability.HistoryRead] = new(TimeoutSeconds: 30, RetryCount: 2, BreakerFailureThreshold: 5), }, DriverTier.B => new Dictionary { [DriverCapability.Read] = new(TimeoutSeconds: 4, RetryCount: 3, BreakerFailureThreshold: 5), [DriverCapability.Write] = new(TimeoutSeconds: 4, RetryCount: 0, BreakerFailureThreshold: 5), [DriverCapability.Discover] = new(TimeoutSeconds: 60, RetryCount: 2, BreakerFailureThreshold: 3), [DriverCapability.Subscribe] = new(TimeoutSeconds: 8, RetryCount: 3, BreakerFailureThreshold: 5), [DriverCapability.Probe] = new(TimeoutSeconds: 4, RetryCount: 3, BreakerFailureThreshold: 5), [DriverCapability.AlarmSubscribe] = new(TimeoutSeconds: 8, RetryCount: 3, BreakerFailureThreshold: 5), [DriverCapability.AlarmAcknowledge] = new(TimeoutSeconds: 8, RetryCount: 0, BreakerFailureThreshold: 5), [DriverCapability.HistoryRead] = new(TimeoutSeconds: 60, RetryCount: 2, BreakerFailureThreshold: 5), }, DriverTier.C => new Dictionary { [DriverCapability.Read] = new(TimeoutSeconds: 10, RetryCount: 1, BreakerFailureThreshold: 0), [DriverCapability.Write] = new(TimeoutSeconds: 10, RetryCount: 0, BreakerFailureThreshold: 0), [DriverCapability.Discover] = new(TimeoutSeconds: 120, RetryCount: 1, BreakerFailureThreshold: 0), [DriverCapability.Subscribe] = new(TimeoutSeconds: 15, RetryCount: 1, BreakerFailureThreshold: 0), [DriverCapability.Probe] = new(TimeoutSeconds: 10, RetryCount: 1, BreakerFailureThreshold: 0), [DriverCapability.AlarmSubscribe] = new(TimeoutSeconds: 15, RetryCount: 1, BreakerFailureThreshold: 0), [DriverCapability.AlarmAcknowledge] = new(TimeoutSeconds: 15, RetryCount: 0, BreakerFailureThreshold: 0), [DriverCapability.HistoryRead] = new(TimeoutSeconds: 120, RetryCount: 1, BreakerFailureThreshold: 0), }, _ => throw new ArgumentOutOfRangeException(nameof(tier), tier, $"No default policy table defined for tier {tier}."), }; } /// Policy for one capability on one driver instance. /// Per-call timeout (wraps the inner Polly execution). /// Number of retry attempts after the first failure; zero = no retry. /// /// Consecutive-failure count that opens the circuit breaker; zero = no breaker /// (Tier C uses the supervisor's process-level breaker instead). /// public sealed record CapabilityPolicy(int TimeoutSeconds, int RetryCount, int BreakerFailureThreshold);