Commit Graph

493 Commits

Author SHA1 Message Date
Joseph Doherty e6280cb1da feat(runtime): announce added nodes after the materialise passes on non-rebuild applies (R2-07 T4b) 2026-07-13 11:58:40 -04:00
Joseph Doherty a86d9f65bf feat(opcua): make MaterialiseAlarmCondition idempotent for same-kind re-applies (R2-07 T4a) 2026-07-13 11:50:14 -04:00
Joseph Doherty 9dbc363ef0 feat(opcua): announce pure-add nodes via Part 3 NodeAdded model-change (R2-07 T3) 2026-07-13 11:48:02 -04:00
Joseph Doherty 0d717e4785 feat(opcua): route AddressSpaceApplier through the classifier — pure-add deploys no longer rebuild (R2-07 T2) 2026-07-13 11:46:24 -04:00
Joseph Doherty bb226f45ed feat(opcua): add AddressSpaceChangeClassifier — typed delta classification for the applier (R2-07 T1) 2026-07-13 11:38:30 -04:00
Joseph Doherty 1a698cbb97 Merge R2-11 TagConfig consolidation (arch-review round 2) [PR #434]
v2-ci / build (push) Successful in 3m55s
v2-ci / unit-tests (push) Failing after 8m14s
Findings 01/C-1, 01/P-1 (single TagConfigIntent.Parse in Commons, 4 copies
delegate, parse-once) + 05 CONV-2/UNDER-1/UNDER-6 (shared strict TagConfigJson
readers, per-driver Inspect() + writable key, FOCAS capability pre-flight,
Modbus probe timeoutMs, deploy-gate Warn|Error). Phase C runtime-strict flip
deferred. T22/T24 deferred. Auto-merged AddressSpaceApplier.cs + DriverHostActor.cs
with R2-04/R2-10; verified OpcUaServer.Tests 286/286 + Runtime.Tests 396/396.
Build clean.
2026-07-13 11:29:32 -04:00
Joseph Doherty 4449baac69 feat(adminui): writable checkbox in driver-typed tag editors (R2-11, 05/UNDER-6)
Models gain an optional bool? Writable (absent stays omitted; explicit false round-trips,
unknown keys preserved); the six editors gain a Writable checkbox (FOCAS disabled+read-only
hint). 9 model round-trip tests green. docker-dev /run verify of one editor deferred-live.
2026-07-13 11:22:12 -04:00
Joseph Doherty d62b140ccd Merge R2-04 Failure-visibility trio (arch-review round 2) [PR #433]
v2-ci / build (push) Successful in 3m48s
v2-ci / unit-tests (push) Failing after 9m14s
Findings 01/S-1 (AddressSpaceApplyOutcome failure field + apply.failed logging,
no optimistic success), 06/S-1 (Galaxy write fails closed -> BadCommunicationError
+ #5 revert, no knowingly-lost raw Write), 03/S4 (PrimaryGatePolicy default-deny
unknown-role-multi-driver on all gates + scripted-alarm emit gate). T13/T15 (2-node
live gates) deferred -- T15 is the behavior-affecting S4 live gate, must run in heavy
pass. Clean merge, build clean.
2026-07-13 11:19:24 -04:00
Joseph Doherty 4260b6ecaa feat(controlplane): deploy-gate TagConfig strictness — Warn default, Error opt-in at the draft gate (R2-11, 05/CONV-2) 2026-07-13 11:15:34 -04:00
Joseph Doherty 68588da05f test(r2-04): 2-node delivery-path guard for the primary write gate (deferred-live) 2026-07-13 11:13:31 -04:00
Joseph Doherty 1647942414 fix(r2-04): scripted-alarm alerts emit uses PrimaryGatePolicy (consistency with 03/S4) 2026-07-13 11:10:43 -04:00
Joseph Doherty 8401055c9d feat(controlplane): EquipmentTagConfigInspector — DriverType-dispatched deploy-time TagConfig inspection (R2-11) 2026-07-13 11:09:35 -04:00
Joseph Doherty c6f975b963 test(r2-04): primary-gate boot-window guards (deny-on-unknown multi-node, allow single-node) 2026-07-13 11:06:44 -04:00
Joseph Doherty f2e899aaad feat(r2-04): PrimaryGatePolicy - membership-aware default-deny + denial meter (03/S4) 2026-07-13 10:55:46 -04:00
Joseph Doherty 7bbec77716 refactor(runtime,opcuaserver): device-host normalization re-homed to Commons DeviceConfigIntent (R2-11) 2026-07-13 10:46:30 -04:00
Joseph Doherty 759125372a fix(r2-04): surface degraded address-space applies (Error log + otopcua.opcua.apply.failed) 2026-07-13 10:44:18 -04:00
Joseph Doherty 99ce5b401e fix(r2-10): in-process resilience status store 2026-07-13 10:43:47 -04:00
Joseph Doherty 12b2ce078b fix(r2-10): register the tracker's production reader on driver nodes + wiring guard 2026-07-13 10:42:18 -04:00
Joseph Doherty a452a20947 refactor(opcuaserver): composer parses TagConfig once per tag via TagConfigIntent (R2-11, 01/P-1)
Deletes the composer's ExtractTagFullName/Alarm/Historize/Array statics and (co-located
to keep the build green, per plan T5) the three OpcUaServer.Tests ExtractTag* suites whose
tables now live in Commons.Tests/TagConfigIntentTests. T10 is the grep-sweep verification.
2026-07-13 10:42:08 -04:00
Joseph Doherty e37abf36a0 fix(r2-04): Materialise* passes return swallowed-failure tallies (void -> int) 2026-07-13 10:41:19 -04:00
Joseph Doherty 37f180c5a3 fix(r2-10): periodic DPS publisher for tracker snapshots 2026-07-13 10:40:54 -04:00
Joseph Doherty b3d57ca363 fix(r2-04): Safe* helpers report failure; Apply tallies removal-pass failures into FailedNodes 2026-07-13 10:38:10 -04:00
Joseph Doherty 437f82b145 fix(r2-04): AddressSpaceApplyOutcome carries RebuildFailed/FailedNodes; SafeRebuild returns bool (01/S-1) 2026-07-13 10:36:42 -04:00
Joseph Doherty 2e14fe1f24 test(runtime): golden TagConfig corpus + compose/decode parity characterization (R2-11) 2026-07-13 10:35:47 -04:00
Joseph Doherty 46fedda3e4 Merge R2-02 ResilienceConfig hardening (arch-review round 2) [PR #431]
v2-ci / build (push) Successful in 3m53s
v2-ci / unit-tests (push) Failing after 8m15s
Findings 01/S-6 (High — clamp hostile ResilienceConfig, no ValidationException
driver-brick), 01/U-6 (delete dead bulkhead knob), 01/S-8=03/S12 (Write/Ack
non-idempotent + retry->0), 04/C-7 (JsonObject round-trip preserves unknown
keys), 01/S-7=03/S13 (per-Create options-generation kills respawn cache race).
T15/T18 deferred. STATUS.md conflict resolved additively. Build clean.
2026-07-13 10:29:43 -04:00
Joseph Doherty 901d4e6ed3 Merge R2-03 Virtual-Tag failure quality (arch-review round 2) [PR #430]
v2-ci / build (push) Successful in 4m2s
v2-ci / unit-tests (push) Failing after 8m20s
Findings 02/S13 (VT failure/timeout degrades node to Bad quality, no stale-Good
forever), 02/S12 (evaluator retry-once on disposed-race), 02/P7 (compile-cache
clear gated on changed expression set). T11/T12 deferred. STATUS.md conflict
resolved additively. Build clean.
2026-07-13 10:20:43 -04:00
Joseph Doherty 9e0a05dc6a fix(r2-02): non-lossy ResilienceFormModel round-trip — preserve unknown keys, never overwrite unparseable stored JSON (04/C-7) 2026-07-13 10:18:31 -04:00
Joseph Doherty 37664e6992 test(runtime): red — apply-boundary compile-cache clear must be gated on a changed expression set (02/P7) 2026-07-13 10:14:50 -04:00
Joseph Doherty 12b3206555 Merge R2-05 AdminUI authorization (arch-review round 2) [PR #428]
v2-ci / build (push) Successful in 3m27s
v2-ci / unit-tests (push) Failing after 8m29s
Finding 04/C-1 (High): explicit named-policy gating on all 38 AdminUI pages via
new AdminUiPolicies (ConfigEditor + AuthenticatedRead) + anti-drift reflection
guard. T15 live pass deferred (docker-dev auto-admin can't observe deny).
STATUS.md conflict resolved additively. Build clean.
2026-07-13 10:12:57 -04:00
Joseph Doherty 524a0b5606 refactor(r2-02): remove bulkhead fields from the resilience form (01/U-6) 2026-07-13 10:10:06 -04:00
Joseph Doherty d67900e455 test(host): red repro — apply-boundary cache clear races in-flight VT evaluation into ObjectDisposedException (02/S12) 2026-07-13 10:09:37 -04:00
Joseph Doherty 496f97da20 fix(r2-02): write dispatch defaults to non-idempotent — no-retry arm authoritative (03/S12 layer 2) 2026-07-13 10:04:09 -04:00
Joseph Doherty 57c224c65a fix(r2-02): range-validation warnings on the resilience override form (01/S-6 authoring layer) 2026-07-13 10:00:43 -04:00
Joseph Doherty b77bbd13b7 fix(r2-06): warning parity — malformed-endpoint warning in ServerHistorianOptions.Validate() (06/S-11) 2026-07-13 09:57:29 -04:00
Joseph Doherty b5bf4b73ff fix(adminui): gate every page with named policies — ConfigEditor on the mutating surface (R2-05, 04/C-1) 2026-07-13 09:56:08 -04:00
Joseph Doherty 05c35c93d9 test(host): pin sequential clear-then-recompile contract of RoslynVirtualTagEvaluator (02/S12 baseline) 2026-07-13 09:55:44 -04:00
Joseph Doherty f2006adb79 test(runtime): pin VT inputs-ready gate — no Bad flash before all dependencies arrive (02/S13) 2026-07-13 09:55:04 -04:00
Joseph Doherty 987459fab3 test(runtime): pin VT Bad-quality transition semantics — once-per-transition, same-value recovery, throw path (02/S13) 2026-07-13 09:53:59 -04:00
Joseph Doherty 35d8abee84 fix(r2-06): RED — ServerHistorianOptionsValidator unit + wiring tests (net-new type; does not compile) (06/S-11) 2026-07-13 09:53:21 -04:00
Joseph Doherty 01253818de fix(runtime): VT script failure/timeout degrades node quality to Bad once per transition (02/S13) 2026-07-13 09:52:56 -04:00
Joseph Doherty 183b72b7cb feat(security): add ConfigEditor + AuthenticatedRead AdminUI policies behind AdminUiPolicies constants 2026-07-13 09:52:13 -04:00
Joseph Doherty c5ca3e8587 feat(runtime): EvaluationResult carries OpcUaQuality; VT bridge publishes it + historizes BadInternalError on Bad (02/S13) 2026-07-13 09:51:15 -04:00
Joseph Doherty b977548593 test(runtime): red repro — VT script failure after success leaves node stale-Good (02/S13) 2026-07-13 09:48:41 -04:00
Joseph Doherty 45455b7677 Merge branch 'fix/archreview-crit2-vt-timeout' 2026-07-09 06:07:33 -04:00
Joseph Doherty 0ed30483b4 Merge branch 'fix/archreview-crit1-split-brain-resolver' 2026-07-09 06:07:33 -04:00
Joseph Doherty 8a8b9ec564 test(archreview #10/#13): guard prod DI binds the REAL invoker factory
AddOtOpcUaDriverFactories (the driver-node bootstrap) must bind the real
DriverCapabilityInvokerFactory — not the NullDriverCapabilityInvokerFactory
pass-through — and it must mint a real CapabilityInvoker per instance. This is the
deterministic DI half of the #10 live gate: a refactor dropping back to the
pass-through would make the whole retry/breaker/bulkhead pipeline inert in production
while every pass-through-defaulting unit test stays green. 3 tests (real factory bound,
real invoker minted, shared singletons), non-fixtured. Host.IntegrationTests.
2026-07-08 23:16:23 -04:00
Joseph Doherty 75403caa1a feat(archreview #13): apply per-instance ResilienceConfig from the deploy artifact
The DriverInstance.ResilienceConfig column was authored in AdminUI, persisted to the
entity, and serialized into the deployment artifact by ConfigComposer — but the runtime
read path dropped it: DriverInstanceSpec didn't carry it and the invoker factory always
passed null, so every driver got tier defaults regardless of its configured overrides
(a silent dead-config gap — #10's residual sub-finding).

Read-path plumbing (write side was already complete):
- DriverInstanceSpec gains ResilienceConfig; DeploymentArtifact.TryReadSpec reads the column.
- IDriverCapabilityInvokerFactory.Create takes resilienceConfigJson; DriverHostActor.SpawnChild
  threads spec.ResilienceConfig; the concrete factory parses it (ParseOrDefaults, layering on the
  tier), logs any parse diagnostic (never throws), and builds the invoker with the merged options.
- Invalidate-on-change: the pipeline cache keys on (instance, host, capability) and ignores options
  on a hit, so Create() now Invalidate()s the instance's cached pipelines first (no-op on first
  spawn) — a respawn with changed options rebuilds them.
- DriverSpawnPlanner treats a ResilienceConfig change as a stop+respawn (the invoker/options are
  bound to the child at spawn); a pure DriverConfig change stays an in-place delta (no reconnect).
- Host DI passes a logger to the factory for the parse diagnostic.

Verification (deterministic): factory Create applies a retryCount:0 override to actual execution
(control test proves tier default retries), invalidates the instance's cache on re-create (scoped —
sibling survives), malformed config logs+falls-back; planner respawns on ResilienceConfig change
(incl null→json) and stays delta on a pure config change; artifact parse carries/omits the column.
Core.Tests 243 (+5), Runtime.Tests 363 (+5), Host builds clean.
2026-07-08 22:48:17 -04:00
Joseph Doherty 62556c245a test(archreview #10): cover actor-context preservation through the REAL CapabilityInvoker
The pass-through NullDriverCapabilityInvoker returns the call-site task directly, so it
never exercises the real invoker's internal `pipeline.ExecuteAsync(...).ConfigureAwait(false)`.
Adds a test-only Core reference to Runtime.Tests (the PRODUCTION Runtime assembly stays
Polly-free via the seam) and a discovery test that drives DriverInstanceActor through a real
CapabilityInvoker over a genuinely-yielding ITagDiscovery driver — proving the invoker's
internal ConfigureAwait(false) does NOT leak to the actor's own await, so the post-await
Context.Parent.Tell still runs (DiscoveredNodesReady arrives). Runtime.Tests 358, Core.Tests 238.
2026-07-08 21:38:14 -04:00
Joseph Doherty bacea1a44f fix(archreview #10): wire CapabilityInvoker into prod dispatch via a Polly-free seam
Remediates RESILIENCE-DISPATCH-GAP (surfaced by the 07/C-1 analyzer): the Phase 6.1
CapabilityInvoker resilience pipeline (retry / breaker / bulkhead / telemetry) was
constructed only in tests — the production dispatch layer called driver-capability
methods directly, bypassing it entirely.

The filed plan (thread CapabilityInvoker directly into DriverInstanceActor) is
INFEASIBLE: Runtime is deliberately Polly-free (references Core.Abstractions, not
Core — the same boundary IDriverFactory documents). So this introduces a seam,
mirroring IDriverFactory exactly:

- Core.Abstractions: IDriverCapabilityInvoker (+ NullDriverCapabilityInvoker
  pass-through) and IDriverCapabilityInvokerFactory (+ null factory).
- Core: CapabilityInvoker now implements the interface; new
  DriverCapabilityInvokerFactory builds a per-instance invoker over the
  process-singleton pipeline builder + status tracker + tier resolver.
- Runtime: DriverInstanceActor takes an IDriverCapabilityInvoker (default =
  pass-through) and routes all 6 dispatch sites (write, alarm-ack, subscribe,
  unsubscribe, alarm-subscribe, discover) through it; per-host key resolved via
  IPerCallHostResolver for single-ref calls, driver-instance key for bulk calls.
  DriverHostActor builds + injects the real invoker per spawned driver.
- Host: DriverFactoryBootstrap registers the tracker, pipeline builder, and
  concrete factory (needs DriverFactoryRegistry.GetTier). Runtime SCE resolves the
  factory from DI like IDriverFactory; pass-through on nodes without it bound.
- Analyzer: IDriverCapabilityInvoker.ExecuteAsync/ExecuteWriteAsync are now
  recognized wrapper homes (interface-typed invoker calls must suppress OTOPCUA0001
  too). All 6 RESILIENCE-DISPATCH-GAP pragmas removed — the analyzer (an error in
  Runtime) is now the standing regression guard.

Also adds retry / breaker-open / breaker-close LOGGING to the pipeline builder —
the operator-facing observability surface (the Admin /hosts reader, Phase 6.1
Stream E.2/E.3, was never built, so the pipeline otherwise runs silently).

Scope notes:
- Tier-DEFAULT policy only: DriverInstanceSpec (the deploy artifact) does not carry
  the per-instance ResilienceConfig JSON, so overrides aren't applied yet (tracked
  follow-up: plumb ResilienceConfig through the composer/artifact).
- GenericDriverNodeManager's call is NOT wired: that class is test-only scaffolding
  (zero production references — the Server has its own address-space path); its
  pragma is re-annotated accordingly, not left as a "tracked gap".

Verification (unit + analyzer; live behavioral gate still pending — see FOLLOWUP-10):
- Negative control: unwrapping any site fails the Runtime build with OTOPCUA0001.
- New Runtime guard (recording invoker): write routes via ExecuteWriteAsync with the
  IPerCallHostResolver host; subscribe via ExecuteAsync — proves runtime routing.
- New analyzer test: the interface is a valid wrapper home.
- New pipeline-builder test: retry events are logged.
- Full solution builds clean (0 errors); Runtime.Tests 357, Core.Tests 238,
  Analyzers.Tests 32 all green; pass-through default keeps existing dispatch tests
  byte-for-byte unchanged.
2026-07-08 21:33:16 -04:00
Joseph Doherty a25c9ed097 test(cluster): hard-kill failover integration test on the 2-node harness (arch-review 03/S1)
The load-bearing integration half of Critical 1. Adds TwoNodeClusterHarness.HardKillNodeAAsync
(canonical in-process crash sim: shut down node A's remoting transport via
IRemoteActorRefProvider.Transport.Shutdown() — associations drop, heartbeats/gossip stop, NO
graceful Cluster.Leave, so node B's failure detector marks A Unreachable) + a
WaitForNodeBSoleDriverLeaderAsync helper (waits for a STABLE takeover: B sole Up member + driver
role-leader). HardKillFailoverTests crashes the oldest node and asserts the survivor takes over —
the exact scenario the graceful StopNodeBAsync path (Cluster.Leave, no downing decision) cannot
exercise.

Live-verified in-process (~35s: acceptable-heartbeat-pause 10s + stable-after 15s + convergence).
Negative control confirmed the test BITES: forcing explicit NoDowning
(akka.cluster.downing-provider-class = "") makes it time out with the node stuck Up-but-Unreachable.

FINDING surfaced by the negative control: removing Critical 1's typed
ClusterOptions.SplitBrainResolver did NOT break failover, because Akka.Cluster.Hosting's
WithClustering applies SplitBrainResolverOption.Default when the option is null — enabling the SBR
downing provider that reads the akka.conf keep-oldest block (present on master BEFORE Critical 1).
So the cluster was NOT running NoDowning before Critical 1; the typed option reinforces the akka.conf
strategy rather than being the sole activator, and Critical 1's 'HOCON inert / NoDowning' premise is
inaccurate. The test guards the failover OUTCOME regardless of activation path. Comments corrected to
reflect this; flagged for review.
2026-07-08 18:24:49 -04:00