32055c2238
Flipped the docker-dev rig end-to-end to MeshTransport:Mode=ClusterClient
(OTOPCUA_MESH_MODE, no rebuild) and ran the gate. PASSED for the transport:
deploy seals on ClusterClient, a stopped node fails the deploy naming it, the
MaintenanceMode hatch shrinks the DB-sourced contact set live (6->5->6 on both
centrals independently), and the frame-size canary stayed silent.
Four findings, recorded in 2026-07-22-mesh-phase2-live-gate.md:
A. A 10-30s post-restart delivery window exists, but the DPS control fails
identically -- it is a cluster-convergence property, not a Phase 2
regression. Running the control is the only reason this is a note and not
a false attribution.
B. buffer-size=0's stated rationale was OVERSTATED. A node applies a TimedOut
deployment anyway -- live-observed at +44s -- via DriverHostActor.Bootstrap
replaying the orphan Applying row on restart. Zero buffering removes the
silent, timer-driven replay, not the boot-time DB one. Corrected in the
plan decisions table, Configuration.md, and CLAUDE.md; the decision stands,
the justification narrowed.
C. Stopping BOTH centrals strands the non-seed nodes at the Akka membership
layer (frozen mid-Exiting, not Unreachable, so auto-down does not help).
Transport-independent; a Phase 7 downing/rejoin drill item.
D. The AdminUI driver Restart/Reconnect buttons (DriverStatusPanel) are
rendered by no page -- orphaned since v3 Batch 2 retired /clusters/{id}/
drivers. So gate steps 6/7 could not run; the driver-control node-side leg
is proven live only because DispatchDeployment shares its EventStream path.
Step 8's planned log-signal does not exist (HandleApplyAck is silent on
success); proven instead by both centrals independently logging per-instance
ClusterClient creation, which a singleton cannot.
Also adds a repo-root .dockerignore: docker-dev/Dockerfile does COPY . ., which
was pulling .claude/worktrees (19GB of agent checkouts with their own bin/obj)
into the build context and exhausting the Docker VM disk mid-COPY.
Rig restored to Mode=Dps, six members, no leftover maintenance flags.
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
141 lines
6.7 KiB
JSON
141 lines
6.7 KiB
JSON
{
|
|
"planPath": "docs/plans/2026-07-22-mesh-phase2-clusterclient-transport.md",
|
|
"note": "Per-cluster mesh Phase 2. Decisions pre-made: config-flagged dark switch (MeshTransport:Mode), buffer-size=0, alarm-commands central leg in scope. Three design-doc corrections and the single-mesh duplicate-delivery trap are documented in the plan header \u2014 read them before Task 3.",
|
|
"tasks": [
|
|
{
|
|
"id": 0,
|
|
"subject": "Task 0: MeshTransportOptions + validator + registration",
|
|
"status": "completed",
|
|
"classification": "small",
|
|
"parallelizableWith": [
|
|
1
|
|
],
|
|
"commit": "5439f148"
|
|
},
|
|
{
|
|
"id": 1,
|
|
"subject": "Task 1: receptionist extension, buffer-size=0, frame-size logging (effective-config test)",
|
|
"status": "completed",
|
|
"classification": "small",
|
|
"parallelizableWith": [
|
|
0
|
|
],
|
|
"commit": "7b71a6a3",
|
|
"note": "First draft of the buffering test was VACUOUS -- Config.GetInt returns 0 for a missing key, so it passed before the feature existed. Rewritten to assert through ClusterClientSettings/ClusterReceptionistSettings."
|
|
},
|
|
{
|
|
"id": 2,
|
|
"subject": "Task 2: MeshCommand envelope + generation-suffixed ClusterClient factory",
|
|
"status": "completed",
|
|
"classification": "small",
|
|
"blockedBy": [
|
|
0
|
|
],
|
|
"commit": "0d6669d5",
|
|
"note": "Dropped a third test that asserted ClusterClientSettings directly and never touched the factory."
|
|
},
|
|
{
|
|
"id": 3,
|
|
"subject": "Task 3: CentralCommunicationActor \u2014 dark-switched SendToAll fan-out, DB-sourced contacts, ApplyAck forward",
|
|
"status": "completed",
|
|
"classification": "high-risk",
|
|
"blockedBy": [
|
|
1,
|
|
2
|
|
],
|
|
"parallelizableWith": [
|
|
4
|
|
],
|
|
"commit": "d5b5cb6e",
|
|
"note": "SendToAll sabotage-verified. Two test-harness bugs found: SubscribeAck goes to the SENDER, and EventFilter matches case-INSENSITIVELY (a 'was NOT' filter also caught 'was not created')."
|
|
},
|
|
{
|
|
"id": 4,
|
|
"subject": "Task 4: NodeCommunicationActor \u2014 inbound to node-local EventStream, outbound ApplyAck",
|
|
"status": "completed",
|
|
"classification": "standard",
|
|
"blockedBy": [
|
|
1,
|
|
2
|
|
],
|
|
"parallelizableWith": [
|
|
3
|
|
],
|
|
"commit": "915beec1",
|
|
"note": "No Ask crosses the boundary in Phase 2, so this actor is far smaller than the sister project's."
|
|
},
|
|
{
|
|
"id": 5,
|
|
"subject": "Task 5: node subscribers accept EventStream too; rename _coordinatorOverride -> _ackRouter",
|
|
"status": "completed",
|
|
"classification": "standard",
|
|
"blockedBy": [
|
|
4
|
|
],
|
|
"commit": "5cb0e721",
|
|
"note": "Two wiring tests added -- unit tests either side of the seam both pass with the subscription deleted. First draft raced (ActorOf returns before PreStart; EventStream publish to nobody is dropped silently); alarm version was vacuous until the warm-up moved outside the assertion window."
|
|
},
|
|
{
|
|
"id": 6,
|
|
"subject": "Task 6: wire both sides, receptionist registration, pin the wire-contract actor paths",
|
|
"status": "completed",
|
|
"classification": "standard",
|
|
"blockedBy": [
|
|
3,
|
|
5
|
|
],
|
|
"commit": "16d59856",
|
|
"note": "Dropped a planned 'not a singleton' assertion: two candidate discriminators were both invalid (a SingletonManager also sits at /user/<name>; /singleton is null even for the known singleton config-publish). A sabotage re-registering via WithSingleton left both green, which exposed them. Property deferred to Task 8."
|
|
},
|
|
{
|
|
"id": 7,
|
|
"subject": "Task 7: route the five publish sites through the mesh router (nullable, mediator fallback)",
|
|
"status": "completed",
|
|
"classification": "standard",
|
|
"blockedBy": [
|
|
6
|
|
],
|
|
"note": "Sabotage-verified by inverting the branch: all 6 tests red. Comm actor registration moved above the singletons so registry.Get works; an earlier ResolveOne().GetAwaiter().GetResult() blocked inside a props factory."
|
|
},
|
|
{
|
|
"id": 8,
|
|
"subject": "Task 8: real two-ActorSystem ClusterClient boundary test + falsifiability control",
|
|
"status": "completed",
|
|
"classification": "high-risk",
|
|
"blockedBy": [
|
|
7
|
|
],
|
|
"parallelizableWith": [
|
|
9
|
|
],
|
|
"commit": "5468b79d",
|
|
"note": "Not flaky (3/3 clean repeats; 4 tests, ~16s). Both legs sabotage-verified against production code, and the control was itself falsified by restoring the receptionist. The control necessarily removes RegisterService too -- registering would materialise the extension it is removing. Task 6's dropped per-node/singleton property does NOT land here (one node per side cannot discriminate); it is live-gate step 8, and the MeshCommActorPathTests comment was corrected to say so."
|
|
},
|
|
{
|
|
"id": 9,
|
|
"subject": "Task 9: docker-dev rig config + docs + design-doc correction",
|
|
"status": "completed",
|
|
"commit": "e4cb9a08",
|
|
"note": "Rig Mode is ${OTOPCUA_MESH_MODE:-Dps} rather than a hardcoded Dps, so Task 10 can flip all six nodes at `docker compose up` with no compose edit and no rebuild. Verified via `docker compose config` (6 services resolved).",
|
|
"classification": "small",
|
|
"blockedBy": [
|
|
7
|
|
],
|
|
"parallelizableWith": [
|
|
8
|
|
]
|
|
},
|
|
{
|
|
"id": 10,
|
|
"subject": "Task 10: live gate on docker-dev (11 steps; step 4 = deploy-seal semantics, step 8 = per-node/contact-rotation discriminator [replaces the dropped Task 6 assertion \u2014 do not skip], step 9 = both-centrals drop behavior)",
|
|
"status": "completed",
|
|
"note": "PASSED for the transport (steps 1-5, 8, 11 green; flip end-to-end via OTOPCUA_MESH_MODE=ClusterClient). Record: 2026-07-22-mesh-phase2-live-gate.md. Steps 6/7 NOT RUN (no UI surface for driver-control \u2014 finding D, DriverStatusPanel is orphaned; no alarms configured). Step 8's stated log-signal does not exist (HandleApplyAck is silent on success \u2014 finding E); proven instead by both centrals independently logging per-instance ClusterClient creation + rebuild, which a singleton cannot. FOUR findings: A) 10-30s post-restart delivery window is PRE-EXISTING (DPS control failed identically); B) buffer-size=0 rationale OVERSTATED \u2014 a node applies a TimedOut deploy anyway via Bootstrap() orphan-row replay at +44s, corrected in 3 docs; C) stopping BOTH centrals strands non-seed nodes at the membership layer (Phase 7 drill, transport-independent); D) driver Restart/Reconnect UI orphaned since v3 Batch 2. Rig restored to Dps default.",
|
|
"classification": "high-risk",
|
|
"blockedBy": [
|
|
8,
|
|
9
|
|
]
|
|
}
|
|
],
|
|
"lastUpdated": "2026-07-22T00:00:00Z"
|
|
} |