| index d3431f19e..af45a6fab 100644 |
| --- a/internal/registry/registry.go |
| +++ b/internal/registry/registry.go |
| @@ -86,6 +86,20 @@ type Route struct { |
| // auto-removed by reconciliation; custom domains are operator/owner |
| // property and never touched. |
| Platform bool |
| + // Provisioned marks a route the instance-provisioning reconcile pass |
| + // (internal/provision) minted from a bucket instance record. Only |
| + // provisioned routes are ever auto-removed by that pass — the same |
| + // authority rule Platform carries for the domains pass, kept as a |
| + // SEPARATE flag because Platform means "alias to another route's |
| + // upstream" to three consumers (config sync, upstream picking, apex |
| + // candidacy) and a provisioned instance is the opposite: it owns a |
| + // process. |
| + Provisioned bool |
| + // Seed is the app's first-boot init argv as a JSON array (e.g. |
| + // ["seed"]), materialized from the app's instances.json by the |
| + // provision pass; "" = no seed. Run by the activator only on a wake |
| + // where restore-on-miss found no replica (backend_exec.go). |
| + Seed string |
| // DNSStatus is the last DNS health verdict for this host: "pointing" |
| // (resolves to a deployment edge IP), "elsewhere", or "" — unknown, the |
| // honest answer for a resolver failure, never a guess. |
| @@ -179,6 +193,8 @@ CREATE TABLE IF NOT EXISTS routes ( |
| `ALTER TABLE routes ADD COLUMN cert_expires_at INTEGER NOT NULL DEFAULT 0`, |
| `ALTER TABLE routes ADD COLUMN account TEXT NOT NULL DEFAULT ''`, |
| `ALTER TABLE routes ADD COLUMN environment TEXT NOT NULL DEFAULT ''`, |
| + `ALTER TABLE routes ADD COLUMN provisioned INTEGER NOT NULL DEFAULT 0`, |
| + `ALTER TABLE routes ADD COLUMN seed TEXT NOT NULL DEFAULT ''`, |
| } { |
| if _, err := r.sql.Exec(stmt); err != nil && !strings.Contains(err.Error(), "duplicate column") { |
| return err |
| @@ -344,20 +360,22 @@ func (r *Registry) Accounts() ([]Account, error) { |
| const routeCols = `host, slug, kind, socket, addr, status, last_error, db_path, |
| version, prev_version, app, pool, region, channel, hibernate, canary, |
| replication, next_due, backing, status_named, platform, dns_status, |
| -dns_checked_at, cert_expires_at, account, environment, created_at` |
| +dns_checked_at, cert_expires_at, account, environment, provisioned, seed, |
| +created_at` |
| |
| func scanRoute(scan func(...any) error) (Route, error) { |
| var t Route |
| - var hib, can, sn, plat int |
| + var hib, can, sn, plat, prov int |
| err := scan(&t.Host, &t.Slug, &t.Kind, &t.Socket, &t.Addr, &t.Status, |
| &t.LastError, &t.DBPath, &t.Version, &t.PrevVersion, &t.App, &t.Pool, |
| &t.Region, &t.Channel, &hib, &can, &t.Replication, &t.NextDue, &t.Backing, |
| &sn, &plat, &t.DNSStatus, &t.DNSCheckedAt, &t.CertExpiresAt, &t.Account, |
| - &t.Environment, &t.CreatedAt) |
| + &t.Environment, &prov, &t.Seed, &t.CreatedAt) |
| t.Hibernate = hib != 0 |
| t.Canary = can != 0 |
| t.StatusNamed = sn != 0 |
| t.Platform = plat != 0 |
| + t.Provisioned = prov != 0 |
| return t, err |
| } |
| |
| @@ -442,7 +460,7 @@ func (r *Registry) Upsert(t Route) error { |
| } |
| _, err := r.sql.Exec(` |
| INSERT INTO routes (`+routeCols+`) |
| -VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |
| +VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |
| ON CONFLICT(host) DO UPDATE SET |
| slug=excluded.slug, kind=excluded.kind, socket=excluded.socket, |
| addr=excluded.addr, status=excluded.status, last_error=excluded.last_error, |
| @@ -455,12 +473,14 @@ ON CONFLICT(host) DO UPDATE SET |
| platform=excluded.platform, dns_status=excluded.dns_status, |
| dns_checked_at=excluded.dns_checked_at, |
| cert_expires_at=excluded.cert_expires_at, account=excluded.account, |
| - environment=CASE WHEN excluded.environment = '' THEN routes.environment ELSE excluded.environment END`, |
| + environment=CASE WHEN excluded.environment = '' THEN routes.environment ELSE excluded.environment END, |
| + provisioned=excluded.provisioned, seed=excluded.seed`, |
| t.Host, t.Slug, t.Kind, t.Socket, t.Addr, t.Status, t.LastError, |
| t.DBPath, t.Version, t.PrevVersion, t.App, t.Pool, t.Region, t.Channel, |
| b2i(t.Hibernate), b2i(t.Canary), t.Replication, t.NextDue, t.Backing, |
| b2i(t.StatusNamed), b2i(t.Platform), t.DNSStatus, t.DNSCheckedAt, |
| - t.CertExpiresAt, t.Account, t.Environment, t.CreatedAt) |
| + t.CertExpiresAt, t.Account, t.Environment, b2i(t.Provisioned), t.Seed, |
| + t.CreatedAt) |
| return err |
| } |
| |
| @@ -687,6 +707,27 @@ func (r *Registry) SetNextDue(host string, due int64) error { |
| return err |
| } |
| |
| +// SetSeed stamps a route's first-boot init argv as a narrow UPDATE — same |
| +// rationale as SetAccount (a read-modify-write Upsert racing a deploy's |
| +// SetVersions or the activator's SetNextDue would clobber it). Unlike |
| +// SetAccount it errors when no row matched (the SetChannel contract): the |
| +// instance-provisioning reconcile pass re-stamps this every tick and must |
| +// notice a row that vanished under it rather than silently doing nothing. |
| +func (r *Registry) SetSeed(host, seed string) error { |
| + res, err := r.sql.Exec(`UPDATE routes SET seed = ? WHERE host = ?`, seed, host) |
| + if err != nil { |
| + return err |
| + } |
| + n, err := res.RowsAffected() |
| + if err != nil { |
| + return err |
| + } |
| + if n == 0 { |
| + return fmt.Errorf("no route for host %q", host) |
| + } |
| + return nil |
| +} |
| + |
| // DueInstances returns hibernatable instance routes whose next_due has |
| // arrived (0 means no schedule and never matches). |
| func (r *Registry) DueInstances(now int64) ([]Route, error) { |