package activator import ( "context" "sync/atomic" "github.com/carlosframework/platform/internal/edge" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // The Service's generation surface (zero-downtime spec §7.1). AppSocket is // for traffic; the handle (Gen) is for whoever started the generation. var _ edge.GenSource = (*Service)(nil) // GenerationsOn is the dormant switch as this Service read it at New. func (a *Service) GenerationsOn() bool { return a.gens } // startGen spawns the next generation of rt's instance: the process-wide // counter's next number, and the backend's generation spawn when it has // one. A backend without one is spawned with Start and gets the canonical // handle. func (a *Service) startGen(ctx context.Context, rt registry.Route) (Proc, Gen, error) { n := nextGen() if g, ok := a.backend.(GenBackend); ok { return g.StartGen(ctx, rt, n, a.gens) } proc, ver, err := a.backend.Start(ctx, rt) // The handle goes back with an error too (P2-13): a failed start may // have left its process running, and the failed-wake cleanup proves it // stopped by the generation it names. return proc, canonicalGen(rt, n, ver), err } // startGenExe is startGen on the identity a wake recorded (settings spec // §4.2): the same next number and the same socket choice (perGen is the // switch), but the process runs id's file by path, never the live link. // A failed start's handle names id too when it is a handle at all (a // refusal answers none): a process it may have left running is retained by // that handle (P2-13), and the generations file must keep naming the file // that process runs (genIdentity lists a staged path whatever the switch). func (a *Service) startGenExe(ctx context.Context, ib identityBackend, rt registry.Route, id release.StagedIdentity) (Proc, Gen, error) { proc, g, err := ib.StartGenExe(ctx, rt, nextGen(), a.gens, id) if err != nil && g.N != 0 { g.Build, g.StagedPath, g.Digest, g.Demand = id.Version, id.StagedPath, id.Digest, id.Demand } return proc, g, err } // AppSocket names host's CURRENT generation's socket, for traffic: the edge // proxy, scheduled delivery, the next-due probe at a stop, and the status // probe (spec §7.1). It answers for an awake or stopping entry (a stopping // generation is still the one in-flight requests reach). Asleep, waking or // a worker: false. func (a *Service) AppSocket(host string) (string, bool) { a.mu.Lock() defer a.mu.Unlock() e := a.entries[host] if e == nil || e.state == actWaking || e.gen.Socket == "" { return "", false } return e.gen.Socket, true } // ServingGen is AppSocket with the generation's number and build: the edge // picks a connection pool, stamps X-Carlos-Version and tags cache entries // from it (spec §7.2). func (a *Service) ServingGen(host string) (edge.GenInfo, bool) { a.mu.Lock() defer a.mu.Unlock() e := a.entries[host] if e == nil || e.state == actWaking || e.gen.Socket == "" { return edge.GenInfo{}, false } return edge.GenInfo{Socket: e.gen.Socket, N: e.gen.N, Build: e.gen.Build}, true } // CacheGen is the newest generation that has been current for host in this // process. Lock-free on purpose: the edge cache calls it under its own lock, // and plan 3's switch purges the cache while holding a.mu. func (a *Service) CacheGen(host string) (uint64, bool) { v, ok := a.cacheGens.Load(host) if !ok { return 0, false } return v.(*atomic.Uint64).Load(), true } // noteCurrent records that generation n of host has become current. func (a *Service) noteCurrent(host string, n uint64) { v, _ := a.cacheGens.LoadOrStore(host, new(atomic.Uint64)) v.(*atomic.Uint64).Store(n) } // OnGenRetired registers fn for every generation that stops being current // (edge.GenSource). Hooks fire only with generations on. func (a *Service) OnGenRetired(fn func(host, socket string)) { a.retiredMu.Lock() defer a.retiredMu.Unlock() a.retiredHooks = append(a.retiredHooks, fn) } // genRetired tells every hook that g has stopped being host's current // generation. The caller holds no lock. A no-op with generations off: the // canonical path is shared by every generation of a host then, so closing // "its" idle connections could close the next process's. func (a *Service) genRetired(host string, g Gen) { if !a.gens || g.Socket == "" { return } a.retiredMu.Lock() hooks := append([]func(string, string){}, a.retiredHooks...) a.retiredMu.Unlock() for _, fn := range hooks { fn(host, g.Socket) } } // nextDue is the sleep-time next-due probe, dialled at the instance's // current generation (spec §7.1, `backend_exec.go`'s NextDue). Hibernation // joins any successor first (plan 3), so at a stop the current generation // is the only one. With generations off AppSocket is rt.Socket, and this is // the old call. func (a *Service) nextDue(ctx context.Context, e *actEntry) (int64, error) { if g, ok := a.backend.(GenBackend); ok { if sock, ok := a.AppSocket(e.host); ok { return g.NextDueVia(ctx, e.route, sock) } } return a.backend.NextDue(ctx, e.route) }