carlos / platform Public

moves pass: the page and log outboxes flush only with the switch on (final review I1)

With CARLOS_MOVES_PHASE1 off, a pass no longer sends kept pages, writes kept
log entries, or retires delivered page tombstones. What the outboxes hold
waits on disk, untouched, until the switch is on again. With it on, the
flush still runs first, before any record or fleet read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 3eeb1ed942a7de624af959359ae548b646edfb28 parent 651efdf
2 files changed, +103 −9
  • internal/host/movespaging_test.go +87 −0
  • internal/host/movespass.go +16 −9
diff --git a/internal/host/movespaging_test.go b/internal/host/movespaging_test.go
index fe3b8a7d4..f647e9769 100644
--- a/internal/host/movespaging_test.go
+++ b/internal/host/movespaging_test.go
@@ -671,3 +671,90 @@ func TestPagingIsRateLimited(t *testing.T) {
}
})
}
+
+// outboxTree lists every file under dataDir's .moves directory, with its
+// bytes, so a test can tell whether a pass changed anything there.
+func outboxTree(t *testing.T, dataDir string) map[string]string {
+ t.Helper()
+ out := map[string]string{}
+ root := filepath.Join(dataDir, ".moves")
+ err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
+ if errors.Is(err, fs.ErrNotExist) && p == root {
+ return fs.SkipAll
+ }
+ if err != nil || d.IsDir() {
+ return err
+ }
+ b, err := os.ReadFile(p)
+ if err != nil {
+ return err
+ }
+ rel, _ := filepath.Rel(root, p)
+ out[filepath.ToSlash(rel)] = string(b)
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ return out
+}
+
+// Final review I1: with the switch off, a pass leaves both outboxes alone.
+// The pending page, the delivered page's tombstone and the kept log entry
+// stay on disk untouched, and nothing is sent or written, until the switch
+// is on again; then the same pass delivers them.
+func TestSwitchOffPassLeavesTheOutboxes(t *testing.T) {
+ ctx := context.Background()
+ dd := t.TempDir()
+ now := time.Date(2026, 10, 11, 9, 0, 0, 0, time.UTC)
+ var pages pagesSeen
+ refuse := true
+ var logged []moves.LogEvent
+ logFails := true
+ w := &movesWorker{cfg: moveCfg{DataDir: dd, Box: "ie-box", Enabled: true, Now: func() time.Time { return now }, Page: refusingPager(&pages, &refuse),
+ LogEvent: func(_ context.Context, _ release.Store, _ provision.Record, ev moves.LogEvent) error {
+ if logFails {
+ return errors.New("the bucket is down")
+ }
+ logged = append(logged, ev)
+ return nil
+ }}, mirrors: map[string]lease.MirrorStore{}}
+ rec := provision.Record{Account: "bab", App: "shop", Host: p1Host, Move: &provision.MoveState{ID: p1ID, State: provision.MoveStuck, From: "ie", To: "uk"}}
+ // A page the pager refused: pending on disk.
+ w.pageStuck(ctx, rec, "source", provision.StuckBaselineMismatch)
+ // A delivered page of a move no record carries any more: a tombstone a
+ // pass with the switch on would retire.
+ refuse = false
+ other := provision.Record{Host: p1Host, Move: &provision.MoveState{ID: "mv_zyxwvutsrqponmlkjihgfedcba"}}
+ w.pageRolledBack(ctx, other)
+ pages = nil
+ // A log entry whose write failed: kept in the log outbox.
+ w.observeMove(ctx, nil, provision.Record{Account: "bab", App: "shop", Host: p1Host, Region: "ie",
+ Move: &provision.MoveState{ID: p1ID, State: provision.MovePreparing, From: "ie", To: "uk"}})
+ before := outboxTree(t, dd)
+ if len(before) != 3 {
+ t.Fatalf("the fixture's outboxes %v, want a pending page, a tombstone and a kept log entry", before)
+ }
+ logFails = false
+ now = now.Add(time.Hour) // every backoff is over
+
+ s := release.NewDirStore(t.TempDir())
+ w.cfg.Enabled = false
+ if err := movesPass(ctx, s, w); err != nil {
+ t.Fatal(err)
+ }
+ if len(pages) != 0 || len(logged) != 0 {
+ t.Fatalf("a pass with the switch off sent pages %+v and wrote log entries %+v", pages, logged)
+ }
+ if after := outboxTree(t, dd); fmt.Sprint(after) != fmt.Sprint(before) {
+ t.Fatalf("a pass with the switch off changed the outboxes:\nbefore %v\nafter %v", before, after)
+ }
+
+ w.cfg.Enabled = true
+ if err := movesPass(ctx, s, w); err != nil {
+ t.Fatal(err)
+ }
+ if n, _ := pagesWith(pages, "carlos move stuck: "); n != 1 || len(logged) != 1 || logged[0].Identity != "requested" {
+ t.Fatalf("with the switch on again: pages %+v, log entries %+v", pages, logged)
+ }
+}
diff --git a/internal/host/movespass.go b/internal/host/movespass.go
index 5dca41750..678ab1a6f 100644
--- a/internal/host/movespass.go
+++ b/internal/host/movespass.go
@@ -735,12 +735,16 @@ func movesPass(ctx context.Context, s release.Store, w *movesWorker) error {
if cfg.claimRecords {
ctx = provision.WithRecordSequence(ctx)
}
- // The pages the outbox and the index keep undelivered are sent again
- // first, before any record or fleet read (movespaging.go, fix round 3).
- w.flushPageOutbox(ctx)
- // The move log's kept entries are retried next, whatever their moves
- // now are (moveslogoutbox.go).
- w.flushLogOutbox(ctx, s)
+ if cfg.Enabled {
+ // The pages the outbox and the index keep undelivered are sent again
+ // first, before any record or fleet read (movespaging.go, fix round
+ // 3). The move log's kept entries are retried next, whatever their
+ // moves now are (moveslogoutbox.go). With the switch off neither
+ // outbox is touched: what they keep waits on disk until it is on
+ // again (final review I1).
+ w.flushPageOutbox(ctx)
+ w.flushLogOutbox(ctx, s)
+ }
recs, err := provision.ListAllRecords(ctx, s)
if err != nil {
return fmt.Errorf("moves: list records: %w", err)
@@ -885,9 +889,12 @@ func movesPass(ctx context.Context, s release.Store, w *movesWorker) error {
// no longer routes here (movesfinal.go, fix round 1).
w.sweepFinalJournals(recs)
}
- // Delivered pages of moves retired on this box are removed, now that
- // every record is listed (movespaging.go).
- w.retirePageTombstones(recs)
+ if cfg.Enabled {
+ // Delivered pages of moves retired on this box are removed, now that
+ // every record is listed (movespaging.go). With the switch off the
+ // outbox is left as it is (final review I1).
+ w.retirePageTombstones(recs)
+ }
w.liftFences(recs)
return nil
}