D1: the assigner, builder and indexer call every node's manifest-reads again at their consumer's bind (issue 1966, ADR 0668 §4)

Note 155: a manifest-reads-changed said before the assigner's consumer existed was never heard, and
ace's bootstrapped statement stood. Each part registers the engine-reads keeper's AtBind on its hub
and calls the verb as a kept call (CallKeptOn). The asks-at-bind half (Q7) is not in this commit.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-10 02:59:53 +01:00
parent 1fad9c8855
commit 416e270899
8 changed files with 49 additions and 5 deletions
@@ -263,7 +263,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
watch.put = gate.Toolchain
// What each counted node's engine reads (ADR 0618 §2, ADR 0624 §1): asked through the node-engine seat's verb as the
// builder's own account, for every node the assignments names, and kept in the state engine-reads once it is bound.
reads := newEngineReads(calls.CallOn, gate, logger.Printf)
reads := newEngineReads(calls.CallKeptOn, gate, logger.Printf)
// The old reference form's window over them (DLV-B9), kept in the state old-form once it is bound; builder_old_form
// reads it and drops a retired node by hand.
reads.ctx = context.WithoutCancel(ctx)
@@ -351,6 +351,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
if err := hub.Handle("mesh.seat.*.event.>", events); err != nil {
return err
}
hub.OnBound(reads.atBind) // what a node's engine said before the consumer was bound (ADR 0668 §4)
if stopped != nil {
return stopped
}
@@ -4,6 +4,7 @@ import (
"context"
"slices"
"sync"
"time"
"github.com/nats-io/nats.go/jetstream"
@@ -35,7 +36,7 @@ type engineReads struct {
ctx context.Context
}
// newEngineReads asks through call (process.Conn.CallOn) and keeps each answer in the state once it is bound.
// newEngineReads asks through call (process.Conn.CallKeptOn, a kept call; ADR 0668) and keeps each answer in the state once it is bound.
func newEngineReads(call enginereads.Caller, gate *buildrun.ToolchainGate, logf func(string, ...any)) *engineReads {
r := &engineReads{gate: gate, keeper: &enginereads.Keeper{Call: call, Logf: logf}}
r.keeper.Changed = r.stepWindow
@@ -107,6 +108,16 @@ func (r *engineReads) placed() {
r.stepWindow()
}
// atBind asks every counted node again when the builder's consumer is bound (the hub's OnBound): a manifest-reads-changed
// said before the consumer existed is not delivered to it, so the statement it would have ended is ended here (novox/hq
// ADR 0668 §4; issue 1966 note 155).
func (r *engineReads) atBind(created time.Time) {
r.mu.Lock()
nodes := slices.Clone(r.counted)
r.mu.Unlock()
r.keeper.AtBind(func() []string { return nodes })(created)
}
// heard takes an event of the builder's consumer on EVENTS: manifest-reads-changed asks its node again, and answers true.
func (r *engineReads) heard(m jetstream.Msg, logf func(string, ...any)) bool {
node, ours, err := enginereads.NodeOf(m.Subject(), m.Data())
@@ -414,6 +414,18 @@ func aFailedTrunkBuildIsAFinding(t *testing.T) {
said: func(b versions.OldFormClosedBody) { closings = append(closings, b) },
log: func(e builds.BuildLogEntry) { logged = append(logged, e) }}
reads := &engineReads{keeper: &enginereads.Keeper{}, window: window, counted: []string{"a", "b"}}
// Issue 1966 note 155, ADR 0668 §4: at the consumer's bind every counted node is called once again.
var boundCalls []string
reads.keeper.Sync = true
reads.keeper.Call = func(node, _ string, _ any) (json.RawMessage, error) {
boundCalls = append(boundCalls, node)
return nil, errors.New("not served")
}
reads.atBind(now)
if !slices.Equal(boundCalls, []string{"a", "b"}) {
t.Fatalf("at the bind called %v, want every counted node once", boundCalls)
}
reads.keeper = &enginereads.Keeper{}
newest := []string{forms.FormNeed("oci")}
counted := map[string][]string{"a": newest, "b": nil}
window.step(context.Background(), []string{"a", "b"}, counted)