Files
ukmesh/docs/db-lifecycle.md
T

160 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DB Lifecycle
## Rules
- `backend/src/db/schema/base.sql`
- only cheap, idempotent base schema work
- `backend/src/db/migrations.ts`
- runs additive versioned migrations
- historical backfills must not run on backend startup
## Use the right layer
### Base schema
Use for:
- table creation
- index creation
- safe `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`
Do not use for:
- whole-table `UPDATE` backfills
- historical recomputation
- data repair
### Migrations
Use for:
- additive schema changes that need one-time application
- constraints or indexes that belong to a versioned rollout
### Backfills / maintenance jobs
Use for:
- recomputing derived packet fields
- rebuilding link tables
- recalculating historical summaries
Run historical work deliberately during a low-traffic window. For example,
after building the current backend image, reconstruct the bounded stats
rollups without restarting the live API:
```bash
docker compose build backend
docker compose run --rm --no-deps backend node dist/tools/backfillStatsRollups.js --apply
```
`backfillStatsRollups` uses independent daily/24-hour slices and monotonic
upserts, so a live ingest write cannot be replaced by an older candidate. It
defaults to the same 31 calendar dates used by the longest-hop API and the
eight-day observer-window retention boundary. It is idempotent; use
`--daily-days` or `--observer-days` only when an operator intentionally wants
a different bounded window.
The hourly chart rollup uses a persisted cursor and a pinned end time. Each
historical hour, its replacement aggregate rows, and its next cursor commit in
one transaction. `--hourly-days` defaults to eight; interruption can therefore
resume without skipping or double-counting a slice.
## History contract and retention inventory
Content-bearing packets have a hard 30-day bound. Timescale drops complete
chunks, while the health worker deletes exact expired rows from the partial
boundary chunk in deterministic 25,000-row batches; new packet chunks are one
day. Content-free path reconstruction reads `packet_paths`, so it continues
after the source packet expires. Status samples retain 180 days and neighbor
samples seven days. Privacy-filtered aggregates, model parameters, current
node/link/coverage state, and privacy state remain longer lived.
Run the exact, read-only inventory before considering compression or deletion:
```bash
docker compose exec backend npm run db:lifecycle
```
It reports exact expired rows, expired/compressed Timescale chunks, relation
bytes, oldest/newest timestamps, and the affected features for every target.
The inventory can be expensive by design; run it away from peak ingest.
Compression and retention are separate, table-at-a-time changes. Both require:
- a named, fresh database backup;
- a successful isolated restore verification from the last 30 days;
- the target in `DATA_LIFECYCLE_RETENTION_TARGETS` for deletion or
`DATA_LIFECYCLE_COMPRESSION_TARGETS` for compression;
- the action flag set to `true`; and
- an exact per-table approval argument.
Example compression rollout:
```bash
DATA_LIFECYCLE_COMPRESSION_ENABLED=true
DATA_LIFECYCLE_COMPRESSION_TARGETS=packets,packet_paths,node_status_samples,node_neighbor_samples
DATA_LIFECYCLE_BACKUP_REFERENCE=backup-20260729
DATA_LIFECYCLE_RESTORE_VERIFIED_AT=2026-07-29T12:00:00Z
docker compose exec backend npm run db:lifecycle -- \
--apply-compression --target=packets \
--approve=apply-data-lifecycle-compression-packets
```
Measure query CPU, ingest WAL, and storage after cold-chunk compression. Only
after aggregate cutover and another inventory may retention be enabled:
```bash
docker compose exec backend npm run db:lifecycle -- \
--apply-retention --target=packets \
--approve=apply-data-lifecycle-retention-packets
```
Hypertable retention uses a Timescale policy. Row-table deletion is bounded and
performed by the health worker only for targets explicitly listed in
`DATA_LIFECYCLE_RETENTION_TARGETS`. Failed/pending owner alert deliveries are
not discarded while they remain retryable.
Compression state is migration-defined: `packets`, `packet_paths`, and
`node_status_samples` compress after 14 days (matching the reviewed live
setting), while seven-day `node_neighbor_samples` compress after one day.
`packet_paths` is compression-only and is rejected by the retention registry.
At the audited 10,00023,000 path rows/day and roughly 1 kB/uncompressed row,
uncompressed growth would be about 3.68.4 GB/year; alert if the 30-day row-rate
or bytes/row exceeds that reviewed band, or if any path chunk older than 15 days
is uncompressed.
Owner/private packet content uses the same 30-day hard boundary. An owner export
must be completed before enabling deletion if older raw evidence is required.
Removing raw data is irreversible without the named restore; turning the flag
off stops future policy runs but does not recreate deleted chunks.
## Startup guarantee
Backend startup should be safe against a production-sized database. If a change can lock or scan large tables, it does not belong in startup schema init.
## Compose deployment
`docker compose up -d --build` runs the one-shot `db-migrate` service after
TimescaleDB becomes healthy and before the backend starts. It applies only
unrecorded files from `backend/src/db/migrations/`; after a successful run it
exits with no changes on later deploys.
Existing production services keep `DATABASE_SKIP_SCHEMA_INIT=true`, so they do
not repeat base-schema DDL during ordinary startup. For a manual migration run,
use `docker compose run --rm db-migrate` and inspect its output before starting
new application containers.
## Production network-label cutover
The historical `teesside` and `northeast` labels remain read-compatible until a deliberate cutover. Start with a non-mutating audit:
```bash
scripts/unify-networks.sh audit
```
Before applying, stop or upgrade every writer, create and verify a database/volume snapshot, and record its identifier. The apply command refuses to run if a legacy-labelled packet arrived in the last 15 minutes, if confirmation is absent, or if no backup reference is supplied:
```bash
CONFIRM_NETWORK_UNIFICATION=ukmesh \
BACKUP_REFERENCE='snapshot-2026-07-11T1600Z' \
scripts/unify-networks.sh apply
```
The workflow preserves sighting intervals, updates status history in 50,000-row commits, rewrites packet chunks individually, restores the prior compression-policy state after interruption, and records progress in `network_unification_runs`. Re-running with the same `NETWORK_UNIFICATION_RUN_ID` is safe. Run `scripts/unify-networks.sh verify` after any interrupted maintenance.
The relabel discards the distinction between historical production labels and is not logically reversible. Rollback means stopping all writers, restoring the snapshot named by `BACKUP_REFERENCE`, restoring the matching application version, and only then reopening ingest. Do not attempt a reverse `UPDATE`: the original label cannot be reconstructed reliably after unification.