carlos / platform Public

agent: arm the moves pass on both converge loops + the staging-fallback mirror (#331)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEypt7de2X1G84aQ9rNkCf
Paul Campbell pushed by paul@keymail.dev f44f9dc68b7d6b3d1abbe6cd293020f8df4c4d6b parent 13fb387
2 files changed, +56 −0
  • internal/host/agent.go +14 −0
  • internal/host/movespass.go +42 −0
diff --git a/internal/host/agent.go b/internal/host/agent.go
index 13bb1580c..c6ead7c4f 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -307,6 +307,18 @@ func RunAgent(args []string) {
if logSink != nil {
svc.Logs = logSink
}
+ // The residence-move machinery (issue #331), armed with the
+ // activator: the staging-fallback restore needs a same-region
+ // mirror client, and the move worker needs the service's quiesce
+ // answer. Both are best-effort — a mirror client that cannot
+ // build logs and leaves the fallback dark, exactly like every
+ // other optional duty here.
+ if m, err := lease.NewS3Mirror(context.Background(), cfg.Region, cfg.Region); err != nil {
+ log.Printf("agent: staging-fallback mirror client: %v (restore fallback disarmed)", err)
+ } else {
+ eb.Mirror = m
+ }
+ armMovesPass(*cfg, dd, reg, svc)
svc.Start()
act = svc
log.Printf("agent: hibernation activator armed (bucket %s, idle %s)", cfg.Bucket, cfg.Idle)
@@ -1451,6 +1463,7 @@ func fastConvergeLoop(reg *registry.Registry, svc *activator.Service, sd *schedu
if err := reconcileSerialized(ctx, store, reg, provisionTickConfig(os.Getenv)); err != nil {
log.Printf("agent: fast converge reconcile: %v", err)
}
+ runMovesPass(ctx, store)
// Folded once per pass and handed in, never re-read inside the
// loop: this leg runs on every serial bump (a seconds cadence),
// and the fold is a LoadBoxes plus a List plus one Get per edges
@@ -1916,6 +1929,7 @@ func instancesLoop(reg *registry.Registry) {
if err := reconcileSerialized(ctx, store, reg, cfg); err != nil {
log.Printf("agent: instances pass: %v", err)
}
+ runMovesPass(ctx, store)
deploymentRoutes := edgesfile.DeploymentRoutes(ctx, store, time.Now())
if err := domains.ReconcileAliases(ctx, store, reg, time.Now, deploymentRoutes); err != nil {
log.Printf("agent: instances alias pass: %v", err)
diff --git a/internal/host/movespass.go b/internal/host/movespass.go
index 4c2c07786..37df170a4 100644
--- a/internal/host/movespass.go
+++ b/internal/host/movespass.go
@@ -5,13 +5,16 @@ import (
"encoding/json"
"fmt"
"log"
+ "os"
"path/filepath"
"strings"
+ "sync/atomic"
"time"
"github.com/carlosframework/platform/internal/activator"
"github.com/carlosframework/platform/internal/lease"
"github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
)
@@ -38,6 +41,45 @@ import (
// Every step is idempotent; a box that dies mid-cut resumes at the top and
// falls through the already-done steps.
+// armedMoveCfg is RunAgent's one write; runMovesPass' reads are its only
+// other touch. An atomic pointer rather than plain assignment because the
+// arm happens on RunAgent's goroutine and the passes run on the converge
+// and instances loops.
+var armedMoveCfg atomic.Pointer[moveCfg]
+
+// armMovesPass wires the worker with what only RunAgent has in hand: the
+// activator's hibernate coordinates (the own-bucket side of every mirror),
+// the data dir the cut purges, the registry the cut quiesces through, and
+// the service that answers whether a child is down. Called once, before
+// svc.Start.
+func armMovesPass(hib activator.Config, dataDir string, reg *registry.Registry, svc *activator.Service) {
+ cfg := moveCfg{
+ Region: strings.TrimSpace(os.Getenv("CARLOS_REGION")),
+ OwnBucket: hib.Bucket,
+ OwnPrefix: hib.Prefix,
+ DataDir: dataDir,
+ Mirror: func(ctx context.Context, dstRegion string) (lease.MirrorStore, error) {
+ return lease.NewS3Mirror(ctx, hib.Region, dstRegion)
+ },
+ Quiesced: svc.Quiesced,
+ SetStatus: reg.SetStatus,
+ Now: time.Now,
+ }
+ armedMoveCfg.Store(&cfg)
+}
+
+// runMovesPass is the loops' call: inert until armed (no activator = no
+// exec instances = nothing this box could ever move).
+func runMovesPass(ctx context.Context, s release.Store) {
+ cfg := armedMoveCfg.Load()
+ if cfg == nil || s == nil {
+ return
+ }
+ if err := movesPass(ctx, s, *cfg); err != nil {
+ log.Printf("agent: moves pass: %v", err)
+ }
+}
+
// moveCfg carries what the worker needs, injectable for tests.
type moveCfg struct {
Region string // this box's placement code, "" included (the home)