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