Files
scadaproj/components
Joseph Doherty a1df7ef26c fix(secrets): root-cause + fix the Akka replicator's hosted-process DI deadlock (0.2.2)
Closes the defect in scadaproj#1. The hang was never Akka: the package's DI
wiring closed a circular singleton dependency the container cannot see through
factory lambdas — ISecretStore (ReplicatingSecretStore decorator) ->
ISecretReplicator -> SecretReplicationActorProvider -> ISecretCacheInvalidator
-> DefaultSecretResolver -> ISecretStore. Resolution recurses around the loop
until MS.DI's StackGuard hops it onto a fresh thread-pool thread, which then
blocks forever on a singleton call-site lock the first thread still holds:
a silent permanent hang instead of a stack overflow. Managed stacks from
dotnet-dump show the repeating cycle and both parked threads; both candidate
causes in the issue (DistributedPubSub.Get vs the Lazy lock, missing
Akka.Cluster.Tools HOCON) are disproven — the actor constructor was never
reached, and the deadlock reproduces on a single non-clustered node.

Fix: defer the one cycle-closing edge. The provider now gets a
DeferredSecretCacheInvalidator that resolves the real invalidator on first
eviction — which only happens when a replicated row is applied, strictly after
graph resolution. Severing the edge instead is wrong: a null-invalidator
experiment ran the live gate at 5/6, with deleted secrets still resolving on
the peer. The SqlServer package never had the cycle (its replicator chain
never touches the invalidator), which is why the hub gate always passed.

Verified: live 2-node convergence gate now 6/6 (was: infinite hang), including
the delete-visibility check that proves the deferred invalidator really evicts.
New HostedProcessResolutionTests builds the graph as a host does (container-
registered ActorSystem, hosted services, watchdogged resolves) and fails on
0.2.1; DeferredSecretCacheInvalidatorTests pins the wrapper contract. Full
suite 180 passed / 0 failed / 15 skipped (env-gated live SQL).

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-18 14:39:40 -04:00
..

Component normalization

This tree normalizes how cross-cutting components work across the sister projects indexed in ../CLAUDE.md (currently OtOpcUa, MxAccessGateway, ScadaBridge). The sister repos are deliberately decoupled — separate processes coupled only over wire protocols — so the same concern (auth, logging, config, health, …) tends to get re-implemented three times and drift apart. This folder is where we write down the one target for each such component, record where each project stands today (verified against real code), and track the gaps that close the distance between them.

The goal is convergence toward shared, versioned contracts/libraries — see each component's shared-contract/. Nothing here changes project code directly; these are specs and analyses that drive changes made in the individual repos.

Component registry

Component Status Applies to Goal Folder
Auth (login / identity / authz) Draft OtOpcUa, MxAccessGateway, ScadaBridge Path to shared code (ZB.MOM.WW.Auth) auth/
UI Theme (layout / tokens / components) Draft OtOpcUa, MxAccessGateway, ScadaBridge Path to shared code (ZB.MOM.WW.Theme) ui-theme/
Health (readiness / liveness / active-node) Draft OtOpcUa, MxAccessGateway, ScadaBridge Shared ZB.MOM.WW.Health lib (3 packages) health/
Observability (metrics / traces / logs) Draft OtOpcUa, MxAccessGateway, ScadaBridge Shared ZB.MOM.WW.Telemetry lib (2 packages) observability/
Config + validation (options / startup validation) Draft OtOpcUa, MxAccessGateway, ScadaBridge Shared ZB.MOM.WW.Configuration lib (1 package) configuration/
Audit (event model + writer seam) Draft OtOpcUa, MxAccessGateway, ScadaBridge Path to shared code (ZB.MOM.WW.Audit) audit/
Secrets (encrypted store + ${secret:} resolution) Built + reference-consumer adopted OtOpcUa, MxAccessGateway, ScadaBridge Shared ZB.MOM.WW.Secrets lib (3 packages + CLI) secrets/

Add a row when you start normalizing a new component. Status: DraftReviewedAdoptingConverged.

Folder convention

Every normalized component is one subfolder of components/ with this layout:

<component>/
  README.md                     # overview + per-project status table (links into the docs below)
  spec/
    SPEC.md                     # the ONE normalized target for this component
  shared-contract/              # only when the goal is shared code
    <Package>.md                # proposed shared-library API: packages, interfaces, options, records
  current-state/
    <project>/                  # one subfolder per project the component applies to
      CURRENT-STATE.md          # how it works in THAT project today (code-verified) + adoption plan
  GAPS.md                       # divergences of each project vs SPEC.md + the extraction/adoption backlog
  • spec/SPEC.md is authoritative: the single design every project should converge on.
  • shared-contract/ exists only when the component's goal is shared code (vs docs-only convergence). It is the proposed public API of the library to extract — a contract on paper, not a created package.
  • current-state/<project>/ is descriptive, not aspirational. It must match the project's real code, with file:line references. Each ends in an Adoption plan: what that project deletes/replaces to reach the spec, and what stays bespoke.
  • GAPS.md is the working backlog: it turns the delta between every current-state and the spec into concrete, prioritized items.

Workflow to normalize a component

  1. Map current state from code. For each applicable project, read the actual implementation (not just its CLAUDE.md) and write current-state/<project>/CURRENT-STATE.md with file:line refs. Fan out one reader per project — they're independent.
  2. Write the target. Synthesize the common ground and the divergences into spec/SPEC.md — the one design. Call out explicitly what is normalized vs. what stays per-project (domain differences that should not be forced together).
  3. Propose the contract (if shared-code goal). Turn the spec's normalized seams into a concrete library API in shared-contract/. Keep the surface minimal — extract only what is genuinely common; leave domain-specific logic in the projects.
  4. Log gaps + adoption. Fill GAPS.md with per-project divergences and an adoption/extraction backlog (priority / effort / risk). Add the per-project "what to delete/replace" to each current-state doc.
  5. Register. Add/maintain the component's row in the registry table above.

Maintenance rules

  • current-state/<project>/CURRENT-STATE.md must stay code-verified. When a project's auth (or whichever component) changes materially, update its current-state doc and re-check GAPS.md.
  • The spec/ is changed deliberately, with the same "update the doc and the consequences" discipline the sister repos use — moving the target re-opens gaps.
  • Keep cross-references accurate: README.md status tables link to the docs; don't let them rot.