Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Upgrading

Two things change independently and have different procedures. Confusing them is how a deploy goes wrong.

ChangingProcedureDowntime
The application modelPOST /_admin/model/reloadNone
The database schemaruntime db apply, then a deployNone
The runtime itselfA rolling deployNone

Reloading a model

If a change needs no schema change — a role’s constraint, a flow’s script, a metric, a validation rule — reload it in place:

$ curl -X POST localhost:9090/_admin/model/reload
{"outcome":"reloaded","model":"northwind","version":"0.1.0","flows":1}

It re-reads the bundle the process was started with. It takes no request body, and that is the security design rather than an omission: the admin listener has no authentication, and an endpoint that accepted a model — flow scripts included, which execute — would be remote code execution behind a health port. Re-reading a mounted path bounds the endpoint to whoever already controls that mount.

Every refusal leaves the running model untouched:

ResponseMeaning
200Swapped. The next request uses it.
409The model would change the database schema. Not swapped.
422The bundle does not validate, or a script does not compile. The diagnostics are in the body.

A reload is atomic across the model and its compiled flows: a request that read the new model and the old flows would be running yesterday’s scripts against today’s entities, and there is no window in which that can happen.

Changing the schema

runtime db plan myapp    # what would change
runtime db apply myapp   # do it

db plan is safe against production and is what a reviewer reads before a deploy — it names every change that would take a lock or lose data.

Additive changes are the default. Anything destructive needs --allow-drop, and even then a dropped column is renamed out of the way rather than removed, so the data is recoverable. If the runtime cannot tell a rename from a delete-plus-add it refuses, and no flag overrides that: --allow-drop means “I meant to lose this”, and a refusal means it cannot tell what you meant.

Dropped columns accumulate

--allow-drop renames a column out of the way rather than removing it: fax becomes _dropped__fax__v2, nullable, with the data intact. That is what makes a destructive change recoverable.

Nothing ever reclaims them. That is deliberate — the moment the runtime is willing to delete one, the guarantee is gone — but it does mean somebody eventually drops them by hand, and that somebody is you:

SELECT table_schema, table_name, column_name
FROM information_schema.columns
WHERE column_name LIKE '\_dropped\_\_%';

They cost a little storage and nothing else. Reclaim them on your own schedule, once you are certain the data is not wanted.

Rolling deploys

They work because migrations are additive, and nothing else makes them survivable: during a roll, two model versions run against one database.

The order that works:

  1. runtime db apply — additive, so the old version keeps working.
  2. Roll the new version out.

A pod serving a model the database has not been provisioned for fails readiness and takes no traffic. That is what makes the deploy wait rather than serve errors, and it is why the model check is in readiness rather than being a startup assertion.

Make sure terminationGracePeriodSeconds is above 35 before you rely on any of this — see Health and shutdown.

What readiness does not notice

It compares the loaded model against the one the database was last provisioned from — not against the live catalogue. So it catches the rolling-deploy case, which is the one that matters, and it does not catch a column somebody dropped by hand with psql.