using System.Text.Json; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.Resilience; /// /// Parses the DriverInstance.ResilienceConfig JSON column into a /// instance layered on top of the tier defaults. /// Every key in the JSON is optional; missing keys fall back to the tier defaults from /// . /// /// /// Example JSON shape per Phase 6.1 Stream A.2: /// /// { /// "bulkheadMaxConcurrent": 16, /// "bulkheadMaxQueue": 64, /// "capabilityPolicies": { /// "Read": { "timeoutSeconds": 5, "retryCount": 5, "breakerFailureThreshold": 3 }, /// "Write": { "timeoutSeconds": 5, "retryCount": 0, "breakerFailureThreshold": 5 } /// } /// } /// /// /// Unrecognised keys + values are ignored so future shapes land without a migration. /// Per-capability overrides are layered on top of tier defaults — a partial policy (only /// some of TimeoutSeconds/RetryCount/BreakerFailureThreshold) fills in the other fields /// from the tier default for that capability. /// /// Parser failures (malformed JSON, type mismatches) fall back to pure tier defaults /// + surface through an out-parameter diagnostic. Callers may log the diagnostic but should /// NOT fail driver startup — a misconfigured ResilienceConfig should never brick a /// working driver. /// public static class DriverResilienceOptionsParser { private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true, AllowTrailingCommas = true, ReadCommentHandling = JsonCommentHandling.Skip, }; /// /// Parse the JSON payload layered on 's defaults. Returns the /// effective options; is null on success, or a /// human-readable error message when the JSON was malformed (options still returned /// = tier defaults). /// public static DriverResilienceOptions ParseOrDefaults( DriverTier tier, string? resilienceConfigJson, out string? parseDiagnostic) { parseDiagnostic = null; var baseDefaults = DriverResilienceOptions.GetTierDefaults(tier); var baseOptions = new DriverResilienceOptions { Tier = tier, CapabilityPolicies = baseDefaults }; if (string.IsNullOrWhiteSpace(resilienceConfigJson)) return baseOptions; ResilienceConfigShape? shape; try { shape = JsonSerializer.Deserialize(resilienceConfigJson, JsonOpts); } catch (JsonException ex) { parseDiagnostic = $"ResilienceConfig JSON malformed; falling back to tier {tier} defaults. Detail: {ex.Message}"; return baseOptions; } if (shape is null) return baseOptions; var merged = new Dictionary(baseDefaults); if (shape.CapabilityPolicies is not null) { foreach (var (capName, overridePolicy) in shape.CapabilityPolicies) { if (!Enum.TryParse(capName, ignoreCase: true, out var capability)) { parseDiagnostic ??= $"Unknown capability '{capName}' in ResilienceConfig; skipped."; continue; } var basePolicy = merged[capability]; merged[capability] = new CapabilityPolicy( TimeoutSeconds: overridePolicy.TimeoutSeconds ?? basePolicy.TimeoutSeconds, RetryCount: overridePolicy.RetryCount ?? basePolicy.RetryCount, BreakerFailureThreshold: overridePolicy.BreakerFailureThreshold ?? basePolicy.BreakerFailureThreshold); } } return new DriverResilienceOptions { Tier = tier, CapabilityPolicies = merged, BulkheadMaxConcurrent = shape.BulkheadMaxConcurrent ?? baseOptions.BulkheadMaxConcurrent, BulkheadMaxQueue = shape.BulkheadMaxQueue ?? baseOptions.BulkheadMaxQueue, }; } private sealed class ResilienceConfigShape { public int? BulkheadMaxConcurrent { get; set; } public int? BulkheadMaxQueue { get; set; } public Dictionary? CapabilityPolicies { get; set; } } private sealed class CapabilityPolicyShape { public int? TimeoutSeconds { get; set; } public int? RetryCount { get; set; } public int? BreakerFailureThreshold { get; set; } } }