[archreview #10] Observe resilience retry/breaker firing in live dispatch (+ ResilienceConfig not in deploy artifact) #456
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Status: non-blocking follow-up. The dispatch wiring itself is merged (
62556c24, seamIDriverCapabilityInvoker/IDriverCapabilityInvokerFactoryinCore.Abstractions;DriverInstanceActorwraps its 6 dispatch sites) and DI-wiring was live-confirmed on docker-dev (2026-07-08). Two things remain:ResilienceConfigis not in the deploy artifact, so the pipeline runs tier-defaults only. Operator-authored per-driverResilienceConfigis authored in AdminUI but never reaches the runtime pipeline. Until it does, a behavioral live gate only proves plumbing, not operator policy.Source of record:
archreview/plans/FOLLOWUP-10-resilience-dispatch-gap.md(rig recipe there),archreview/plans/STATUS.md(#10 row).Acceptance:
ResilienceConfiginto the deploy artifact so the runtime pipeline uses authored policy (not just tier-defaults), then re-verify the behavioral gate with a non-default policy.Update 2026-07-15 — sub-gap CLOSED (item 2 done, live-verified on both nodes); narrowed to item 1. master
b364a499(PR #461).Finding: the artifact-threading was already wired; the gap was coverage + live re-verify
The issue body carried the pre-#13 banner. In current master the per-instance
ResilienceConfigalready threads end-to-end:ConfigComposer.SnapshotAndFlattenAsyncserialises the wholeDriverInstanceentity →DeploymentArtifact.ParseDriverInstancesrecovers it ontoDriverInstanceSpec.ResilienceConfig(:173) →DriverHostActor.SpawnChild(:1727) passes it toDriverCapabilityInvokerFactory.Create, which parses + layers it onto the driver's Polly pipeline;DriverSpawnPlanrespawns on change. The only untested leg was the real composer→artifact serialization (the existingDeploymentArtifactTestshand-build the JSON).✅ Item 2 — DONE (threading + non-default-policy re-verify)
ConfigComposerTests.ResilienceConfig_survives_ConfigComposer_to_ParseDriverInstances_round_trip: seeds aDriverInstanceWITH a non-default override + one WITHOUT, runs the realSnapshotAndFlattenAsync, assertsParseDriverInstancesrecovers the override byte-for-byte (guards against a future projection /[JsonIgnore]silently reverting authored policy to tier defaults). ControlPlane.TestsConfigComposerTests6/6 green.Subscribe.retryCount:999override, deployed headless. Proof (archreview/plans/artifacts/456-resilience-config-live-proof-2026-07-15.md):Deployment.ArtifactBlob(4055 B) containsResilienceConfig+999.[WRN] Driver resilience config for instance=drv-res type=OpcUaClient: Subscribe.retryCount 999 exceeds the cap (100); clamped.→[INF] spawned OpcUaClient driver drv-res.999appears verbatim in the runtime log. If the artifact didn't carryResilienceConfig,spec.ResilienceConfigwould be null, the parser would take pure tier-defaults, and no clamp diagnostic would fire. So the diagnostic firing = the authored policy rode DB → composer → artifact → runtime pipeline, on both nodes.⏳ Item 1 — remaining (raw retry/breaker log line on an actual fault)
Not captured. It needs a connect-then-fault-mid-flight trigger: the invoker-wrapped capability calls (
Subscribe/Discover/Write) only fire after a driver reaches Connected (DriverInstanceActor.ResubscribeDesiredruns on theBecome(Connected)transition), so a driver at a dead endpoint stays in Reconnecting and never exercises the wrapped path — the same wall the 2026-07-08 run hit. Reliable trigger: an idempotent Write to a Connected-then-killed driver (LDAP-gated Client.CLI write) or an S7 dead-socket reconnect. The retry/breaker behaviour is already proven deterministically (CapabilityInvokerTestsretry, the pipeline-builder logging test,DriverCapabilityInvokerFactoryTestsoverride-applies-to-behaviour), so this remaining line is operator-confidence only.Acceptance:
ResilienceConfiginto the deploy artifact + re-verify with a non-default policy — done + live-verified on both nodes.Item 1 — live grind done; write-trigger proven structurally impossible. Closing. master
46bc458f(PR #466).The grind (full end-to-end, docker-dev)
Authored a live Modbus equipment hierarchy via SQL (Equipment-kind ns →
drv-mbModbus @10.100.0.35:5020→ Device → Area/Line →EQ-111111111111→ writablereg1). Deployed headless →spawned Modbus driver drv-mb→connected→subscribed. Baseline: live readGood, LDAP-authed (multi-role/WriteOperate) write=123succeeded — the wrapped write path is live end-to-end (OPC UA write →NodeWriteRouter→DriverHostActor.RouteNodeWrite→DriverInstanceActor.WriteAsyncinvoker-wrapped →ModbusDriver→ sim). Thendocker paused the peer and drove 4 writes.Result: 4 failing wrapped writes → 0 retry/breaker lines on either central
Each write logged
Operator write … rejected: StatusCode=0x80020000(node reverted), butgrep 'Driver resilience retry|circuit-breaker OPENED'= 0.Why — the issue's write-trigger is structurally impossible
ModbusDriver.WriteAsynccatches every exception → returnsWriteResult(StatusBadInternalError)= 0x80020000; it never throws (ModbusDriver.cs:964-967).CapabilityInvokerfeeds only exceptions to the Polly pipeline (CapabilityInvoker.cs:84/112/147) — it does not inspect the returned status. BreakerShouldHandleisHandle<Exception>;Writeretry is pinned to 0 (R2-02/S-8).Where the line actually lives
Only a session driver (OpcUaClient) can emit it — its
DiscoverAsync(remote browse) /SubscribeAsyncthrow on a session fault, and the post-connect discovery loop runsDiscoverAsyncunder a 30s Polly timeout. The line requires a session fault landing mid-Discover/Subscribebefore keepalive drops the driver toReconnecting— a genuine production timing race, not deterministically forcible on the rig (polling drivers'Subscribe/Discoverare lazy/offline and never throw). This is exactly why three sessions with polling-driver fixtures never observed it.Finding artifact · FOLLOWUP-10 updated.
Disposition
DriverResiliencePipelineBuilderlogging unit test +CapabilityInvokerretry tests + the factory override test; the wrapped dispatch path is now live-confirmed end-to-end; and the write-based live trigger is structurally impossible. The only real live trigger is an OpcUaClient session fault mid-Discover/Subscribe — an operator/staging observation during a real network blip, not a code deliverable.Closing as resolved.