Skip to content

Index-building migrations run silently at boot with no startupProbe to survive them #337

Description

@FrameAutomata

Raised while measuring #335. Not folded into that PR — the exposure is created by the migration mechanism and the Helm chart, not by any one migration.

What happens now

runMigrationsOn (app/migrations/migrations.go:60-76) executes each statement as a bare target.Exec: no transaction, no timing, no logging. migrations.Run is called synchronously from cmd/run.go before the HTTP server binds, so a slow statement is boot latency with zero operator feedback — the process looks hung.

Index builds are the slow statements. Measured on a 2M-row / 8.1GB telemetry database (#335): a single CREATE INDEX on exception_stack_traces took ~10s, and the DROP before it another ~2s.

Why it is worth fixing now

#334 (0020) builds three indexes and #335 (0021) builds a fourth. An operator upgrading across both runs all four in one boot, on the same database, before anything listens on a port.

helm/traceway/templates/deployment.yaml:54-61 has no startupProbe — verified, there is no startupProbe anywhere in helm/. There is only:

livenessProbe:
  initialDelaySeconds: 10
  periodSeconds: 30
  failureThreshold: 3

That is roughly a 70-100s budget from container start before the kubelet kills the pod. Four index builds on a large database plus normal startup can plausibly reach it.

The failure is not self-correcting. The version row is recorded by a separate Exec after the statements (migrations.go:73), outside any transaction, so a kill mid-CREATE INDEX never records the version. The pod restarts, re-runs the same build, and is killed again at the same point. Both statements are idempotent so there is no corruption — but the crash-loop does not resolve on its own, and it looks like a failing image rather than a slow migration.

Related smaller hole: 0021's DROP and CREATE are separate statements with no transaction, so for the seconds between them the table carries no (project_id, exception_hash) index at all. Only reachable if the process dies in that window, and the rerun repairs it.

What "done" looks like

Roughly in cost order:

  1. config.Logf per migration in runMigrationsOn, with elapsed time. Turns a silent stall into a log line. Cheapest thing here and useful regardless of the rest.
  2. A startupProbe in the Helm chart, with a failureThreshold sized for a large-database migration, so the liveness budget only starts after boot completes. This is what startupProbe is for.
  3. Optionally reconsider recording the version in the same transaction as the statements, so a killed migration is atomic. Note SQLite DDL is transactional, so this is feasible — but it changes behaviour for every migration, not just index builds, so it deserves its own look.

Docker/compose deployments have no equivalent probe and just see a slow start, so (1) is the fix that helps every deployment shape.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions