| index 9f86a586a..27b7882cc 100644 |
| --- a/internal/edge/edge.go |
| +++ b/internal/edge/edge.go |
| @@ -275,6 +275,14 @@ type Edge struct { |
| doorAddr string |
| doorKeys *doorKeyMirror |
| closeDoor func() |
| + // gens is the activator's generation view (zero-downtime spec §7.2), |
| + // set in New only when the activator implements GenSource AND its |
| + // switch is on; genPools is the edge's per-generation-socket idle |
| + // pools, retired through gens.OnGenRetired. Both nil with the switch off |
| + // (CARLOS_SWAP unset), and then nothing below reads them: managed |
| + // routes get unixProxy(t.Socket) and the row's version, as before. |
| + gens GenSource |
| + genPools *GenPools |
| } |
| |
| // RunOption configures the Edge that Run/RunWithActivator* builds, before it |
| @@ -348,6 +356,10 @@ type upstream struct { |
| // captured here would be a stale copy of the target's entry the moment |
| // the target's route changed within the same map build. |
| paths map[string]string |
| + // genManaged marks an activator-managed instance proxied through |
| + // appSocketProxy (generations on). Its cacheable responses are stored |
| + // only when tagged with the generation that served them (cache.go). |
| + genManaged bool |
| } |
| |
| // New builds an Edge over reg and performs the startup reload. act may be nil |
| @@ -356,6 +368,12 @@ func New(reg *registry.Registry, act Activator) (*Edge, error) { |
| e := &Edge{reg: reg, act: act, gate: gate.New(), probers: newProberSet(), gzipOn: gzipEnabled(), steeringHost: steeringProbeHostFromEnv(), appsDomain: appsDomainFromEnv()} |
| e.geo = geo.NewSwappable() |
| e.geoLookup = e.geo.Lookup |
| + // Generations (zero-downtime spec §7.2), before the startup Reload so |
| + // its first table build already proxies through them. |
| + if gs, ok := act.(GenSource); ok && gs.GenerationsOn() { |
| + e.gens, e.genPools = gs, NewGenPools() |
| + gs.OnGenRetired(e.genPools.Retire) |
| + } |
| if err := e.Reload(); err != nil { |
| return nil, err |
| } |
| @@ -621,6 +639,9 @@ func (e *Edge) Reload() error { |
| // viaTunnel: this host's bytes leave the box over a reverse channel, |
| // which is what disqualifies it from hibernation below. |
| viaTunnel := false |
| + // genManaged: an activator-managed instance reached through its |
| + // current generation (appSocketProxy), generations on only. |
| + genManaged := false |
| fleet, isFleetPlaced := registry.FleetPlacement(t.Region) |
| switch { |
| // Both reverse proxies get the Carlos-Purge response hook |
| @@ -681,7 +702,17 @@ func (e *Edge) Reload() error { |
| proxy = p |
| viaTunnel = true |
| case t.Socket != "": |
| - p := unixProxy(t.Socket) |
| + // An activator-managed instance's process binds a generation |
| + // socket, not t.Socket, once generations are on (spec §7.1): |
| + // every new connection asks the activator for the current one. |
| + // t.Kind/t.Hibernate are the activator-managed test below |
| + // (hib); viaTunnel is false in this arm by construction. |
| + var p *httputil.ReverseProxy |
| + if e.gens != nil && t.Kind == "instance" && t.Hibernate { |
| + p, genManaged = appSocketProxy(t.Host, e.gens, e.genPools), true |
| + } else { |
| + p = unixProxy(t.Socket) |
| + } |
| p.ModifyResponse = e.responseModifier(t) |
| proxy = p |
| // TCP upstreams. The gate is AddrList (registry), not t.Addr, so a |
| @@ -771,7 +802,11 @@ func (e *Edge) Reload() error { |
| // wrapping those would put a version on a response that is serving |
| // none, which is exactly the false "verified" the header exists to |
| // prevent. |
| - if t.Version != "" && t.Kind != "static" && t.Kind != "status" { |
| + // |
| + // A genManaged route is excluded too: its header comes from the |
| + // generation that served the request (genRoundTripper, spec §7.2), |
| + // and the registry's Version names only what is installed. |
| + if t.Version != "" && t.Kind != "static" && t.Kind != "status" && !genManaged { |
| proxy = versionHeader(t.Version, proxy) |
| } |
| // Hibernation is per-host: only an instance whose route carries the |
| @@ -832,7 +867,7 @@ func (e *Edge) Reload() error { |
| "route %s is a hibernating instance but this box has no activator, so nothing here will create %s or start its process. If this box runs `carlos agent`, set CARLOS_HIBERNATE_BUCKET in /etc/carlos/host.env and restart carlos-edge; `carlos edge` on its own never runs instances.", |
| t.Host, t.Socket)) |
| } |
| - m[t.Host] = upstream{proxy: proxy, route: t, hibernating: hib, viaTunnel: viaTunnel} |
| + m[t.Host] = upstream{proxy: proxy, route: t, hibernating: hib, viaTunnel: viaTunnel, genManaged: genManaged} |
| } |
| // Path routes, folded BEFORE aliases so an alias of a prefix-routed |
| // hostname inherits its map along with everything else it copies — an |