// generations.go is the generation identity of zero-downtime deploys // (docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md §7.1, // as amended by 2026-10-13-zero-downtime-deploys-rev19-amendment.md): the // process-wide counter, the generation socket paths, the Gen handle, and the // dormant switch that keeps all of it off by default. package activator import ( "context" "errors" "log" "math" "os" "path/filepath" "strconv" "sync/atomic" "github.com/carlosframework/platform/internal/cgroup" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" ) // generationsOn is the dormant switch (spec §15). Off, every spawn binds the // route's canonical socket exactly as before, and nothing outside this // package changes behaviour. Plan 5 turns it on from CARLOS_SWAP=overlap by // calling SetGenerations(true) in internal/host's runAgent BEFORE // activator.New: a Service reads the switch once, at construction, so one // process never mixes the two socket schemes. var generationsOn atomic.Bool // SetGenerations sets the dormant switch for Services built after the call. func SetGenerations(on bool) { generationsOn.Store(on) } // GenerationsEnabled reports the switch as it stands now. func GenerationsEnabled() bool { return generationsOn.Load() } // genCounter is the ONE generation counter for the whole process (spec // §7.1). It is package-level, never per Service and never per entry, and it // is never reset: an entry deleted at a stop and recreated at the next wake // cannot reuse a number an earlier entry had, so a cached (route, n) pair in // the edge (same process) can never match a later generation. var genCounter atomic.Uint64 // nextGen takes the next generation number. The first is 1; 0 means "no // generation" everywhere a Gen is optional. func nextGen() uint64 { return genCounter.Add(1) } // Gen is one spawn of one instance (spec §3 "Generation"). It is the // generation HANDLE (spec §7.1): returned by the spawn and held by whoever // started it. Health checks, settle observation and retirement use the // handle, never AppSocket. Plan 1 fills N, Socket, Control and Build; // StagedPath, Digest and Demand stay empty until plans 3 and 4 exec by // staged path. type Gen struct { N uint64 // process-wide, never reused (spec §7.1) Socket string // generation app socket (spec §7.1, rev 19: the flag-off file name in a directory of its own) Control string // Socket + ".control" Build string // version name the generation runs StagedPath string // immutable staged executable it exec'd (spec §7.5 step 3) Digest string // sha256 of StagedPath at exec, hex Demand string // the release's own `swap` demand verified for this artifact: "hold" or "" (spec §4.2, §8.5) // Tier is the generation's own artifact's tier (zero-downtime spec // §7.3), captured when an attempt is admitted (plan 3). Zero for a // live-link wake until a swap captures it from Build. Tier cgroup.Limit // Worker marks a worker's generation (zero-downtime spec §7.9): it has // no generation child, and its termination proof is its host cgroup, // with or without a process handle (genTerminated). Plan 3 Task 11 // sets it. Worker bool } // GenSocketDir is the root of the flat generation directories: // /run/carlos/g. Generation n's directory is /run/carlos/g/. const GenSocketDir = provision.SocketDir + "/g" // maxUnixPath is the longest path a unix socket address holds. const maxUnixPath = provision.MaxUnixPath // GenSocketPath is flat generation n's socket (zero-downtime spec §7.1 as // amended by rev 19, rule 1): /run/carlos/g//.sock. The file name // is the flag-off one, because an app may work out its host from the file // name of its --socket argument, and a name the flag-off launch never gives // it can make it refuse to start (amadan #272, Gate F). Each generation gets // its own directory instead. The hostname is in the path, so its length is // bounded by the host's: flatGenFits decides whether a host has generation // paths at all (rule 4). func GenSocketPath(host string, n uint64) string { return genSocketPathIn(GenSocketDir, host, n) } // genSocketPathIn is GenSocketPath under another directory (a test's // RunDirOverride). func genSocketPathIn(dir, host string, n uint64) string { return filepath.Join(dir, strconv.FormatUint(n, 10), host+".sock") } // flatGenFits reports whether host's flat generation sockets fit a unix // socket address (provision.MaxUnixPath, 107 bytes) for every generation // number (rev 19, rule 4): the control socket, the longer of the pair, at // the longest number a generation can have. That admits hosts of up to 59 // bytes; the flag-off control socket already caps a flat host at 82. It // depends on the host alone, so a row never changes treatment as // the process-wide counter grows, and an attempt admitted as an overlap // never meets a generation that cannot bind. A host that does not fit is // handled as a foreign row is (genLayout): it keeps its canonical socket. // // It is measured against GenSocketDir even under a test's RunDirOverride, // so a test sees the decision a box makes. func flatGenFits(host string) bool { return len(GenSocketPath(host, math.MaxUint64)+".control") <= maxUnixPath } // genLayout is layout l of host's row as generations see it (rev 19, rule // 4): a flat row whose generation sockets would not fit a unix socket path // (flatGenFits) reads foreign, so it keeps its canonical socket and an // overlap on it runs as a hold, exactly as for a foreign row. Every other // layout is l. Classify and pathsFor are unchanged: the flag-off spawn, the // layout pass and every path but the generation socket's still read the row // as flat. func genLayout(l Layout, host string) Layout { if l == LayoutFlat && !flatGenFits(host) { return LayoutForeign } return l } // NestedGenSocket is the nested layout's generation socket (rev 19, rule // 1): /run/carlos/instances//g/app.sock, the flag-off file // name in a directory of the generation's own. The identifier is bounded at // 40 bytes, so this is at most 93 bytes (101 with ".control") whatever n is. func NestedGenSocket(host string, n uint64) string { return nestedGenSocketIn(provision.InstanceRunDir(host), n) } // nestedGenSocketIn is NestedGenSocket under an already-resolved run dir. func nestedGenSocketIn(runDir string, n uint64) string { return filepath.Join(runDir, "g"+strconv.FormatUint(n, 10), "app.sock") } // removeGenDir removes generation g's own socket directory once g's // termination is proven, if it is empty (rev 19, rule 3). It touches only a // directory named for g.N (flat: , nested: g) that holds g.Socket, so // a sibling's directory, the run dir and GenSocketDir itself are never // touched. A generation on its row's canonical socket (generations off, a // foreign row, a unit tenant) has no directory of its own and is skipped. A // directory that still holds a file (an app that left its socket behind) // stays: it is harmless, and the stale-file rule clears that file if the // number is ever used again. func removeGenDir(rt registry.Route, g Gen) { if g.N == 0 || !linksCanonical(rt, g) { return } dir := filepath.Dir(g.Socket) n := strconv.FormatUint(g.N, 10) if base := filepath.Base(dir); base != n && base != "g"+n { return } entries, err := os.ReadDir(dir) if err != nil || len(entries) != 0 { return } if err := os.Remove(dir); err != nil && !errors.Is(err, os.ErrNotExist) { log.Printf("activator: %s: remove generation %d's socket directory: %v", rt.Host, g.N, err) } } // canonicalGen is the generation handle of a spawn that bound the route's // canonical socket: every spawn with the switch off, a backend with no // generation support (unit-backed tenants, test doubles), and a foreign // layout row. func canonicalGen(rt registry.Route, n uint64, build string) Gen { g := Gen{N: n, Socket: rt.Socket, Build: build} if rt.Socket != "" { g.Control = rt.Socket + ".control" } return g } // GenBackend is the optional Backend capability behind generations (spec // §7.1). ExecBackend and SplitBackend implement it. A backend that does not // (UnitBackend, every test double that predates this plan) is spawned with // Start and gets canonicalGen: one generation per entry, on the canonical // socket. type GenBackend interface { // StartGen spawns generation n of rt's instance and returns its handle. // perGen false must behave exactly as Start; true binds the generation's // own socket where the layout has one. StartGen(ctx context.Context, rt registry.Route, n uint64, perGen bool) (Proc, Gen, error) // NextDueVia is NextDue dialling socket rather than rt.Socket. NextDueVia(ctx context.Context, rt registry.Route, socket string) (int64, error) } var ( _ GenBackend = (*ExecBackend)(nil) _ GenBackend = (*SplitBackend)(nil) )