Merge pull request #362 from carlosframework/move-serial-nudge
moves: serial nudges — cut chain in seconds (#331 rehearsal measurement)
1 file changed,
+29
−1
internal/host/movespass.go+29 −1
diff --git a/internal/host/movespass.go b/internal/host/movespass.go| index 1772374c5..f61190ba9 100644 |
| --- a/internal/host/movespass.go |
| +++ b/internal/host/movespass.go |
| @@ -216,6 +216,7 @@ func runMoveRound(ctx context.Context, s release.Store, cfg moveCfg, rec provisi |
| if err != nil && !isMoveConflict(err) { |
| return err |
| } |
| + nudgeSerial(ctx, s) |
| log.Printf("moves: %s: staged replica caught up in %s/%s — ready to cut", rec.Host, tgt.DataBucket, dstPrefix) |
| } |
| return nil |
| @@ -237,6 +238,7 @@ func runMoveRound(ctx context.Context, s release.Store, cfg moveCfg, rec provisi |
| if err != nil && !isMoveConflict(err) { |
| return err |
| } |
| + nudgeSerial(ctx, s) |
| log.Printf("moves: %s: abort complete — staged copy deleted, instance stays in %q", rec.Host, rec.Region) |
| return nil |
| @@ -247,7 +249,10 @@ func runMoveRound(ctx context.Context, s release.Store, cfg moveCfg, rec provisi |
| return fmt.Errorf("set route status: %w", err) |
| } |
| if !cfg.Quiesced(rec.Host) { |
| - return nil // the retire is in flight; next pass re-enters here |
| + if _, done := movingStatusNudged.LoadOrStore(rec.Host, true); !done { |
| + nudgeSerial(ctx, s) |
| + } |
| + return nil // the retire is in flight; the nudged pass re-enters here |
| } |
| // Step 3: prove the own replica carries the tail. The activator's |
| // stop SWALLOWS a failed final checkpoint (safe pre-move: the warm |
| @@ -274,12 +279,35 @@ func runMoveRound(ctx context.Context, s release.Store, cfg moveCfg, rec provisi |
| if err != nil && !isMoveConflict(err) { |
| return err |
| } |
| + movingStatusNudged.Delete(rec.Host) |
| + // The flip's nudge is what turns the TARGET's mint from a |
| + // 5-minute wait into a seconds-cadence converge. |
| + nudgeSerial(ctx, s) |
| log.Printf("moves: %s: cut complete — now homed in %s (route retires on the next reconcile pass)", rec.Host, rec.Move.To) |
| return nil |
| } |
| return nil |
| } |
| +// nudgeSerial bumps control/serial after a box-side move write, so the |
| +// NEXT actor — this box's own cut continuation, the target's mint, the |
| +// tenant's poll — reacts on the fast-converge path instead of the 5-minute |
| +// floor. The live rehearsal measured that floor three times over in one |
| +// cut (status→quiesce→flip 331s, target mint+wake 700s); with the nudge, |
| +// each hop is a serial-triggered pass. Best-effort like the console's own |
| +// bumpSerial: a failed bump costs cadence, never correctness. |
| +func nudgeSerial(ctx context.Context, s release.Store) { |
| + if err := release.BumpSerial(ctx, s, time.Now()); err != nil { |
| + log.Printf("moves: serial nudge: %v (falling back to the timer floor)", err) |
| + } |
| +} |
| + |
| +// movingStatusNudged remembers which hosts this process already nudged for |
| +// the status→quiesce hop, so a child that refuses to die does not turn the |
| +// fast-converge loop into a 2s self-triggering cycle: ONE nudge per host |
| +// per process, then the 5-minute floor stands under a wedged stop. |
| +var movingStatusNudged sync.Map |
| + |
| // logMoveTrouble rate-limits per-host move errors to one line a minute. |
| var moveTroubleLast sync.Map // host → time.Time |