carlos / platform Public

console: the db door refuses on every move kind (§19.1)

moving, landing, reverting, restoring and relocating (K14) join deleting,
parking and purging in dbClaim's switch: errDBClaimClosed, a 409 with
Retry-After: 1, in every phase, landing/committed and the opened phases
included. The door reopens at the close, when the claim is serving again.

TestDBDoorRefusesDuringMoves walks every phase of the five kinds and the
serving close.

The B8 census digest of Server.dbClaim (a RecordKey key-site) changes for
the added case; its record-key comparison is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev d1e272677c36f2bc8adcd803e6433c8acdc0803d parent 5adc46f
3 files changed, +85 −1
  • internal/console/apidbclaim.go +5 −0
  • internal/console/apidbclaim_test.go +79 −0
  • internal/provision/plan9_b8_census_test.go +1 −1
diff --git a/internal/console/apidbclaim.go b/internal/console/apidbclaim.go
index fbd685ead..d9fb9e8f0 100644
--- a/internal/console/apidbclaim.go
+++ b/internal/console/apidbclaim.go
@@ -139,6 +139,11 @@ func (s *Server) dbClaim(ctx context.Context, g dbRequest, rec provision.Record)
switch obs.Claim.Op.Kind {
case hostclaim.KindDeleting, hostclaim.KindParking, hostclaim.KindPurging:
return obs, errDBClaimClosed
+ case hostclaim.KindMoving, hostclaim.KindLanding, hostclaim.KindReverting, hostclaim.KindRestoring, hostclaim.KindRelocating:
+ // Every move kind, in every phase, landing/committed and the
+ // opened phases included (moves spec §19.1): the door reopens at
+ // the close, when the claim is serving again.
+ return obs, errDBClaimClosed
}
if h.Location == "" {
return obs, errDBClaimClosed
diff --git a/internal/console/apidbclaim_test.go b/internal/console/apidbclaim_test.go
index 560258018..898057e82 100644
--- a/internal/console/apidbclaim_test.go
+++ b/internal/console/apidbclaim_test.go
@@ -384,3 +384,82 @@ func TestDoorPlanRangeReaderCloses(t *testing.T) {
})
}
}
+
+// TestDBDoorRefusesDuringMoves (moves spec §19.1, Review Focus 4, K14): the
+// db door refuses on every move kind, in every phase, including
+// landing/committed and the restoring and reverting opened phases; it
+// reopens at the close, when the claim is serving again. The refusal is a
+// 409 with Retry-After: 1, and the data bucket is never read.
+func TestDBDoorRefusesDuringMoves(t *testing.T) {
+ const move = "mv_abcdefghijklmnopqrstuvwxyz"
+ reloc := func(phase, exec string) hostclaim.Op {
+ return hostclaim.Op{Kind: hostclaim.KindRelocating, ID: move, Phase: phase, Exec: exec,
+ Intent: map[string]string{"region": "uk", "source_box": "ie-box", "target_box": "uk-box"}}
+ }
+ cases := []hostclaim.Op{
+ {Kind: hostclaim.KindMoving, ID: move, Phase: hostclaim.PhaseMoving, Exec: "ie-box"},
+ {Kind: hostclaim.KindMoving, ID: move, Phase: hostclaim.PhaseSealed},
+ {Kind: hostclaim.KindMoving, ID: move, Phase: hostclaim.PhaseAborting, Exec: "ie-box"},
+ {Kind: hostclaim.KindLanding, ID: move, Phase: "checking", Exec: "uk-box"},
+ {Kind: hostclaim.KindLanding, ID: move, Phase: hostclaim.PhaseLandingProbing, Exec: "uk-box"},
+ {Kind: hostclaim.KindLanding, ID: move, Phase: hostclaim.PhaseLandingProbed, Exec: "uk-box"},
+ {Kind: hostclaim.KindLanding, ID: move, Phase: hostclaim.PhaseLandingCommitted, Exec: "uk-box"},
+ {Kind: hostclaim.KindReverting, ID: move, Exec: "ie-box", Journal: map[string]string{hostclaim.JournalRevPhase: hostclaim.RevPhaseEvidence}},
+ {Kind: hostclaim.KindReverting, ID: move, Exec: "ie-box", Journal: map[string]string{hostclaim.JournalRevPhase: hostclaim.RevPhaseVerify}},
+ {Kind: hostclaim.KindReverting, ID: move, Phase: hostclaim.PhaseProbing, Exec: "ie-box"},
+ {Kind: hostclaim.KindReverting, ID: move, Phase: hostclaim.PhaseOpened, Exec: "ie-box"},
+ {Kind: hostclaim.KindRestoring, ID: move, Phase: hostclaim.PhaseReleasing, Exec: "uk-box"},
+ {Kind: hostclaim.KindRestoring, ID: move, Phase: hostclaim.PhaseReleased},
+ {Kind: hostclaim.KindRestoring, ID: move, Phase: hostclaim.PhaseClaimed, Exec: "ie-box"},
+ {Kind: hostclaim.KindRestoring, ID: move, Phase: hostclaim.PhaseProbing, Exec: "ie-box"},
+ {Kind: hostclaim.KindRestoring, ID: move, Phase: hostclaim.PhaseProbed, Exec: "ie-box"},
+ {Kind: hostclaim.KindRestoring, ID: move, Phase: hostclaim.PhaseOpened, Exec: "ie-box"},
+ reloc("starting", "ie-box"),
+ reloc("releasing", "none"),
+ reloc("pinning", "none"),
+ reloc("installing", "uk-box"),
+ reloc("installed", "uk-box"),
+ reloc("probing", "uk-box"),
+ reloc("probe-failed", "uk-box"),
+ reloc("serving", "uk-box"),
+ }
+ kinds := map[string]bool{}
+ for _, op := range cases {
+ kinds[op.Kind] = true
+ name := op.Kind + "/" + op.Phase
+ if rp := op.Journal[hostclaim.JournalRevPhase]; rp != "" {
+ name += "rev_phase=" + rp
+ }
+ t.Run(name, func(t *testing.T) {
+ s, g, rec, m := doorPlanFixture(t, false)
+ m.FailOn = func(op, key string) error { t.Fatalf("a refusal reached the data bucket: %s %s", op, key); return nil }
+ c := cloneDoorClaim(g.claim.Claim)
+ c.Op = op
+ writeDoorClaim(t, s, dbRequest{host: rec.Host}, c)
+ _, err := s.dbClaim(t.Context(), g, rec)
+ if !errors.Is(err, errDBClaimClosed) {
+ t.Fatalf("%v, want the door closed", err)
+ }
+ w := httptest.NewRecorder()
+ s.dbClaimFailure(w, httptest.NewRequest(http.MethodGet, "/", nil), g, err)
+ if w.Code != http.StatusConflict || w.Header().Get("Retry-After") != "1" {
+ t.Fatalf("%d Retry-After %q, want 409 and 1", w.Code, w.Header().Get("Retry-After"))
+ }
+ })
+ }
+ for _, k := range []string{hostclaim.KindMoving, hostclaim.KindLanding, hostclaim.KindReverting, hostclaim.KindRestoring, hostclaim.KindRelocating} {
+ if !kinds[k] {
+ t.Errorf("the table has no %s row", k)
+ }
+ }
+ // The close: serving again, the door reopens.
+ t.Run("serving", func(t *testing.T) {
+ s, g, rec, _ := doorPlanFixture(t, false)
+ c := cloneDoorClaim(g.claim.Claim)
+ c.Op = hostclaim.Op{Kind: hostclaim.KindServing}
+ writeDoorClaim(t, s, dbRequest{host: rec.Host}, c)
+ if _, err := s.dbClaim(t.Context(), g, rec); err != nil {
+ t.Fatalf("serving: %v", err)
+ }
+ })
+}
diff --git a/internal/provision/plan9_b8_census_test.go b/internal/provision/plan9_b8_census_test.go
index f62eb3dd5..50ceb077a 100644
--- a/internal/provision/plan9_b8_census_test.go
+++ b/internal/provision/plan9_b8_census_test.go
@@ -546,7 +546,7 @@ var plan9B8CensusInventory = []censusEntry{
{"internal/boxid/record.go", "Publish", "nonrecord", "sel:Put", "c60d1cf14e758de490c0824b635eb5d6ac54e24b5dd05c99a576a8a03d46ead7"},
{"internal/boxvolumes/volumes.go", "Write", "nonrecord", "sel:Put", "70ec487d1180d4abe18031d07f49ea98ff7d7fb222403c1ed9304c00429e7825"},
{"internal/console/account.go", "WriteAccountManifest", "nonrecord", "sel:Put", "f6f1ec0f2aa488ab51850ccf47753e37736d65b94708a1cf25ee0af1152ce251"},
- {"internal/console/apidbclaim.go", "Server.dbClaim", "key-site", "key:RecordKey", "9901c67966f4f932b0dd4acff8b243be5d91bcef55fff0c34ec03ba080b87983"},
+ {"internal/console/apidbclaim.go", "Server.dbClaim", "key-site", "key:RecordKey", "ba56b1742d07a47e9ccb17ff1f7a4def9722c2ef0ed51eecf6fd8550d292fa31"},
{"internal/console/apidomains.go", "Server.handleAPIDomainAttach", "key-site", "lit:instances", "f235eb06f14a7a2d74b69214076329b7d506eb5d49532ba74aa78b61e90fc5e2"},
{"internal/console/apidoorkeys.go", "Server.publishDoorKeysLocked", "foreign-recordkey", "key:RecordKey,sel:Put", "0fc498cd4af50570a0b7ed3ff71994213cab40040f6701a303bdf85d94f958da"},
{"internal/console/apiinstancemove.go", "Server.moveTransitionSaying", "record-writer", "key:SetRecordMove", "d1f7162eecdc234607c708a83e9633cdf77cc6931bac6754fb51042dd8409377"},