carlos / platform Public

offline abort: before the seal a pinned move waits for its pinned sources (final review F2)

offlineStepFor's reverting check takes the required source boxes of a move
aborted before the seal from the record's pinned Participants when it has
them (only the self-serve queue pins them), never the current fleet, so a
box that joined the source region since cannot hold the abort open. An
operator move keeps the fleet's boxes, byte for byte.

Witness: TestAbortUsesPinnedSourcesAfterFleetGrowth (e2e).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 6a2fe8b88687a36539dc3a2184106648eee319d6 parent 76bb215
2 files changed, +68 −4
  • internal/console/apiinstancemove_offline.go +11 −4
  • internal/e2e/selfserve/abort_pinned_test.go +57 −0
diff --git a/internal/console/apiinstancemove_offline.go b/internal/console/apiinstancemove_offline.go
index 44c5241ea..f789a8b93 100644
--- a/internal/console/apiinstancemove_offline.go
+++ b/internal/console/apiinstancemove_offline.go
@@ -1301,11 +1301,18 @@ func (s *Server) offlineStepFor(ctx context.Context, d offlineDoor, mv *provisio
return offlineStep{mut: offlineFinishMut(mv.ID, now)}, nil
case provision.OfflineReverting:
// Every box the seal named, and the sealer. A move aborted before
- // the seal named neither: the boxes are the source region's as the
- // edges files publish them now, and there is no sealer (it never
- // took a tombstone).
+ // the seal named neither, and there is no sealer (it never took a
+ // tombstone): a move that pinned its participants waits for its
+ // pinned sources, never a box that joined the region since (final
+ // review F2); an operator move's boxes are the source region's as
+ // the edges files publish them now.
boxes, sealerPhase := mv.Boxes, moves.OfflinePhaseReverted
- if mv.Sealer == "" {
+ if mv.Sealer == "" && mv.Participants != nil && len(mv.Participants.Source) > 0 {
+ boxes, sealerPhase = nil, ""
+ for _, p := range mv.Participants.Source {
+ boxes = append(boxes, p.Label)
+ }
+ } else if mv.Sealer == "" {
fleet, ok := s.readOfflineFleet(ctx)
if !ok {
return offlineStep{}, &offlineRefusal{status: http.StatusConflict, msg: "the fleet's edges files could not all be read. try again", transient: true}
diff --git a/internal/e2e/selfserve/abort_pinned_test.go b/internal/e2e/selfserve/abort_pinned_test.go
new file mode 100644
index 000000000..b87283174
--- /dev/null
+++ b/internal/e2e/selfserve/abort_pinned_test.go
@@ -0,0 +1,57 @@
+//go:build e2e
+
+package selfserve
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/host"
+ "github.com/carlosframework/platform/internal/provision"
+)
+
+// TestAbortUsesPinnedSourcesAfterFleetGrowth (final review F2): an abort
+// before the seal waits for the pinned sources' reports, never the current
+// fleet's. Prepared with source A, the move reaches ready; a second box B
+// joins the source region (its edges file); the member cancels. A reports
+// its revert and B, never a participant, reports nothing: the source
+// reopens, the move ends aborted and its reservations are released.
+func TestAbortUsesPinnedSourcesAfterFleetGrowth(t *testing.T) {
+ w := newJobWorld(t)
+ row := w.admit()
+ w.watch = row.ID
+ region := w.record().Region
+ w.phase(row.ID, "ready", 60)
+
+ // B: the source box's edges file under another box's name.
+ raw, _, err := w.raw.Get(w.ctx, "control/edges/"+host.E2ESourceBox+".json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var f map[string]any
+ if err := json.Unmarshal(raw, &f); err != nil {
+ t.Fatal(err)
+ }
+ f["box"] = "ie-joined"
+ body, _ := json.Marshal(f)
+ if _, err := w.raw.Put(w.ctx, "control/edges/ie-joined.json", body, ""); err != nil {
+ t.Fatal(err)
+ }
+
+ w.cancel(row.ID)
+ r := w.finished(row.ID, 60)
+ if r.Outcome != provision.OfflineOutcomeAborted || r.SourceReopenedAt.IsZero() {
+ t.Fatalf("the abort did not finish: %s, reopened %v", describe(r), r.SourceReopenedAt)
+ }
+ if rec := w.record(); rec.Move != nil || rec.Region != region {
+ t.Fatalf("the record after the abort: region %q move %+v", rec.Region, rec.Move)
+ }
+ for k, e := range w.reservations() {
+ if e.ID == row.ID {
+ t.Fatalf("the reservation %s is still held: %+v", k, e)
+ }
+ }
+ if _, ok := w.report(row.ID, "source", "ie-joined"); ok {
+ t.Fatal("the joined box reported, so the witness proves nothing")
+ }
+}