Files
lmxopcua/docs/plans/2026-07-21-per-cluster-mesh-phase1.md.tasks.json
T
Joseph Doherty 90c1302f71 feat(fleet): reconcile ClusterNode dial targets against live membership
ClusterNode.AkkaPort and the node's own Cluster:Port are the same fact in two
places and nothing made them agree. Phase 2 dials the row instead of gossiping,
so a node binding 4054 while its row says 4053 becomes unreachable from central
— and the symptom is a silent absence of acks rather than an error. That is the
shape of the Modbus/ModbusTcp and TwinCat/Focas drifts already in this repo.
Since Phase 1 also made the rows the deploy path's expected-ack set, drift
already costs a failed deployment today.

Implemented as the plan's preferred option: an admin-role singleton comparing
rows against the membership an admin node can already see, rather than each
driver node asserting its own row — Phase 4 removes the driver nodes' ConfigDb
connection, so a self-assertion written there would have to be deleted again.

Three shapes, split by severity: a row whose dial target disagrees with its own
NodeId and a running node with no row are Errors; an enabled row with no
matching member is a Warning, because a node down for maintenance is a
legitimate state. Findings are logged only when the set changes — a check that
reprints the same warning every sweep trains operators to filter it out.

Documented limitation: Phase 2 must revisit this. Once the fleet splits into
one mesh per cluster an admin node cannot see site members, and every site row
would report EnabledRowNotInCluster forever.

Sabotage: removing the change-detection guard turns the repeat test red.
ControlPlane.Tests 100/100.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-22 08:36:44 -04:00

19 lines
1.2 KiB
JSON

{
"plan": "docs/plans/2026-07-21-per-cluster-mesh-phase1.md",
"branch": "feat/mesh-phase1",
"decisions": {
"seal-semantics": "DB-derived + Enabled hatch (confirmed 2026-07-22)",
"cluster-scope": "Dropped — no cluster-scoped deployment exists; expected set = all enabled ClusterNode rows",
"node-roles": "Every ClusterNode row is a driver node; documented + pinned by test, no Roles column",
"adminui-edit": "Deferred to Phase 2 (nothing reads the columns until then)"
},
"tasks": [
{ "id": 1, "subject": "Address columns on ClusterNode", "status": "completed", "commit": "2bbb0271", "blockedBy": [] },
{ "id": 2, "subject": "EF migration AddClusterNodeTransportPorts", "status": "completed", "commit": "da74ebd6", "blockedBy": [1] },
{ "id": 3, "subject": "Coordinator sources expected-ack set from ClusterNode", "status": "completed", "commit": "d88e2455", "blockedBy": [2] },
{ "id": 4, "subject": "Reconcile duplicated AkkaPort with the node's own Cluster:Port", "status": "pending", "blockedBy": [2] },
{ "id": 5, "subject": "Rig seed + docs", "status": "pending", "blockedBy": [3, 4] },
{ "id": 6, "subject": "Live gate on docker-dev", "status": "pending", "blockedBy": [5] }
]
}