carlos / platform Public

console: the doors' sentences in shapes the copy gate reads

recordEligibility reads its sentences through its moveCopy parameter, so
the kind rail is a method (kindRail) with both forms written out rather
than a function field; the seed refusal is seedMoveRefusal beside
refuseSeedMove; every self-serve refusal is a literal or a format the gate
derives (the in-flight 409 is MV-N43's format, the run 409 is written out).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev bb74a452103783f98e5e5fcca6b9e7ab7eceeeb7 parent 2a95303
5 files changed, +45 −34
  • internal/console/apiinstancemove.go +21 −16
  • internal/console/apiinstancemove_offline.go +2 −2
  • internal/console/seedmove.go +9 −0
  • internal/console/selfserve_door.go +11 −14
  • internal/console/selfserve_door_test.go +2 −2
diff --git a/internal/console/apiinstancemove.go b/internal/console/apiinstancemove.go
index c2e546482..299dc11eb 100644
--- a/internal/console/apiinstancemove.go
+++ b/internal/console/apiinstancemove.go
@@ -84,23 +84,20 @@ func (s *Server) moveGate(w http.ResponseWriter, r *http.Request) (acct *Account
// door and the job (self-serve moves plan 2 Task 5) run the same checks on
// the record they act on, so every caller refuses one record one way.
func recordEligibility(rec *provision.Record, c moveCopy) *offlineRefusal {
- refuse := func(msg string) *offlineRefusal {
- return &offlineRefusal{status: http.StatusBadRequest, msg: msg}
+ if k := rec.Kind; k != "" && k != "instance" {
+ return &offlineRefusal{status: http.StatusBadRequest, msg: c.kindRail(k)}
}
- switch {
- case rec.Kind != "" && rec.Kind != "instance":
- return refuse(c.kind(rec.Kind))
- case rec.Addr != "" || len(rec.Addrs) > 0:
- return refuse(c.addr)
+ if rec.Addr != "" || len(rec.Addrs) > 0 {
+ return &offlineRefusal{status: http.StatusBadRequest, msg: c.addr}
}
if _, fleet := registry.FleetPlacement(rec.Region); fleet {
- return refuse(c.fleet)
+ return &offlineRefusal{status: http.StatusBadRequest, msg: c.fleet}
}
- switch {
- case rec.Pool != "":
- return refuse(c.pool)
- case !rec.HibernateOrDefault():
- return refuse(c.hibernate)
+ if rec.Pool != "" {
+ return &offlineRefusal{status: http.StatusBadRequest, msg: c.pool}
+ }
+ if !rec.HibernateOrDefault() {
+ return &offlineRefusal{status: http.StatusBadRequest, msg: c.hibernate}
}
return nil
}
@@ -111,16 +108,25 @@ func recordEligibility(rec *provision.Record, c moveCopy) *offlineRefusal {
// record MV-N47 to MV-N54), which no other door ever speaks.
type moveCopy struct {
gate gateCopy
- kind func(kind string) string
addr, fleet, pool, hibernate string
}
+// kindRail is the kind rail's sentence for a route of kind k, in c's words:
+// the Phase 1 sentence for the Phase 1 copy, main's otherwise. Written out
+// whole in both forms, so the copy gate reads it through
+// recordEligibility's parameter.
+func (c moveCopy) kindRail(k string) string {
+ if c.addr == phase1MoveCopy().addr {
+ return "only exec-backed instances move. this route is kind " + k
+ }
+ return "only exec-backed instances move \u2014 this route is kind " + k
+}
+
// mainMoveCopy returns main's sentences, byte for byte (the dashes are escaped
// only in the source).
func mainMoveCopy() moveCopy {
return moveCopy{
gate: mainGateCopy(),
- kind: func(k string) string { return "only exec-backed instances move \u2014 this route is kind " + k },
addr: "an addr-backed route's process is not platform-managed \u2014 nothing to move",
fleet: "fleet-placed instances keep state-does-not-follow semantics \u2014 move is for platform regions",
pool: "pooled instances do not move in v1 \u2014 their data lives on the pool's own box",
@@ -139,7 +145,6 @@ func phase1MoveCopy() moveCopy {
tooMany: func(string) string { return "too many instance requests. wait a minute and try again" },
noBucket: "this console has no deployment bucket, so it cannot answer this",
},
- kind: func(k string) string { return "only exec-backed instances move. this route is kind " + k },
addr: "an addr-backed route's process is not platform-managed. there is nothing to move",
fleet: "fleet-placed instances keep state-does-not-follow semantics. moves are for platform regions",
pool: "pooled instances do not move yet. their data lives on the pool's own box",
diff --git a/internal/console/apiinstancemove_offline.go b/internal/console/apiinstancemove_offline.go
index 075654cda..217907e8b 100644
--- a/internal/console/apiinstancemove_offline.go
+++ b/internal/console/apiinstancemove_offline.go
@@ -498,8 +498,8 @@ func (s *Server) offlineClaimEvidence(ctx context.Context, acct, app, host strin
// (offlineClaimEvidence). The operator start and stage doors, the self-serve
// door and the job run it on the record they act on.
func (s *Server) offlineRecordChecks(ctx context.Context, acct, app, host string, rec *provision.Record) *offlineRefusal {
- if b := rec.Seed.MoveBlock(); b != "" {
- return &offlineRefusal{status: http.StatusConflict, msg: provision.SeedMoveSentence(b)}
+ if ref := seedMoveRefusal(rec); ref != nil {
+ return ref
}
if rec.Role != "" {
return &offlineRefusal{status: http.StatusBadRequest, msg: "a replica does not move. delete it and create one in the new region"}
diff --git a/internal/console/seedmove.go b/internal/console/seedmove.go
index f4412dcde..5b8089050 100644
--- a/internal/console/seedmove.go
+++ b/internal/console/seedmove.go
@@ -26,6 +26,15 @@ func (s *Server) refuseSeedMove(w http.ResponseWriter, rec *provision.Record) bo
return false
}
+// seedMoveRefusal is refuseSeedMove as a refusal, for a check that answers
+// later (offlineRecordChecks): the same 409 and the same sentence.
+func seedMoveRefusal(rec *provision.Record) *offlineRefusal {
+ if b := rec.Seed.MoveBlock(); b != "" {
+ return &offlineRefusal{status: http.StatusConflict, msg: provision.SeedMoveSentence(b)}
+ }
+ return nil
+}
+
// placeSeedReadFailed is carlos apps place's answer when the strict read of
// the app's records fails, before anything has moved. The listing after the
// placement is written keeps main's sentence (placeInstancesFailed).
diff --git a/internal/console/selfserve_door.go b/internal/console/selfserve_door.go
index 636c3f7dc..0bc22d690 100644
--- a/internal/console/selfserve_door.go
+++ b/internal/console/selfserve_door.go
@@ -72,10 +72,6 @@ func selfServeNotReady(phase string) *offlineRefusal {
return &offlineRefusal{status: http.StatusConflict, msg: fmt.Sprintf("the move is not ready to cut yet. it is %s", phase)}
}
-func conflict(msg string) *offlineRefusal {
- return &offlineRefusal{status: http.StatusConflict, msg: msg}
-}
-
// selfServeDoorHook, when set, is called by the cut and cancel doors
// between reading the row and their write ("cut", "abort"), and by the
// request door before it migrates a legacy record ("migrate"). A TEST SEAM: the race tests
@@ -90,13 +86,13 @@ func selfServeDoorPause(door string) {
// selfServeInFlight is the 409 for a move already in flight: the record's
// own sentence for its move ("a move to <to> is already <state>", as every
-// start transition words it), and for an open row the record does not
-// carry yet the same sentence in the view's word for the row.
+// start transition words it, MV-N43), and for an open row the record does
+// not carry yet the same sentence in the view's word for the row.
func selfServeInFlight(rec *provision.Record, open *SelfServeMove) *offlineRefusal {
if rec != nil && rec.Move != nil {
- return conflict(offlineConflict(fmt.Errorf("%w: a move to %s is already %s", provision.ErrMoveConflict, rec.Move.To, rec.Move.State)))
+ return &offlineRefusal{status: http.StatusConflict, msg: fmt.Sprintf("a move to %s is already %s", rec.Move.To, rec.Move.State)}
}
- return conflict(offlineConflict(fmt.Errorf("%w: a move to %s is already %s", provision.ErrMoveConflict, open.To, selfServeRowState(open))))
+ return &offlineRefusal{status: http.StatusConflict, msg: fmt.Sprintf("a move to %s is already %s", open.To, selfServeRowState(open))}
}
// selfServeBusy is the prepare's checks of the record and the journal, in
@@ -107,7 +103,8 @@ func selfServeBusy(rec *provision.Record, open *SelfServeMove) *offlineRefusal {
return ref
}
if rec.ActiveRun.Open() {
- return conflict((&provision.MoveRunOpenError{Run: *rec.ActiveRun}).Sentence())
+ // provision.MoveRunOpenError's sentence, the run family's 409.
+ return &offlineRefusal{status: http.StatusConflict, msg: fmt.Sprintf("a run is open on this instance: %s. wait for it to finish or cancel it, then move the instance", rec.ActiveRun.ID)}
}
if rec.Move != nil || open != nil {
return selfServeInFlight(rec, open)
@@ -174,21 +171,21 @@ func selfServeSourceFacts(m *status.BoxMetrics, host string, inc uint64, now tim
}
switch {
case !ok:
- return f, conflict(msgSelfServeUnmeasured)
+ return f, &offlineRefusal{status: http.StatusConflict, msg: msgSelfServeUnmeasured}
case f.OverCap:
- return f, conflict(msgSelfServeTooLarge)
+ return f, &offlineRefusal{status: http.StatusConflict, msg: msgSelfServeTooLarge}
case !f.Known:
- return f, conflict(msgSelfServeUnmeasured)
+ return f, &offlineRefusal{status: http.StatusConflict, msg: msgSelfServeUnmeasured}
}
fresh := func(at int64) bool {
age := now.Sub(time.Unix(at, 0))
return at > 0 && age <= selfServeFactsFresh && age >= -selfServeFactsSkew
}
if f.Incarnation != inc || !fresh(f.CollectedAt) || !fresh(f.ReplicaCollectedAt) {
- return f, conflict(msgSelfServeSourceStale)
+ return f, &offlineRefusal{status: http.StatusConflict, msg: msgSelfServeSourceStale}
}
if f.Truncated {
- return f, conflict(msgSelfServeUnmeasured)
+ return f, &offlineRefusal{status: http.StatusConflict, msg: msgSelfServeUnmeasured}
}
return f, nil
}
diff --git a/internal/console/selfserve_door_test.go b/internal/console/selfserve_door_test.go
index ba6031c40..b4eb2f097 100644
--- a/internal/console/selfserve_door_test.go
+++ b/internal/console/selfserve_door_test.go
@@ -231,14 +231,14 @@ func TestRecordEligibilityIsMoveGatesChecks(t *testing.T) {
want string
}{
{"an eligible record", func(r *provision.Record) {}, ""},
- {"a service", func(r *provision.Record) { r.Kind = "service" }, c.kind("service")},
+ {"a service", func(r *provision.Record) { r.Kind = "service" }, c.kindRail("service")},
{"an addr", func(r *provision.Record) { r.Addr = "127.0.0.1:9000" }, c.addr},
{"addrs", func(r *provision.Record) { r.Addrs = []string{"127.0.0.1:9000"} }, c.addr},
{"a fleet placement", func(r *provision.Record) { r.Region = "fleet/shop" }, c.fleet},
{"a pool", func(r *provision.Record) { r.Pool = "shop" }, c.pool},
{"not hibernating", func(r *provision.Record) { r.Hibernate = &no }, c.hibernate},
// moveGate's order: the first rail a record fails is the answer.
- {"a service in a pool", func(r *provision.Record) { r.Kind = "service"; r.Pool = "shop" }, c.kind("service")},
+ {"a service in a pool", func(r *provision.Record) { r.Kind = "service"; r.Pool = "shop" }, c.kindRail("service")},
{"an addr in a pool", func(r *provision.Record) { r.Addr = "127.0.0.1:9000"; r.Pool = "shop" }, c.addr},
{"a fleet placement in a pool", func(r *provision.Record) { r.Region = "fleet/shop"; r.Pool = "shop" }, c.fleet},
{"a pool, not hibernating", func(r *provision.Record) { r.Pool = "shop"; r.Hibernate = &no }, c.pool},