carlos / platform Public

zdd-5 task 1: pin the plan 1-4 surface; CARLOS_SWAP flag

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 5bf25d58da4363c81231db879d0851ba9fe251da parent a3bf7d6
4 files changed, +131 −0
  • internal/activator/zdd5_surface_test.go +67 −0
  • internal/release/zdd5_surface_test.go +32 −0
  • internal/rollout/flag.go +18 −0
  • internal/rollout/flag_test.go +14 −0
diff --git a/internal/activator/zdd5_surface_test.go b/internal/activator/zdd5_surface_test.go
new file mode 100644
index 000000000..c086635bb
--- /dev/null
+++ b/internal/activator/zdd5_surface_test.go
@@ -0,0 +1,67 @@
+package activator
+
+import (
+ "context"
+ "time"
+
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// Plan 5 of the zero-downtime deploys work consumes these names. The index
+// fixes the first block; plans 1 to 4 fix the rest in their own documents
+// (the index's "Cross-plan interface ledger"). If this file does not
+// compile, a plan merged a different name, and plan 5 stops until it is
+// reconciled with that plan's owner.
+var (
+ _ func(*Service, string) (string, bool) = (*Service).AppSocket
+ _ func(*Service, context.Context, Attempt, func(string) uint64) SwapResult = (*Service).Swap
+ _ = Gen{N: 1, Socket: "", Control: "", Build: "", StagedPath: "", Digest: "", Demand: ""}
+ _ = Attempt{Host: "", Cause: CauseRelease, Build: "", StagedPath: "", Digest: "", Demand: "", Mode: ModeOverlap, ConfigRev: "", RestartID: "", SchedRev: 0, Canary: false}
+ _ = SwapResult{Outcome: SwapDone, Gen: Gen{}, Reason: "", Drain: time.Second}
+ _ = []SwapOutcome{SwapDone, SwapFailed, SwapAbandoned, SwapDown}
+ _ = []Cause{CauseRelease, CauseInstall, CauseRetry, CauseRestart, CauseConfig}
+ _ = []SwapMode{ModeOverlap, ModeHold}
+
+ _ func(bool) = SetGenerations // plan 1
+ _ func(*ExecBackend, context.Context, registry.Route, uint64) error = (*ExecBackend).KillGen // plan 2
+ _ func(*SplitBackend, context.Context, registry.Route, uint64) error = (*SplitBackend).KillGen // plan 2
+ _ SwapBackend = (*SplitBackend)(nil) // plan 3
+ _ genKiller = (*SplitBackend)(nil) // plan 3 Task 2 declares genKiller; plan 5 declares none
+ _ func(*Service, *actEntry, Attempt, func(string) uint64) error = (*Service).swapChecksLocked // plan 3
+ _ func(*Service, context.Context, registry.Route, Gen) (Proc, string, Gen, error) = (*Service).startPinned // plan 3
+ _ func(context.Context, Proc, time.Duration) settleResult = observeSettle // plan 3
+ _ = []settleResult{settleOK, settleExited, settleCancelled}
+ _ = []error{errSwapNotAwake, errSwapBusy, errSwapStopped} // plan 3
+ _ = StartPolicy{Pin: &Gen{}, Fallback: &Gen{}, HealthBy: time.Time{}} // plan 3
+
+ // Adapted: the plan pinned startWorker as returning (Proc,
+ // release.StagedIdentity, error) with no gate. Main
+ // (workerbuild.go:77) takes a gate and also returns a pendingGeneration.
+ _ func(*Service, context.Context, registry.Route, map[string]string, *release.StagedIdentity, func() error) (Proc, release.StagedIdentity, pendingGeneration, error) = (*Service).startWorker // plan 4
+ _ func(*Service, *actEntry, Proc, string, *release.StagedIdentity) = (*Service).respawnWorkerLocked // plan 4
+
+ // Added by the controller: later tasks build on these merged names.
+ _ func(*Service, context.Context, registry.Route, Gen, func() error) (Proc, string, Gen, error) = (*Service).startPinnedAt // swaphold.go
+ _ func(*Service, context.Context, *actEntry, registry.Route, Gen, Lease) (Proc, string, Gen, error) = (*Service).startPinnedChecked // swaphold.go
+ _ error = ErrBuildNotPublished // workerbuild.go
+ _ func(*Service) error = (*Service).persistGenerations // generations_file.go
+)
+
+// Fields plan 5 reads or sets.
+func _(a *Service, e *actEntry, w *wokenInstance, m *genMember, eb *ExecBackend) {
+ a.gens = true // plan 1
+ eb.GenContainment = true // plan 2
+ a.genHealth = func(context.Context, Gen) error { return nil } // plan 3, test seam
+ a.CoalesceRecycle = func(string) bool { return false } // plan 4
+ var _ Gen = e.gen // plan 1
+ var _ Gen = w.gen // plan 1
+ var _ *Gen = e.pin // plan 3
+ var _ *Gen = e.pinFailed // plan 3
+ var _ string = e.pinFailReason // plan 3
+ var _ map[uint64]*genMember = e.ops.members // plan 3
+ var _ Gen = m.gen // plan 3
+ var _ release.StagedIdentity = e.workerServing // plan 4
+ var _ chan struct{} = e.respawnDone // main
+ var _ string = a.GenerationsFile // generations_file.go
+}
diff --git a/internal/release/zdd5_surface_test.go b/internal/release/zdd5_surface_test.go
new file mode 100644
index 000000000..853df618b
--- /dev/null
+++ b/internal/release/zdd5_surface_test.go
@@ -0,0 +1,32 @@
+package release
+
+import (
+ "context"
+
+ "github.com/carlosframework/platform/internal/registry"
+)
+
+// Plan 5 of the zero-downtime deploys work consumes these names from plan
+// 4 and from main. See internal/activator/zdd5_surface_test.go for what to
+// do when this file does not compile.
+var (
+ _ func(context.Context, Store, string, string, string, string, string) (string, string, error) = CanonicalDemand
+ _ = StagedIdentity{Version: "", StagedPath: "", Digest: "", Demand: ""}
+ _ = Manifest{Swap: DemandHold}
+ _ = ErrStagedDigestMismatch
+ _ func(registry.Route) bool = SidecarAdoptable
+
+ _ = Generations{Schema: 1, Hosts: map[string][]string{}}
+ _ func(string) string = GenerationsPath
+ _ func(string) (*Generations, error) = ReadGenerations
+ _ func(string, Generations) error = WriteGenerations
+ _ func(string, func() ([]registry.Route, error)) PinSource = GenerationPins
+ _ func(string, []registry.Route) (StagedPaths, error) = BoxPins
+ _ func(string) (string, error) = FileSHA256
+ _ func(string, string) (*Refusal, error) = ReadRefusal // main, 9e628024
+ _ = ChannelPointer{}.PromotedAt
+ _ = Refusal{}.Version
+
+ // Added by the controller: plan 4's sweep pin reader.
+ _ func(string, string) (map[string]bool, error) = GenerationVersions
+)
diff --git a/internal/rollout/flag.go b/internal/rollout/flag.go
new file mode 100644
index 000000000..e42d5e23c
--- /dev/null
+++ b/internal/rollout/flag.go
@@ -0,0 +1,18 @@
+// Package rollout is the box's rollout scheduler (zero-downtime deploys spec
+// §6, §8, §10). It decides which instance moves to which build, when, in which
+// swap mode, and what happens when a build fails. It is armed only by
+// CARLOS_SWAP=overlap. With the flag unset nothing in this package runs, and
+// the box keeps today's stop-then-wake paths.
+package rollout
+
+import "strings"
+
+// SwapEnv is the box flag (spec §15).
+const SwapEnv = "CARLOS_SWAP"
+
+// Enabled reports whether the box flag arms the scheduler. Only the exact
+// word "overlap" does: a typo must leave the box on today's paths, not on a
+// half-understood new one.
+func Enabled(getenv func(string) string) bool {
+ return strings.TrimSpace(getenv(SwapEnv)) == "overlap"
+}
diff --git a/internal/rollout/flag_test.go b/internal/rollout/flag_test.go
new file mode 100644
index 000000000..ed1e0b424
--- /dev/null
+++ b/internal/rollout/flag_test.go
@@ -0,0 +1,14 @@
+package rollout
+
+import "testing"
+
+func TestEnabledOnlyForOverlap(t *testing.T) {
+ for v, want := range map[string]bool{
+ "": false, "overlap": true, " overlap ": true,
+ "hold": false, "OVERLAP": false, "1": false, "true": false,
+ } {
+ if got := Enabled(func(string) string { return v }); got != want {
+ t.Errorf("Enabled(%q) = %v, want %v", v, got, want)
+ }
+ }
+}