carlos / platform Public

host: the system leg backs off, reuses a verified cache, logs once, prunes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QCUCpfxbCyEAVQ2m51dH5
Paul Campbell pushed by paul@keymail.dev 21bc5d18ef36a0657613e4058dc5a63a981790fd parent 1b81a2d
2 files changed, +230 −21
  • internal/host/systemleg.go +118 −20
  • internal/host/systemleg_test.go +112 −1
diff --git a/internal/host/systemleg.go b/internal/host/systemleg.go
index 8e0c70c90..ec4785a3b 100644
--- a/internal/host/systemleg.go
+++ b/internal/host/systemleg.go
@@ -3,7 +3,10 @@ package host
import (
"context"
"crypto/ed25519"
+ "crypto/sha256"
+ "encoding/hex"
"errors"
+ "io"
"log"
"math/rand/v2"
"os"
@@ -30,12 +33,26 @@ type systemLeg struct {
now func() time.Time
jitter func(time.Duration) time.Duration
+ // fetchTo is release.FetchToFile bound to this leg's store and limit,
+ // behind a seam so a test can count downloads and make one fail.
+ fetchTo func(ctx context.Context, key, dst, wantSHA256 string) error
+ logf func(format string, args ...any)
- pending *systemPending
+ pending *systemPending
+ // backoff is ONE retry ladder for everything that can go wrong after the
+ // deadline — an unreachable store on the fetch, an unwritable /data on
+ // the request, and root's own "deferred" — because they are all the same
+ // failure from this loop's point of view: the work could not be done, and
+ // hammering it every 2 s is what must not happen. Base 2 s doubling to
+ // 5 min, cleared when the pointer moves or root answers.
+ backoff time.Duration
+ backoffUntil time.Time
lastTouch time.Time
lastObserve time.Time
- backoffUntil time.Time
- backoff time.Duration
+ // loggedNoArtifact is the version whose "not for this box" line has
+ // already been said. Without it the hourly re-observe would repeat it
+ // forever for a fleet that mixes architectures.
+ loggedNoArtifact string
observeHook func() // test seam
}
@@ -60,7 +77,19 @@ func newSystemLeg(store release.Store, pub ed25519.PublicKey, getenv func(string
dataDir: dataDir, optDir: optDir,
now: time.Now,
jitter: func(spread time.Duration) time.Duration { return rand.N(spread + 1) },
+ fetchTo: func(ctx context.Context, key, dst, wantSHA256 string) error {
+ return release.FetchToFile(ctx, store, key, dst, systemArtifactLimit, wantSHA256)
+ },
+ logf: log.Printf,
+ }
+}
+
+func (l *systemLeg) printf(format string, args ...any) {
+ if l.logf != nil {
+ l.logf(format, args...)
+ return
}
+ log.Printf(format, args...)
}
// observe reads the pointer and decides. It never blocks on a download:
@@ -75,7 +104,7 @@ func (l *systemLeg) observe(ctx context.Context) {
p, _, err := release.GetSystemPointer(ctx, l.store, l.pub, release.SystemComponentRouter, l.channel)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
- log.Printf("agent: system pointer %s: %v", l.channel, err)
+ l.printf("agent: system pointer %s: %v", l.channel, err)
}
return
}
@@ -95,14 +124,19 @@ func (l *systemLeg) observe(ctx context.Context) {
}
art, ok := p.Current.Artifacts[l.platform]
if !ok && !noop {
- if l.pending == nil || l.pending.version != p.Current.Version {
- log.Printf("agent: system pointer %s names %s with no artifact for %s — not for this box", l.channel, p.Current.Version, l.platform)
+ if l.loggedNoArtifact != p.Current.Version {
+ l.printf("agent: system pointer %s names %s with no artifact for %s — not for this box", l.channel, p.Current.Version, l.platform)
+ l.loggedNoArtifact = p.Current.Version
}
return
}
if l.pending != nil && l.pending.version == p.Current.Version && l.pending.promotedAt.Equal(p.PromotedAt) {
return // already pending; keep its deadline
}
+ // A different pointer is new work, not a continuation of the old one:
+ // its retry ladder starts at the base (same rule as noteRestartFailed's
+ // "a DIFFERENT version starts the hold fresh").
+ l.backoff, l.backoffUntil = 0, time.Time{}
l.pending = &systemPending{version: p.Current.Version, promotedAt: p.PromotedAt, noop: noop, artifact: art,
deadline: now.Add(l.jitter(p.SpreadDuration()))}
if noop {
@@ -110,6 +144,7 @@ func (l *systemLeg) observe(ctx context.Context) {
// record the watermark, so it may as well hear about it now.
l.pending.deadline = now
}
+ l.pruneCache(p.Current.Version)
}
// tick fires a pending request whose deadline has passed and re-touches an
@@ -139,12 +174,7 @@ func (l *systemLeg) tick(ctx context.Context, now time.Time) {
return
case res.Outcome == systemOutcomeDeferred && res.At.After(p.deadline):
// root could not reach the store; try again with backoff
- if l.backoff == 0 {
- l.backoff = fastBackoffBase
- } else if l.backoff < fastBackoffCap {
- l.backoff *= 2
- }
- p.deadline = now.Add(l.backoff)
+ p.deadline = now.Add(l.nextBackoff())
p.requested = false
}
}
@@ -154,33 +184,77 @@ func (l *systemLeg) tick(ctx context.Context, now time.Time) {
if !p.requested {
if !p.noop {
if err := l.fetch(ctx, p); err != nil {
- log.Printf("agent: system update %s: %v", p.version, err)
+ l.printf("agent: system update %s: %v", p.version, err)
os.Remove(systemCachePath(l.dataDir, p.version))
- l.backoffUntil = now.Add(fastBackoffBase)
+ l.holdOff(now)
return
}
}
if err := l.writeRequest(p, now); err != nil {
- log.Printf("agent: system update %s: request: %v", p.version, err)
+ // Same ladder as the fetch: without it the next tick would
+ // re-run the whole download for a /data this process cannot
+ // write, every 2 seconds.
+ l.printf("agent: system update %s: request: %v", p.version, err)
+ l.holdOff(now)
return
}
p.requested = true
}
if now.Sub(l.lastTouch) >= systemRequestEvery {
if err := os.WriteFile(filepath.Join(l.dataDir, systemRequestMarker), []byte(now.UTC().Format(time.RFC3339Nano)+"\n"), 0o644); err != nil {
- log.Printf("agent: system update %s: cannot ask for a root apply: %v", p.version, err)
+ l.printf("agent: system update %s: cannot ask for a root apply: %v", p.version, err)
return
}
l.lastTouch = now
}
}
+// nextBackoff advances the retry ladder one rung: base 2 s, doubling, capped
+// at 5 min — the clamp shape fastDeps.noteUnstaged uses, so a step can never
+// overshoot the cap.
+func (l *systemLeg) nextBackoff() time.Duration {
+ if l.backoff == 0 {
+ l.backoff = fastBackoffBase
+ } else if l.backoff *= 2; l.backoff > fastBackoffCap {
+ l.backoff = fastBackoffCap
+ }
+ return l.backoff
+}
+
+// holdOff pushes the next attempt out by the ladder's next rung. What it
+// bounds is a 96 MB download: a permanently unfetchable artifact must cost
+// this box four attempts an hour, not one every two seconds.
+func (l *systemLeg) holdOff(now time.Time) { l.backoffUntil = now.Add(l.nextBackoff()) }
+
func (l *systemLeg) fetch(ctx context.Context, p *systemPending) error {
dst := systemCachePath(l.dataDir, p.version)
+ if systemCacheHolds(dst, p.artifact.SHA256) {
+ return nil // already downloaded and verified — a deferred retry or a
+ // re-promote of the same version must not pull it again
+ }
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
- return release.FetchToFile(ctx, l.store, p.artifact.Key, dst, systemArtifactLimit, p.artifact.SHA256)
+ return l.fetchTo(ctx, p.artifact.Key, dst, p.artifact.SHA256)
+}
+
+// systemCacheHolds reports whether the cache file at path IS the artifact
+// the pointer signed. It hashes the file: presence proves nothing, and a
+// truncated or swapped file is exactly what the signed sha is there for.
+func systemCacheHolds(path, wantSHA256 string) bool {
+ if wantSHA256 == "" {
+ return false
+ }
+ f, err := os.Open(path)
+ if err != nil {
+ return false
+ }
+ defer f.Close()
+ h := sha256.New()
+ if _, err := io.Copy(h, io.LimitReader(f, systemArtifactLimit+1)); err != nil {
+ return false
+ }
+ return hex.EncodeToString(h.Sum(nil)) == wantSHA256
}
func (l *systemLeg) writeRequest(p *systemPending, now time.Time) error {
@@ -190,11 +264,35 @@ func (l *systemLeg) writeRequest(p *systemPending, now time.Time) error {
return writeJSONDurable(systemRequestPath(l.dataDir), systemRequest{Version: p.version, PromotedAt: p.promotedAt, NoOp: p.noop, SeenAt: now.UTC()}, 0o644)
}
+// pruneCache drops every cached version except keep. A promote that moves
+// v2 → v3 before root answered would otherwise leave v2's binary under /data
+// for good — nobody else deletes there. Only names validSystemVersion
+// accepts are removed, so requested.json and anything else under the
+// directory cannot be caught by it.
+func (l *systemLeg) pruneCache(keep string) {
+ dir := systemDataDir(l.dataDir)
+ ents, err := os.ReadDir(dir)
+ if err != nil {
+ return
+ }
+ for _, e := range ents {
+ if !e.IsDir() || e.Name() == keep || !validSystemVersion(e.Name()) {
+ continue
+ }
+ if err := os.RemoveAll(filepath.Join(dir, e.Name())); err != nil {
+ l.printf("agent: system update: cannot drop the cached %s: %v", e.Name(), err)
+ }
+ }
+}
+
// forget: root has answered with a terminal outcome. Drop the pending item
-// and the cache — root never deletes under /data, the agent does.
+// and the cache — root never deletes under /data, the agent does. Only the
+// pending for THIS version is dropped: a newer pointer may already be armed.
func (l *systemLeg) forget(version string) {
- l.pending = nil
- l.backoff = 0
+ if l.pending != nil && l.pending.version == version {
+ l.pending = nil
+ l.backoff, l.backoffUntil = 0, time.Time{}
+ }
if validSystemVersion(version) {
os.RemoveAll(filepath.Dir(systemCachePath(l.dataDir, version)))
}
diff --git a/internal/host/systemleg_test.go b/internal/host/systemleg_test.go
index a8bc4bb56..d7f12abe6 100644
--- a/internal/host/systemleg_test.go
+++ b/internal/host/systemleg_test.go
@@ -5,6 +5,7 @@ import (
"crypto/ed25519"
"crypto/sha256"
"encoding/hex"
+ "errors"
"os"
"path/filepath"
"strings"
@@ -209,7 +210,7 @@ func TestSystemLegIgnoresDevBuildEqualityAndOtherPlatforms(t *testing.T) {
}
}
-func TestSystemLegRefusesShaMismatchAndOversize(t *testing.T) {
+func TestSystemLegRefusesAnArtifactWhoseShaDoesNotMatch(t *testing.T) {
f := newLegFixture(t, "stable", "v1")
f.promote("v2", []byte("good"), 0)
// Swap the object under the signed sha.
@@ -236,6 +237,116 @@ func TestSystemLegIgnoresAPointerOlderThanTheWatermark(t *testing.T) {
}
}
+// countFetches wraps the leg's fetch seam with a counter, leaving the real
+// download in place, and returns a pointer to the count.
+func (f *legFixture) countFetches() *int {
+ n := 0
+ real := f.leg.fetchTo
+ f.leg.fetchTo = func(ctx context.Context, key, dst, sha string) error {
+ n++
+ return real(ctx, key, dst, sha)
+ }
+ return &n
+}
+
+func TestSystemLegBacksOffAGrowingWindowAfterAFailedFetch(t *testing.T) {
+ f := newLegFixture(t, "stable", "v1")
+ f.promote("v2", []byte("good"), 0)
+ fetches := 0
+ f.leg.fetchTo = func(ctx context.Context, key, dst, sha string) error {
+ fetches++
+ return errors.New("store unreachable")
+ }
+ ctx := context.Background()
+ f.leg.observe(ctx)
+ f.leg.tick(ctx, f.now) // first attempt: hold off 2s
+ if fetches != 1 {
+ t.Fatalf("first tick fetched %d times, want 1", fetches)
+ }
+ f.leg.tick(ctx, f.now.Add(time.Second))
+ if fetches != 1 {
+ t.Fatal("re-downloaded inside the first backoff window")
+ }
+ f.leg.tick(ctx, f.now.Add(3*time.Second)) // second attempt: hold off 4s
+ if fetches != 2 {
+ t.Fatalf("after the 2s window fetched %d times, want 2", fetches)
+ }
+ f.leg.tick(ctx, f.now.Add(5*time.Second))
+ if fetches != 2 {
+ t.Fatal("backoff did not grow: re-downloaded 2s after the second failure")
+ }
+ f.leg.tick(ctx, f.now.Add(8*time.Second)) // third attempt: hold off 8s
+ if fetches != 3 {
+ t.Fatalf("after the 4s window fetched %d times, want 3", fetches)
+ }
+ if f.leg.backoff != 8*time.Second {
+ t.Fatalf("ladder = %s after three failures, want 8s", f.leg.backoff)
+ }
+ if _, err := f.marker(); err == nil {
+ t.Fatal("asked root to apply an artifact that never downloaded")
+ }
+}
+
+func TestSystemLegReusesAVerifiedCacheOnARetry(t *testing.T) {
+ f := newLegFixture(t, "stable", "v1")
+ f.promote("v2", []byte("ELF v2"), 0)
+ fetches := f.countFetches()
+ ctx := context.Background()
+ f.leg.observe(ctx)
+ f.leg.tick(ctx, f.now)
+ if *fetches != 1 {
+ t.Fatalf("first tick fetched %d times, want 1", *fetches)
+ }
+ // Root defers: it could not reach the store to verify the pointer.
+ r := f.request()
+ os.MkdirAll(systemRootDir(f.opt), 0o755)
+ writeJSONDurable(filepath.Join(systemRootDir(f.opt), "result.json"),
+ systemResult{Channel: "stable", Version: "v2", PromotedAt: r.PromotedAt, Outcome: systemOutcomeDeferred,
+ Reason: "store", At: f.now.Add(500 * time.Millisecond)}, 0o644)
+ f.leg.tick(ctx, f.now.Add(time.Second)) // sees the deferral, re-deadlines
+ f.leg.tick(ctx, f.now.Add(4*time.Second)) // retries
+ if *fetches != 1 {
+ t.Fatalf("retry re-downloaded an artifact the cache already holds (%d fetches)", *fetches)
+ }
+ if got, _ := os.ReadFile(systemCachePath(f.dd, "v2")); string(got) != "ELF v2" {
+ t.Fatalf("cache = %q after the retry", got)
+ }
+}
+
+func TestSystemLegSaysNotForThisBoxOnce(t *testing.T) {
+ f := newLegFixture(t, "stable", "v1")
+ f.promote("v2", []byte("x"), 0)
+ f.leg.platform = "linux-amd64" // no artifact for me
+ lines := 0
+ f.leg.logf = func(format string, args ...any) { lines++ }
+ for i := 0; i < 3; i++ {
+ f.leg.observe(context.Background())
+ }
+ if lines != 1 {
+ t.Fatalf("three observes of the same pointer logged %d lines, want 1", lines)
+ }
+}
+
+func TestSystemLegPrunesTheCacheOfSupersededVersions(t *testing.T) {
+ f := newLegFixture(t, "stable", "v1")
+ f.promote("v2", []byte("two"), 0)
+ ctx := context.Background()
+ f.leg.observe(ctx)
+ f.leg.tick(ctx, f.now)
+ if _, err := os.Stat(systemCachePath(f.dd, "v2")); err != nil {
+ t.Fatalf("v2 not cached: %v", err)
+ }
+ // v3 lands before root ever answered for v2.
+ f.promote("v3", []byte("three"), 0)
+ f.leg.observe(ctx)
+ if _, err := os.Stat(systemCachePath(f.dd, "v2")); err == nil {
+ t.Fatal("v2's binary kept after v3 superseded it")
+ }
+ if f.request() == nil {
+ t.Fatal("the prune took the request with it")
+ }
+}
+
func TestFastTickRunsTheSystemLeg(t *testing.T) {
fx := newFastFixture(t)
d := fx.deps(nil)