From 0a8a592ef40b78144942a147255ef2a593cd7827 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 26 Sep 2026 23:32:30 +0200 Subject: [PATCH] An empty declaration is sent, so a node drops what it last held (hq 127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit push skipped any node whose declaration composed to zero resources. A node that HELD something before — the broker opening a placement gave an adopted node, say — then kept it forever: the empty declaration that would drop it was never sent, and the node's own heartbeat re-applied the stale resource with no way for the mesh to say it is gone. Now the empty declaration is sent; the host drops what the mesh owned and keeps what it found. A node that never held anything applies it as a no-op. Surfaced on ace: the foundation-opening fix (#74) removed its only resource, and the correction could not reach it until this. --- cmd/mesh-controller/push.go | 10 ++++++++-- cmd/mesh-controller/push_test.go | 10 ++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/cmd/mesh-controller/push.go b/cmd/mesh-controller/push.go index 2319213..74ec025 100644 --- a/cmd/mesh-controller/push.go +++ b/cmd/mesh-controller/push.go @@ -502,8 +502,14 @@ func composeEach(names []string, continue } if len(declared.Resources) == 0 { - fmt.Printf("%s is assigned nothing — skipped\n", name) - continue + // Sent, not skipped (novox/hq issue 127). A node whose declaration composes to + // nothing may have HELD something before — the broker opening a placement gave it, + // say — and skipping the empty declaration leaves that last resource in force + // forever, re-applied by the node's own heartbeat, with no way for the mesh to say + // it is gone. An empty declaration is the correction: the host drops what the mesh + // owned and keeps what it found (the adoption envelope still rides along). A node + // that never held anything applies it as the no-op it is. + fmt.Printf("%s owns nothing now — sent so it drops what it last held\n", name) } sending = append(sending, readyNode{name, declared}) } diff --git a/cmd/mesh-controller/push_test.go b/cmd/mesh-controller/push_test.go index 3e6dc9d..aeaabfa 100644 --- a/cmd/mesh-controller/push_test.go +++ b/cmd/mesh-controller/push_test.go @@ -39,12 +39,14 @@ func TestOneUnresolvableNodeStillLetsTheRestBeSent(t *testing.T) { } } -// And a machine assigned nothing is neither sent nor a refusal — it is nothing to say. -func TestAMachineAssignedNothingIsNotARefusal(t *testing.T) { +// A machine whose declaration composes to nothing is SENT the empty declaration, not skipped +// (novox/hq issue 127): it may have held something before, and only sending the empty +// declaration tells it to drop what the mesh owned. It is never a refusal. +func TestAnEmptyDeclarationIsSentSoTheNodeDropsWhatItHeld(t *testing.T) { sending, refusals := composeEach([]string{"spare"}, func(string) (sendable, error) { return sendable{}, nil }) - if len(sending) != 0 || len(refusals) != 0 { - t.Errorf("a machine assigned nothing was treated as something: %v / %v", sending, refusals) + if len(sending) != 1 || len(refusals) != 0 { + t.Errorf("an empty declaration must be sent, not skipped or refused: %v / %v", sending, refusals) } } -- 2.54.0