From 7235e4fb921d69708186c821d22b8367e0806505 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 31 Oct 2026 01:09:13 +0100 Subject: [PATCH] Keep a record ask the node's have waits on, and put no value a bucket already holds, so a node that composes its own is asked once and hears no want that did not change (issue 774) On novox the asks of postgres, keycloak and mongodb were judged stale against the controller's compositions, taken away and made again under a new id every pass (8 s), so no answer could count; and every put is a change the node composes again on. Quick fix: the stale check keeps a record ask whose digest the node's have waits on; the pass skips a put whose value the bucket holds. Thorough fix beside it (not here, another branch owns the file): the node-engine's composer composes again only when a want's value changed. --- .../cmd/mesh-delivery-assigner/pass.go | 14 ++++++++++ .../cmd/mesh-delivery-assigner/pass_test.go | 27 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go index 7d7f8c8..2f81833 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "encoding/json" "errors" @@ -566,6 +567,11 @@ func (p *passer) put(ctx context.Context, bucket, key string, encode func() ([]b p.logf("%s %s is not written: %v", bucket, key, err) return } + // A value the bucket already holds is not put again: every put is a change its watchers hear, and a want put unchanged + // made its node compose again (issue 774, 2026-10-31: novox composed every 8 s, two asks of the vault each time). + if held := p.w.value(bucket, key); held != nil && bytes.Equal(held, value) { + return + } if _, err := p.kvs[bucket].Put(ctx, key, value); err != nil && !errors.Is(err, context.Canceled) { p.logf("%s %s could not be written: %v", bucket, key, err) return @@ -645,6 +651,14 @@ func (p *passer) staleAsk(key string, trunk map[string]delivery.Module) bool { if !on { return true } + // A node that composes its own asks about its own composition, which its have names as the one it waits on (yeses): that + // ask stands while the have waits on it, whatever the controller composes (issue 774, 2026-10-31: novox's asks of + // postgres, keycloak and mongodb were taken away and made again under a new id every pass, so no answer could count). + if hkey, err := delivery.HaveKey(k.Node, k.Module); err == nil { + if h, err := delivery.DecodeHave(p.w.value(delivery.HaveBucket, hkey)); err == nil && h.Phase == delivery.PhaseWaitingYes && h.Waits == k.Composed { + return false + } + } // The assigner asks about a composition at more than the trunk fingerprint: a node held back by the rules is asked about the // revised composition of the fingerprint it runs (ADR 0545), and a node's first want at the older fingerprint it runs. So a // record ask is stale only when its digest is the composition of none of the fingerprints the node can still want (the diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go index 0cba96b..38f40b9 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "encoding/json" "errors" @@ -26,10 +27,12 @@ import ( type memKV struct { jetstream.KeyValue values map[string][]byte + puts int // how many puts reached the bucket } func (m *memKV) Put(_ context.Context, key string, value []byte) (uint64, error) { m.values[key] = value + m.puts++ return uint64(len(m.values)), nil } @@ -952,6 +955,30 @@ func aPlacedNodesWantsNameTheBuildAloneAndItsYesComesFromItsHave(t *testing.T) { if _, there := asksKV.values[akey]; !there { t.Fatal("a have waiting for a yes raised no record ask") } + // Issue 774, 2026-10-31: while the controller still composes the placed node's module (its lease not handed away), its + // digests are not the one the node's have waits on; the ask stays, and a pass that changes nothing puts nothing: on novox + // the asks of postgres, keycloak and mongodb were taken away and made again every 8 s, and a want put unchanged each + // pass made the node compose again each time. + for _, fp := range []string{fpTest, fpOld} { + ck, _ := delivery.ComposedKey("g14", "web", fp) + cv, _ := delivery.EncodeComposedDigest(delivery.ComposedDigest{Digest: "sha256:" + strings.Repeat("c", 64), Judgement: delivery.JudgedUnchanged}) + put(delivery.ControllerComposedBucket, ck, cv) + } + asked := asksKV.values[akey] + p.wants(context.Background()) + if !bytes.Equal(asksKV.values[akey], asked) { + t.Fatalf("the record ask the node's have waits on was taken away or made again: %q", asksKV.values[akey]) + } + wantPuts, askPuts := wantsKV.puts, asksKV.puts + p.wants(context.Background()) + if wantsKV.puts != wantPuts || asksKV.puts != askPuts { + t.Fatalf("a pass that changed nothing put %d wants and %d asks", wantsKV.puts-wantPuts, asksKV.puts-askPuts) + } + // Whichever rule asks it, a want put with the value the bucket holds reaches no bucket: each put is a change its node hears. + p.put(context.Background(), delivery.AssignerWantsBucket, wkey, func() ([]byte, error) { return wantsKV.values[wkey], nil }) + if wantsKV.puts != wantPuts { + t.Fatalf("a want put unchanged reached the bucket") + } a, err := delivery.DecodeAssignerAsk(akey, asksKV.values[akey]) if err != nil { t.Fatal(err)