A follower joining a watched state hears its current values, and a templates archive is fetched once

A read could start a state's watch before its follower joined, and a change landing in that gap woke nothing (review
of ADR 0580 §2): a joining follower now runs over the copy's current values before any later change. The watched
reader keeps each archive once per URL, and both caches are bounded.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-01 03:51:53 +01:00
parent 8311ecb849
commit c77d17fb42
4 changed files with 87 additions and 6 deletions
@@ -157,6 +157,16 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
byKey[frombus.PlacementRead+" watched"] != 1 {
t.Errorf("the watched reader read %v, want a build by key each time, a manifest once, the placement watched once", byKey)
}
fetched := 0
keep := fromruntime.KeptFetch(func(context.Context, string) ([]byte, error) { fetched++; return []byte("tar"), nil })
for range 2 {
if body, err := keep(context.Background(), "http://store/restic/files/blobs/sha256:a"); err != nil || string(body) != "tar" {
t.Fatalf("the kept fetch answered %q, %v", body, err)
}
}
if fetched != 1 {
t.Errorf("a templates archive was fetched %d times, want once", fetched)
}
}
func planned[P any](plans map[string]P, module string) bool { _, ok := plans[module]; return ok }
+33 -2
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"sync"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/compose/frombus"
@@ -47,9 +48,39 @@ func (w Watched) Get(state, key string) ([]byte, bool, error) {
return w.WatchedStates.Get(state, key)
}
// WatchedReader is frombus's Reader over w, fetching archives over HTTP.
// WatchedReader is frombus's Reader over w, fetching each templates archive over HTTP once (KeptFetch): a composition
// made again on every change reads it again otherwise.
func WatchedReader(w *stdio.WatchedStates) frombus.Reader {
return frombus.StatesReader(Watched{w}, HTTPFetch)
return frombus.StatesReader(Watched{w}, KeptFetch(HTTPFetch))
}
// archivesAtMost bounds the archives KeptFetch keeps; past it they are emptied, which costs only a fetch again.
const archivesAtMost = 64
// KeptFetch is fetch kept once per URL: an archive's URL names its blob by digest, so its body never changes under it
// (frombus checks the digest on every read). A failed fetch is not kept.
func KeptFetch(fetch frombus.Fetch) frombus.Fetch {
var mu sync.Mutex
kept := map[string][]byte{}
return func(ctx context.Context, url string) ([]byte, error) {
mu.Lock()
body, ok := kept[url]
mu.Unlock()
if ok {
return body, nil
}
body, err := fetch(ctx, url)
if err != nil {
return nil, err
}
mu.Lock()
if len(kept) >= archivesAtMost {
clear(kept)
}
kept[url] = body
mu.Unlock()
return body, nil
}
}
// HTTPFetch reads an archive over HTTP, at most 256 MiB.