580c45f494a439a91a800c07a6be9b9fddb17797
2 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bd6568bcbd |
Phase 6.1 Stream B.4 — wire ScheduledRecycleHostedService into bootstrap
Task #125 / #137. The hosted service + scheduler classes already shipped; this commit connects them to the published-generation driver list so a Tier C driver with `RecycleIntervalSeconds` in its `ResilienceConfig` actually gets an armed scheduler at bootstrap. Wiring: - `DriverFactoryRegistry.Register` gains an optional `DriverTier` parameter (default Tier.A). Existing call sites unchanged — `GalaxyProxyDriverFactoryExtensions.Register` explicitly passes Tier.C so the bootstrapper can identify out-of-process drivers without a per-driver-type allow-list. - `DriverResilienceOptions` + parser grow `RecycleIntervalSeconds`. Tier A/B values are rejected with a diagnostic (decision #74 — recycling an in-process driver would kill every OPC UA session). Non-positive values are rejected the same way. - `DriverInstanceBootstrapper` auto-arms a `ScheduledRecycleScheduler` after a successful driver register when: (1) the registered tier is C, (2) the row's ResilienceConfig carries a positive recycle interval, (3) DI has an `IDriverSupervisor` keyed by that `DriverInstanceId`. Missing supervisor → warn + skip (no crash). That keeps the wiring harmless by default: no driver ships a supervisor today, so the hosted service runs with zero schedulers out of the box. - `Program.cs` registers `ScheduledRecycleHostedService` as singleton (shared with `DriverInstanceBootstrapper`) + hosted service (drives the tick loop). Constructor changes on the bootstrapper ripple into DI resolution automatically. Tests: 4 new parser tests covering RecycleIntervalSeconds on Tier C happy path, null default, Tier A/B rejection, non-positive rejection. Existing 283 Server.Tests + 200 Core.Tests all still green. No behavioural change for existing deployments: Galaxy driver + any future Tier C driver gain the opt-in automatically; Tier A/B drivers (FOCAS, Modbus, S7, AB CIP, AB Legacy, TwinCAT) are structurally excluded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7b50118b68 |
Phase 6.1 Stream A follow-up — DriverInstance.ResilienceConfig JSON column + parser + OtOpcUaServer wire-in
Closes the Phase 6.1 Stream A.2 "per-instance overrides bound from DriverInstance.ResilienceConfig JSON column" work flagged as a follow-up when Stream A.1 shipped in PR #78. Every driver can now override its Polly pipeline policy per instance instead of inheriting pure tier defaults. Configuration: - DriverInstance entity gains a nullable `ResilienceConfig` string column (nvarchar(max)) + SQL check constraint `CK_DriverInstance_ResilienceConfig_IsJson` that enforces ISJSON when not null. Null = use tier defaults (decision #143 / unchanged from pre-Phase-6.1). - EF migration `20260419161008_AddDriverInstanceResilienceConfig`. - SchemaComplianceTests expected-constraint list gains the new CK name. Core.Resilience.DriverResilienceOptionsParser: - Pure-function parser. ParseOrDefaults(tier, json, out diag) returns the effective DriverResilienceOptions — tier defaults with per-capability / bulkhead overrides layered on top when the JSON payload supplies them. Partial policies (e.g. Read { retryCount: 10 }) fill missing fields from the tier default for that capability. - Malformed JSON falls back to pure tier defaults + surfaces a human-readable diagnostic via the out parameter. Callers log the diag but don't fail startup — a misconfigured ResilienceConfig must not brick a working driver. - Property names + capability keys are case-insensitive; unrecognised capability names are logged-and-skipped; unrecognised shape-level keys are ignored so future shapes land without a migration. Server wire-in: - OtOpcUaServer gains two optional ctor params: `tierLookup` (driverType → DriverTier) + `resilienceConfigLookup` (driverInstanceId → JSON string). CreateMasterNodeManager now resolves tier + JSON for each driver, parses via DriverResilienceOptionsParser, logs the diagnostic if any, and constructs CapabilityInvoker with the merged options instead of pure Tier A defaults. - OpcUaApplicationHost threads both lookups through. Default null keeps existing tests constructing without either Func unchanged (falls back to Tier A + tier defaults exactly as before). Tests (13 new DriverResilienceOptionsParserTests): - null / whitespace / empty-object JSON returns pure tier defaults. - Malformed JSON falls back + surfaces diagnostic. - Read override merged into tier defaults; other capabilities untouched. - Partial policy fills missing fields from tier default. - Bulkhead overrides honored. - Unknown capability skipped + surfaced in diagnostic. - Property names + capability keys are case-insensitive. - Every tier × every capability × empty-JSON round-trips tier defaults exactly (theory). Full solution dotnet test: 1215 passing (was 1202, +13). Pre-existing Client.CLI Subscribe flake unchanged. Production wiring (Program.cs) example: Func<string, DriverTier> tierLookup = type => type switch { "Galaxy" => DriverTier.C, "Modbus" or "S7" => DriverTier.B, "OpcUaClient" => DriverTier.A, _ => DriverTier.A, }; Func<string, string?> cfgLookup = id => db.DriverInstances.AsNoTracking().FirstOrDefault(x => x.DriverInstanceId == id)?.ResilienceConfig; var host = new OpcUaApplicationHost(..., tierLookup: tierLookup, resilienceConfigLookup: cfgLookup); Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |