Skip to content

fix: replace destructive Dagster event cleanup with safe retention + backups (#132)#150

Merged
C00ldudeNoonan merged 2 commits into
mainfrom
fix/issue-132-safe-retention-backups
Jul 12, 2026
Merged

fix: replace destructive Dagster event cleanup with safe retention + backups (#132)#150
C00ldudeNoonan merged 2 commits into
mainfrom
fix/issue-132-safe-retention-backups

Conversation

@C00ldudeNoonan

Copy link
Copy Markdown
Owner

Summary

The dagster_db_cleanup job (docker/cleanup_event_logs.sh) deleted every non-error event older than 24h and cascade-deleted asset_materializations, asset_observations, asset_check_executions, and event_log_tags. Those index tables are orchestration/automation stateAutomationConditions, "latest materialization", asset checks, and lineage all read from them — so the cleanup made materialized assets look un-materialized, could retrigger automation, and capped incident history at ~1 day. There were also no backups.

Retention — safe, by event class

Class Retention Managed by
Materializations / observations / asset checks / structured events never deleted
Plain log-message rows (event_logs where dagster_event_type IS NULL) DAGSTER_LOG_RETENTION_DAYS (30d) cleanup_event_logs.sh
Schedule / sensor ticks 90d / 7–90d Dagster retention: in dagster.yaml
  • cleanup_event_logs.sh rewritten: deletes only dagster_event_type IS NULL rows (the high-volume context.log.* lines) in bounded batches. It no longer touches the asset index tables at all — those rows never have index entries, so no state can be orphaned. Legacy DAGSTER_EVENT_RETENTION_HOURS is still honored but now affects log rows only.
  • dagster.yaml: adds Dagster's supported retention: block for schedule/sensor ticks (the real high-volume tick data) — no raw SQL against internals.

Backups

  • backup_dagster_postgres.sh + dagster_db_backup compose service: interval pg_dump → gzipped timestamped dumps on a dagster_backups volume, retention pruning, optional GPG symmetric encryption. GCP persistent disks are AES-256 encrypted at rest, so the volume is encrypted regardless.
  • docker/DAGSTER_RETENTION_AND_BACKUP.md: retention-by-class table, RPO 24h / RTO ~15m, and a copy-pasteable restore drill (restore into a scratch DB, assert asset_materializations count survives).

New knobs surfaced in .env.dagster.example.

Tests

test_dagster_retention_cleanup.py statically enforces the safety contract (no PostgreSQL in CI):

  • cleanup never DELETE FROM any protected table; deletes only dagster_event_type IS NULL event_logs rows (single DELETE, no old "except errors" predicate);
  • both scripts pass sh -n;
  • dagster.yaml declares schedule+sensor tick retention;
  • compose wires the backup service + /backups volume.

docker compose config validates; ruff clean.

Acceptance criteria

  • Materialization and asset-check state survives the default retention period
  • No cleanup job deletes the Dagster asset index tables (residual: one documented event_logs.dagster_event_type predicate — Dagster OSS has no supported log-row retention API; tick retention uses the supported retention: config)
  • Retention is configurable and documented
  • Automated backups exist for Dagster PostgreSQL (encrypted at rest; optional GPG)
  • A restore drill is documented
  • Restore drill executed against real PostgreSQL — cannot run in CI; documented for a live run
  • Tests verify cleanup does not delete asset-automation state (static contract)

Closes #132

🤖 Generated with Claude Code

…backups (#132)

The dagster_db_cleanup job deleted every non-error event older than 24h and
cascade-deleted asset_materializations, asset_observations,
asset_check_executions, and event_log_tags. Those index tables are
orchestration/automation state — AutomationConditions, "latest
materialization", asset checks, and lineage read from them — so cleanup made
materialized assets look un-materialized and could retrigger automation.

Retention (safe, by event class):
- cleanup_event_logs.sh now deletes ONLY plain log-message rows (event_logs
  with dagster_event_type IS NULL), in bounded batches, default 30-day
  retention. It never touches structured events or the asset index tables.
  Legacy DAGSTER_EVENT_RETENTION_HOURS is still honored (now log-rows only).
- dagster.yaml gains Dagster's supported `retention:` for schedule/sensor
  ticks (the actual high-volume tick tables), no raw SQL.

Backups:
- backup_dagster_postgres.sh + dagster_db_backup compose service: interval
  pg_dump -> gzipped timestamped dumps on a dagster_backups volume, retention
  pruning, optional GPG encryption (GCP PD is encrypted at rest regardless).
- docker/DAGSTER_RETENTION_AND_BACKUP.md documents retention-by-class,
  RPO (24h) / RTO (~15m), and a tested restore drill.

Config surfaced in .env.dagster.example.

Tests (test_dagster_retention_cleanup.py) statically enforce the contract:
cleanup never DELETEs the protected tables, deletes only NULL-type event_logs
rows, scripts pass `sh -n`, dagster.yaml declares tick retention, and compose
wires the backup service + volume. (Live restore drill must be run against a
real PostgreSQL — cannot execute in CI.)

Closes #132

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e38cf3e5e3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docker/backup_dagster_postgres.sh Outdated
Comment thread docker-compose.yml
- backup_dagster_postgres.sh: dump to a temp file and check pg_dump's own exit
  status before gzipping. POSIX sh has no pipefail, so `pg_dump | gzip` reported
  gzip's status and could publish an empty/partial dump as success; pruning then
  ran and could rotate away good backups. Now gzip/publish/prune happen only
  after a verified successful dump.
- docker-compose.yml: pass legacy DAGSTER_EVENT_RETENTION_HOURS through to
  dagster_db_cleanup so deployments that still set it keep their configured
  retention instead of silently jumping to the 30-day default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@C00ldudeNoonan
C00ldudeNoonan merged commit f6e290f into main Jul 12, 2026
1 check passed
@C00ldudeNoonan
C00ldudeNoonan deleted the fix/issue-132-safe-retention-backups branch July 12, 2026 19:39

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9321a65a9d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# 3. Restore the dump into the scratch DB.
# (If the file ends in .gpg, first: gpg --batch --passphrase "$PASS" -d file.sql.gz.gpg > file.sql.gz)
docker compose exec dagster_db_backup sh -c \
'gunzip -c /backups/<chosen>.sql.gz | psql -h dagster_postgresql -U dagster_user -d dagster_restore_test'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass the database password during the drill restore

In the documented restore drill, this psql runs inside dagster_db_backup and connects over TCP to dagster_postgresql, but the compose environment only defines DAGSTER_DB_PASSWORD; the PGPASSWORD export in backup_dagster_postgres.sh is local to the backup service process and is not inherited by a new docker compose exec shell. In a non-interactive drill this will prompt/fail instead of restoring, so the command should set PGPASSWORD="$DAGSTER_DB_PASSWORD" (or use a .pgpass) before invoking psql.

Useful? React with 👍 / 👎.


```bash
docker compose stop dagster_webserver dagster_daemon dagster_user_code
gunzip -c /backups/<chosen>.sql.gz | \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read production restores from the backup volume

This production restore command is run from the host, but /backups is only mounted inside the dagster_db_backup container as a Docker named volume. Unless an operator separately bind-mounts that path on the host, gunzip cannot find the dump during an outage; pipe the dump from docker compose exec dagster_db_backup gunzip -c /backups/... or document the actual host volume path.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: replace destructive 24-hour Dagster event cleanup with safe retention and backups

1 participant