carlos / platform Public

console: the box floor keeps seedfloor's guards: a label two reports claim refuses, a future-dated report is not fresh, and the watch's edges read is bounded

The status-report join by label refuses 409 "could not confirm box <label>.
Try again in a minute." (seedfloor.go's wording for an ambiguous label) when
two reports share the box's label. A report is fresh while it is at most ten
minutes old and dated no more than a minute ahead (offlineFresh's skew),
judged against the wall clock; a future-dated one answers the existing
"has not reported in 10 minutes". lifetimesFloorWatch reads the edges files
under instanceStoreTimeout, so a hung read cannot stall the cutover watch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 4b69f7b33f21a1087c72d241c1de9f199c7f9c04 parent 516293f
3 files changed, +58 −13
  • internal/console/lifetimes_cutover.go +1 −1
  • internal/console/lifetimes_floor.go +36 −6
  • internal/console/lifetimes_floor_test.go +21 −6
diff --git a/internal/console/lifetimes_cutover.go b/internal/console/lifetimes_cutover.go
index 8feb44de0..db4af0ac1 100644
--- a/internal/console/lifetimes_cutover.go
+++ b/internal/console/lifetimes_cutover.go
@@ -530,7 +530,7 @@ func (s *Server) handleAPILifetimesCutover(w http.ResponseWriter, r *http.Reques
// written; once it is, the door answers the status.
if st := g.current(); st == cutoverBefore || st == cutoverUnknown {
fctx, fcancel := context.WithTimeout(r.Context(), instanceStoreTimeout)
- ref := s.lifetimesBoxFloor(fctx, now)
+ ref := s.lifetimesBoxFloor(fctx)
fcancel()
if ref != nil {
jsonError(w, ref.Status, ref.Msg)
diff --git a/internal/console/lifetimes_floor.go b/internal/console/lifetimes_floor.go
index b1bed2ade..f59ac1dd4 100644
--- a/internal/console/lifetimes_floor.go
+++ b/internal/console/lifetimes_floor.go
@@ -49,6 +49,12 @@ func msgLifetimesBoxSilent(box string) string {
return fmt.Sprintf("box %s has not reported in 10 minutes", box)
}
+// msgLifetimesBoxAmbiguous is seedfloor.go's refusal for a label two
+// status reports claim, worded for one box.
+func msgLifetimesBoxAmbiguous(box string) string {
+ return fmt.Sprintf("could not confirm box %s. Try again in a minute.", box)
+}
+
func pageLifetimesRegressed(box string) string {
return fmt.Sprintf("box %s regressed below the lifetimes floor", box)
}
@@ -61,10 +67,15 @@ type lifetimesFloorState struct {
}
// lifetimesBoxFloor checks every edges file of the deployment and its box's
-// status report at now. nil is accepted; otherwise the refusal to answer.
-// The status report is joined by its label to the file's box, the join every
-// fleet reader makes (status reports carry no other box identity).
-func (s *Server) lifetimesBoxFloor(ctx context.Context, now time.Time) *doorRefusal {
+// status report against the wall clock. nil is accepted; otherwise the
+// refusal to answer. The status report is joined by its label to the file's
+// box, the join every fleet reader makes (status reports carry no other box
+// identity), with seedfloor.go's guards: a label two reports claim is
+// ambiguous and refused, and a report is fresh only while it is at most
+// lifetimesBoxFresh old and not dated more than a minute ahead (the skew
+// offlineFresh allows), so a future-dated report does not pass.
+func (s *Server) lifetimesBoxFloor(ctx context.Context) *doorRefusal {
+ now := time.Now()
snap, err := readEdgesSnapshot(ctx, s.cfg.Releases)
if err != nil || !snap.Complete {
log.Printf("console: lifetimes: the box floor: the edges files are not complete evidence (error %v)", err)
@@ -82,18 +93,34 @@ func (s *Server) lifetimesBoxFloor(ctx context.Context, now time.Time) *doorRefu
log.Printf("console: lifetimes: the box floor: the status reports are not complete evidence (complete %v, error %v)", complete, err)
}
reported := map[string]time.Time{}
+ dup := map[string]bool{}
for _, b := range boxes {
+ if _, seen := reported[b.Label]; seen {
+ dup[b.Label] = true
+ }
reported[b.Label] = b.GeneratedAt
}
for _, f := range files {
at, ok := reported[f.Box]
- if !ok || at.IsZero() || now.Sub(at) > lifetimesBoxFresh {
+ switch {
+ case ok && dup[f.Box]:
+ log.Printf("console: lifetimes: the box floor: box %q has more than one status report with its label", f.Box)
+ return &doorRefusal{http.StatusConflict, msgLifetimesBoxAmbiguous(f.Box)}
+ case !ok || !lifetimesBoxReportFresh(at, now):
return &doorRefusal{http.StatusConflict, msgLifetimesBoxSilent(f.Box)}
}
}
return nil
}
+// lifetimesBoxReportFresh reports whether a status report generated at is
+// at most lifetimesBoxFresh old at now and not dated more than a minute
+// ahead of it.
+func lifetimesBoxReportFresh(at, now time.Time) bool {
+ age := now.Sub(at)
+ return !at.IsZero() && age <= lifetimesBoxFresh && age >= -time.Minute
+}
+
// lifetimesFloorWatch is the floor's check after the cutover, from the
// cutover watch: at most every lifetimesFloorEvery it reads the edges files,
// and pages once per lifetimesFloorPageEvery per box whose file lacks the
@@ -106,7 +133,10 @@ func (s *Server) lifetimesFloorWatch(ctx context.Context, now time.Time) {
}
s.floorReadAt = now
s.floorMu.Unlock()
- snap, err := readEdgesSnapshot(ctx, s.cfg.Releases)
+ // Bounded, so a hung edges read cannot stall the cutover watch.
+ rctx, cancel := context.WithTimeout(ctx, instanceStoreTimeout)
+ snap, err := readEdgesSnapshot(rctx, s.cfg.Releases)
+ cancel()
if err != nil {
log.Printf("console: lifetimes: the box floor watch: %v", err)
return
diff --git a/internal/console/lifetimes_floor_test.go b/internal/console/lifetimes_floor_test.go
index a40acd1db..f6b6edaf9 100644
--- a/internal/console/lifetimes_floor_test.go
+++ b/internal/console/lifetimes_floor_test.go
@@ -29,9 +29,15 @@ func writeEdges(t *testing.T, st release.Store, box string, caps ...string) {
}
func writeBoxStatus(t *testing.T, st release.Store, label string, at time.Time) {
+ t.Helper()
+ writeBoxStatusAt(t, st, label, label, at)
+}
+
+// writeBoxStatusAt writes a status report for label under status/boxes/<key>.json.
+func writeBoxStatusAt(t *testing.T, st release.Store, key, label string, at time.Time) {
t.Helper()
body, _ := json.Marshal(map[string]any{"generated_at": at, "label": label, "routes": map[string]any{}, "replication": map[string]any{}})
- if _, err := st.Put(context.Background(), "status/boxes/"+label+".json", body, ""); err != nil {
+ if _, err := st.Put(context.Background(), "status/boxes/"+key+".json", body, ""); err != nil {
t.Fatal(err)
}
}
@@ -46,19 +52,28 @@ func TestCutoverRefusesBoxesBelowTheFloor(t *testing.T) {
{"a box without the capability", func(t *testing.T, l lifetimesFx) {
writeEdges(t, l.fx.rel, "annie", capLifetimes1)
writeEdges(t, l.fx.rel, "bert")
- writeBoxStatus(t, l.fx.rel, "annie", l.clock.Now())
- writeBoxStatus(t, l.fx.rel, "bert", l.clock.Now())
+ writeBoxStatus(t, l.fx.rel, "annie", time.Now())
+ writeBoxStatus(t, l.fx.rel, "bert", time.Now())
}, http.StatusConflict, "box bert does not publish lifetimes-1; every box must run this release before the cutover"},
{"a stale status report", func(t *testing.T, l lifetimesFx) {
writeEdges(t, l.fx.rel, "annie", capLifetimes1)
- writeBoxStatus(t, l.fx.rel, "annie", l.clock.Now().Add(-11*time.Minute))
+ writeBoxStatus(t, l.fx.rel, "annie", time.Now().Add(-11*time.Minute))
}, http.StatusConflict, "box annie has not reported in 10 minutes"},
+ {"a future-dated status report", func(t *testing.T, l lifetimesFx) {
+ writeEdges(t, l.fx.rel, "annie", capLifetimes1)
+ writeBoxStatus(t, l.fx.rel, "annie", time.Now().Add(11*time.Minute))
+ }, http.StatusConflict, "box annie has not reported in 10 minutes"},
+ {"two status reports with one label", func(t *testing.T, l lifetimesFx) {
+ writeEdges(t, l.fx.rel, "annie", capLifetimes1)
+ writeBoxStatus(t, l.fx.rel, "annie", time.Now())
+ writeBoxStatusAt(t, l.fx.rel, "annie-clone", "annie", time.Now())
+ }, http.StatusConflict, "could not confirm box annie. Try again in a minute."},
{"a missing status report", func(t *testing.T, l lifetimesFx) {
writeEdges(t, l.fx.rel, "annie", capLifetimes1)
}, http.StatusConflict, "box annie has not reported in 10 minutes"},
{"an old edges file and a fresh report", func(t *testing.T, l lifetimesFx) {
writeEdges(t, l.fx.rel, "annie", capLifetimes1, "region-ha-1")
- writeBoxStatus(t, l.fx.rel, "annie", l.clock.Now().Add(-time.Minute))
+ writeBoxStatus(t, l.fx.rel, "annie", time.Now().Add(-time.Minute))
}, http.StatusOK, ""},
}
for _, c := range cases {
@@ -78,7 +93,7 @@ func TestCutoverRefusesBoxesBelowTheFloor(t *testing.T) {
t.Run("a file losing the capability after the cutover pages hourly", func(t *testing.T) {
l := newLifetimesFx(t)
writeEdges(t, l.fx.rel, "annie", capLifetimes1)
- writeBoxStatus(t, l.fx.rel, "annie", l.clock.Now())
+ writeBoxStatus(t, l.fx.rel, "annie", time.Now())
turnLifetimesOn(t, l.fx.srv, l.clock)
var pages []string
var mu sync.Mutex