You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds two nullable columns to sandboxes for the node's attribution of a
sandbox's last stop. #1520 writes and reads them.
stop_cause text: why the sandbox stopped when no one asked it to
(pod_terminated or vm_exited today). It is NULL for a requested stop.
The labels belong to the writer's enum, so there is no CHECK.
stop_cause_at timestamptz: when the node stopped the sandbox, by the
node's clock. It is set for a requested stop too. Both columns NULL means no
attribution is recorded.
lifecycle_changed_at can't stand in for stop_cause_at. It is the control
plane's time of the convergence, one publication after the stop, and later
lifecycle changes, an evacuation included, move it.
A BEFORE UPDATE trigger, sandboxes_clear_stop_cause, clears both columns
when the lifecycle changes to started or paused, meaning the sandbox runs
again. Every other move keeps them, including a stopped sandbox's evacuation
(stopped → restoring → stopped) and a delete. It follows the three sandboxes_clear_* triggers.
Nothing reads or writes the columns until #1520 lands. This revision changes
only comments: the column comment describes the node's attribution, and the
down migration names internal-api.
Severity: does not apply on its own; it enables #1520.
Risk: ALTER TABLE ... ADD COLUMN with no default doesn't rewrite the table,
but it takes a brief ACCESS EXCLUSIVE lock on the hot sandboxes table.
The file sets SET LOCAL lock_timeout = '1s' and statement_timeout = '5s', so a lost lock race fails fast with 55P03,
and the migrator retries it.
The trigger costs one lifecycle comparison per row update, like the three sandboxes_clear_* triggers.
Rollout
Land and deploy this before #1520. #1520's API selects the new columns in its
shared sandbox column list, on every sandbox read, so it fails against a
database without them.
An API without #1520 never reads the columns, and nothing writes them, so this
change does nothing on its own.
Rollback: leave the columns in place; they are inert without #1520. Run the
down migration only once neither #1520's API nor its internal-api is
deployed:
the API selects the columns on every sandbox read;
internal-api writes them inside the node-publish transaction, so dropping
them fails every publication from a node that holds a stopped sandbox.
The migrator doesn't run down migrations on deploy, so a rollback is a
separate, deliberate step.
Stack
#1520 (api: record why a sandbox stopped when its pod or VM died) is stacked
on this and merges after it, and #1521 (ui) on #1520.
AI assistance: Claude Code wrote the migration and this description; a human
reviews them before this pull request opens.
Automatic approval is disabled by the configured approval policy for this pull request. Review comments and requests for changes continue normally.
Commit: d5c1e372398d.
No actionable comments were generated in the recent review.
Walkthrough
The migration adds nullable stop_cause and stop_cause_at columns to public.sandboxes. A trigger clears both fields when the lifecycle changes to started or paused, after trimming and lowercasing the lifecycle value. The down migration removes the trigger, its function, and both columns. The migrations set local lock and statement timeouts.
Merge Risk: High · up to d5c1e
The migration changes which stop attribution a sandbox retains after lifecycle updates. The inspected trigger matches the intended transitions, but this lifecycle-write change warrants validation before merge.
Architecture risk: Low · up to d5c1e
The change affects 1 system.
Changed systems:db
Architecture concerns
No architecture-level concerns identified.
Review details
Systems and components
observed — db (service) was modified; 2 changed files map to changed impact.
Before / after behavior
observed — Modified behavior in db/postgres/20261105163812_sandbox_stop_cause.down.sql: The migration sets local lock and statement timeouts, then removes the stop-cause trigger, function, and columns if present. Comments describe the deployed-service usage condition for running these destructive schema changes.
observed — Modified behavior in db/postgres/20261105163812_sandbox_stop_cause.up.sql: The migration sets local lock and statement timeouts to 1 and 5 seconds, respectively, and adds nullable stop_cause and stop_cause_at columns to public.sandboxes without defaults, backfill, checks, or indexes. The added comments describe the columns’ attribution and timestamp semantics.
observed — Modified behavior in db/postgres/20261105163812_sandbox_stop_cause.up.sql: Adds public.sandboxes_clear_stop_cause() and a BEFORE UPDATE row trigger. When lifecycle changes to started or paused after trimming and lowercasing, the function clears both attribution columns; otherwise it leaves them unchanged.
Pre-merge checks: 5
Check name
Status
Explanation
Title check
The title starts with the lowercase area prefix "db:", uses the lowercase summary "add", and accurately describes the added sandbox stop-cause columns.
Description Follows The Pr Template
The description contains all four required items: Summary, Validation, Severity and risk, and AI assistance. Each has substantive content.
Bug Fixes Name A Regression Test
The PR adds database migrations for new stop attribution fields. It does not fix an application defect under crates/, shim/, or ui/.
Weakened Safeguards Are Justified
The diff adds two migration files. It does not weaken an existing test, assertion, lint, CI check, or repository guard.
Public-Safe Content
The diff, title, and description contain no content that matches the listed failure conditions.
Recent review info
Automatic approval policy
Commit: d5c1e372398d.
Setting
Effective value
Source
Automatic approvals
Disabled
Override 4
Merge risk
Low
Override 2
Security risk
Not set
Global configuration
Matching overrides (numbered as in the resolved configuration):
Any matching disabled override withholds approval, regardless of risk. Otherwise, the strictest explicitly set limit wins for each score. Limits omitted by all matching overrides inherit the global configuration.
Not set means no limit was configured after inheritance. Use @coderabbitai configuration to inspect the resolved settings and their sources.
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 3 remain after this review.
Add nullable sandboxes.stop_cause and stop_cause_at for the cause a node
publishes when it stops a sandbox no one asked to stop, and a trigger that
clears both once the sandbox reaches started or paused again. Nothing
reads or writes them yet; the API change that does depends on this
landing and deploying first, since its shared sandbox column list selects
them on every sandbox read.
The columns now record the node's attribution of a sandbox's last stop,
a requested one included (a NULL cause with its time), written by the
publish ingest in a statement of its own. Say so, and say that the down
migration has to wait for internal-api as well as the API: internal-api
writes the columns inside the node-publish transaction.
Closing: we decided not to add schema columns to record a stop's cause. The value doesn't justify a new column, a trigger on sandboxes, and the ingest rule. The branch stays in place in case we revisit.
The reason will be displayed to describe this comment to others. Learn more.
[Consider] Enforce the pairing the comment above defines: a stop_cause always comes with its stop_cause_at (a requested stop is (NULL, time), no attribution is (NULL, NULL), and (label, NULL) is never valid).
Why: #1520's reader (stop_cause_response) needs both (sandbox.stop_cause_at?), so a row with a label and no time would silently read as a plain stop. The ingest only writes rows where stop_cause_at IS NOT NULL today, but the schema is the one place that holds this for every writer, including manual fixes and future verbs. Skipping a CHECK on the labels makes sense (the writer owns the enum); this one is about the shape of the pair, not the labels.
Add it to the same ALTER TABLE as NOT VALID, so it takes no extra lock and doesn't scan the table under the 5s statement_timeout. The columns are new and all NULL, so there's nothing to validate. The down migration's DROP COLUMN drops it.
Suggested change
-- Nullable with no default, backfill, CHECK or index: the labels belong to the
-- writer's enum, and nothing filters on either column.
ALTERTABLEpublic.sandboxes
ADD COLUMN stop_cause text,
ADD COLUMN stop_cause_at timestamp with time zone;
-- Nullable with no default, backfill or index, and no CHECK on the labels:
-- they belong to the writer's enum, and nothing filters on either column. A
-- cause always carries its time.
ALTERTABLEpublic.sandboxes
ADD COLUMN stop_cause text,
ADD COLUMN stop_cause_at timestamp with time zone,
ADD CONSTRAINT sandboxes_stop_cause_has_time
CHECK (stop_cause IS NULLOR stop_cause_at IS NOT NULL) NOT VALID;
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds two nullable columns to
sandboxesfor the node's attribution of asandbox's last stop. #1520 writes and reads them.
stop_cause text: why the sandbox stopped when no one asked it to(
pod_terminatedorvm_exitedtoday). It is NULL for a requested stop.The labels belong to the writer's enum, so there is no
CHECK.stop_cause_at timestamptz: when the node stopped the sandbox, by thenode's clock. It is set for a requested stop too. Both columns NULL means no
attribution is recorded.
lifecycle_changed_atcan't stand in forstop_cause_at. It is the controlplane's time of the convergence, one publication after the stop, and later
lifecycle changes, an evacuation included, move it.
A
BEFORE UPDATEtrigger,sandboxes_clear_stop_cause, clears both columnswhen the lifecycle changes to
startedorpaused, meaning the sandbox runsagain. Every other move keeps them, including a stopped sandbox's evacuation
(
stopped→restoring→stopped) and a delete. It follows the threesandboxes_clear_*triggers.Nothing reads or writes the columns until #1520 lands. This revision changes
only comments: the column comment describes the node's attribution, and the
down migration names internal-api.
Validation
scripts/check-postgres-migrations-isolated.sh: "Postgres migrations changedwithout runtime app changes; migration isolation check passed."
scripts/check-migration-versions.sh: passes.scripts/check-postgres-migrations-squawk.sh: 0 issues.store tests exercise the trigger, against this file:
startedclears it.stopped, theevacuation case fails (
left: None,right: Some("pod_terminated")).applied by
space-db-migrate.Severity and risk
Severity: does not apply on its own; it enables #1520.
Risk:
ALTER TABLE ... ADD COLUMNwith no default doesn't rewrite the table,but it takes a brief
ACCESS EXCLUSIVElock on the hotsandboxestable.SET LOCAL lock_timeout = '1s'andstatement_timeout = '5s', so a lost lock race fails fast with55P03,and the migrator retries it.
sandboxes_clear_*triggers.Rollout
Land and deploy this before #1520. #1520's API selects the new columns in its
shared sandbox column list, on every sandbox read, so it fails against a
database without them.
An API without #1520 never reads the columns, and nothing writes them, so this
change does nothing on its own.
Rollback: leave the columns in place; they are inert without #1520. Run the
down migration only once neither #1520's API nor its internal-api is
deployed:
them fails every publication from a node that holds a stopped sandbox.
The migrator doesn't run down migrations on deploy, so a rollback is a
separate, deliberate step.
Stack
#1520 (api: record why a sandbox stopped when its pod or VM died) is stacked
on this and merges after it, and #1521 (ui) on #1520.
AI assistance: Claude Code wrote the migration and this description; a human
reviews them before this pull request opens.