| new file mode 100644 |
| index 000000000..15c83b055 |
| --- /dev/null |
| +++ b/internal/activator/generations.go |
| @@ -0,0 +1,100 @@ |
| +// generations.go is the generation identity of zero-downtime deploys |
| +// (docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md §7.1): |
| +// the process-wide counter, the bounded generation socket paths, the Gen |
| +// handle, and the dormant switch that keeps all of it off by default. |
| +package activator |
| + |
| +import ( |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "path/filepath" |
| + "strconv" |
| + "sync/atomic" |
| + |
| + "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 (bounded path, spec §7.1) |
| + 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) |
| +} |
| + |
| +// GenSocketDir is where flat generation sockets live: /run/carlos/g. |
| +const GenSocketDir = provision.SocketDir + "/g" |
| + |
| +// GenSocketPath is the bounded flat generation path (spec §7.1): |
| +// /run/carlos/g/<first 12 hex of sha256(host)>.<n>.sock. It is at most 52 |
| +// bytes (60 with ".control") whatever the hostname, because the hostname |
| +// never appears in it. |
| +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 { |
| + sum := sha256.Sum256([]byte(host)) |
| + return filepath.Join(dir, hex.EncodeToString(sum[:])[:12]+"."+strconv.FormatUint(n, 10)+".sock") |
| +} |
| + |
| +// NestedGenSocket is the nested layout's generation socket: |
| +// /run/carlos/instances/<InstanceID>/app.<n>.sock (spec §7.1). The |
| +// identifier is bounded at 40 bytes, so this is at most 92 bytes (100 with |
| +// ".control"). |
| +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, "app."+strconv.FormatUint(n, 10)+".sock") |
| +} |
| + |
| +// 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 |
| +} |