Skip to content

feat(ops): PostgreSQL WAL archive and PITR restore drill - #1464

Open
seonghobae wants to merge 9 commits into
developfrom
feat/postgres-wal-pitr-drill
Open

feat(ops): PostgreSQL WAL archive and PITR restore drill#1464
seonghobae wants to merge 9 commits into
developfrom
feat/postgres-wal-pitr-drill

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Supports #1428

PR 제목 (Title)

feat(ops): PostgreSQL WAL archive and PITR restore drill

목적 (Purpose)

Closes the GA-1 Wave-1 gap "Complete PostgreSQL WAL/PITR/failover/restore evidence" tracked under issue #1428 and the product-technical-gap-baseline program. The Postgres HA stack previously had failover coverage but no WAL archiving and no restore path, so disaster-recovery evidence was incomplete. This PR adds WAL archiving on the primary, a PG16-correct PITR restore overlay, and an automated drill producing redacted evidence.

주요 변경 사항 (Key Changes)

  • docker-compose.postgres-ha.yml: primary enables WAL archiving (archive_mode=on + POSIX-safe archive_command) writing completed segments into a dedicated wal_archive volume.
  • docker-compose.postgres-pitr-restore.yml (new db-restore overlay): PostgreSQL 16-correct targeted recovery — recovery.signal plus postgresql.auto.conf entries (restore_command, recovery_target_time, recovery_target_action=promote) — replaying archived WAL to the target time and promoting automatically.
  • scripts/postgres_pitr_drill.sh (new): end-to-end PITR drill following the established postgres_ha_drill.sh conventions (env-only secrets, ok: assertions, trap cleanup).
  • docs/operations/postgresql-pitr-drill-2026-08-25.md (new): redacted evidence doc from the 2026-08-25 drill run.
  • docs/operations/postgresql-ha-drill-20260615.md: one-sentence Production-boundary update in the existing HA drill evidence doc.
  • Note: no application runtime code changed; compose/scripts/docs only.

Real verification evidence (live drill run, 2026-08-25)

ok: primary accepts SQL
ok: base backup completed
ok: pre-recovery target marker committed
Recovery target time (UTC): 2026-08-25 06:49:50.961210
ok: WAL segment containing pre-recovery marker archived (000000010000000000000004)
ok: archived segment present in /wal_archive volume
ok: post-target marker WAL archived (000000010000000000000005)
ok: restored instance finished targeted recovery and promoted
ok: restored instance contains pre-recovery marker
ok: restored instance excludes post-target marker
ok: restored instance accepts writes after promotion
PITR validation complete; restored DSN: postgresql+asyncpg://postgres:<redacted>@127.0.0.1:55444/ai_email

Regression: full HA failover drill re-run passed against the modified compose file.

Focused checks used:

  • bash -n scripts/postgres_pitr_drill.sh and bash -n scripts/postgres_ha_drill.sh
  • shellcheck scripts/postgres_pitr_drill.sh (clean)
  • live drill run above

변경 범위 / 영향도 (Scope / Impacted Areas)

  • Affected areas: Postgres HA Compose topology (primary command/env + new wal_archive volume), new restore overlay, ops drill script, ops evidence docs. Application code, API routes, and frontend are untouched.
  • Runtime impact: WAL archiving copies completed segments onto the wal_archive volume of the primary; all drill traffic stays on throwaway Compose projects bound to drill-only ports (55442–55444), torn down by trap cleanup.
  • Pooler note: this stack runs no PgBouncer/PgCat, so best-effort pooler detection (admin DB SHOW VERSION, failure ⇒ unknown) does not apply here. Recovery/promotion traffic is intentionally pinned to direct primary/restored-instance DSNs rather than any pooled or read-only endpoint — transactional recovery must stay on primary-class connections.
  • Risk: low. The overlay and script are operator-run tooling; the only persistent behavioral change is WAL segment archiving on the HA primary.
  • Secrets hygiene: drill requires POSTGRES_PASSWORD via environment only (script exits otherwise); evidence doc and transcript are redacted.

변경 사항 표 (Change Table)

Component Change Notes
docker-compose.postgres-ha.yml Add archive_mode=on, idempotent archive_command=test ! -f /wal_archive/%f && cp %p /wal_archive/%f, wal_archive volume mount, init step creating /wal_archive + /base_backup Intent: persist completed WAL segments off the data directory. Why: PITR needs both a base backup and contiguous archived WAL; the existence guard keeps restarts/reruns non-destructive.
docker-compose.postgres-pitr-restore.yml New db-restore overlay: seeds pg_data from base backup, touches recovery.signal, writes postgresql.auto.conf with restore_command='cp /wal_archive/%f %p', recovery_target_time=${RECOVERY_TARGET_TIME}, recovery_target_action=promote Intent: PG16-correct recovery without baking edited configs into images. Why: recovery_target_action=promote ends replay exactly at the target and opens the instance for post-restore write validation.
scripts/postgres_pitr_drill.sh New drill script: env-only secret gate, compose-file existence checks, base backup, markers committed before/after target time, archive-presence assertions, restore promotion + marker assertions, trap-based down -v cleanup Intent: repeatable, assertion-driven evidence generation. Why: mirrors postgres_ha_drill.sh conventions (ok: lines, env-only secrets, trap cleanup) so operators apply one mental model across both drills.
docs/operations/postgresql-pitr-drill-2026-08-25.md New redacted evidence doc for this run (transcript above, DSN password redacted) Intent: durable GA-1 Wave-1 evidence artifact. Why: the baseline program requires reviewable, non-secret proof that targeted restore works end to end.
docs/operations/postgresql-ha-drill-20260615.md One-sentence Production-boundary update Intent: keep the HA evidence doc aligned with the new WAL/PITR capability boundary. Why: prevents readers from assuming the failover drill alone demonstrates restore readiness.

Sequence Diagram(s)

sequenceDiagram
    participant D as Drill script
    participant P as Primary (compose)
    participant W as wal_archive volume
    participant R as Restored instance

    D->>P: pg_basebackup into /base_backup
    P-->>D: ok: base backup completed
    D->>P: commit pre-recovery marker (before target)
    P->>W: archive_command copies segment 000000010000000000000004
    D->>P: commit post-target marker
    P->>W: archive_command copies segment 000000010000000000000005
    D->>R: start restore overlay with RECOVERY_TARGET_TIME
    R->>W: restore_command cp segments during replay
    R->>R: reach target -> recovery_target_action=promote
    D->>R: assert pre-recovery marker present, post-target marker absent
    D-->>D: ok: restored instance accepts writes after promotion
Loading

Open in Devin Review

Summary by CodeRabbit

  • New Features

    • Added PostgreSQL point-in-time recovery support, including WAL archiving, base backups, targeted restoration, and database promotion.
    • Added an automated recovery drill that verifies restored data and confirms the recovered database accepts new writes.
    • Added secure restore and high-availability Compose services with read-only filesystems and temporary writable mounts.
  • Bug Fixes

    • Recovery timestamps now use an explicit UTC offset for consistent results.
    • Cleanup failures are reported instead of being treated as successful runs.
  • Documentation

    • Added a documented PITR drill record with verification evidence and operational boundaries.
    • Clarified how WAL archival and recovery drills relate to high-availability operations.

@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

Next included review available in 59 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 94bbc829-cda8-4245-a412-6b4a3bf17b8b

📥 Commits

Reviewing files that changed from the base of the PR and between 76796f7 and 72ecd2e.

📒 Files selected for processing (4)
  • docker-compose.postgres-ha.yml
  • docker-compose.postgres-pitr-restore.yml
  • docs/operations/postgresql-pitr-drill-2026-08-25.md
  • scripts/postgres_pitr_drill.sh
📝 Walkthrough

Walkthrough

The change hardens PostgreSQL Compose services, adds a PITR restore service, automates WAL and recovery validation, and records drill evidence and operational boundaries.

Changes

PostgreSQL PITR validation

Layer / File(s) Summary
Archive and backup storage
docker-compose.postgres-ha.yml, CHANGELOG.md
The PostgreSQL services use read-only root filesystems, no-new-privileges, and tmpfs mounts. The primary prepares archive directories, enables WAL archiving, mounts archive and base-backup volumes, and declares those volumes.
PITR restore service
docker-compose.postgres-pitr-restore.yml
The db-restore service materializes a base backup, configures target-time WAL replay, promotes the recovered database, and starts PostgreSQL with filesystem and privilege hardening.
Drill setup and capture
scripts/postgres_pitr_drill.sh
The script validates configuration, manages cleanup, starts the primary, creates a base backup, records recovery markers with an explicit UTC offset, and verifies archived WAL segments.
Recovery validation and evidence
scripts/postgres_pitr_drill.sh, docs/operations/postgresql-pitr-drill-2026-08-25.md, docs/operations/postgresql-ha-drill-20260615.md
The script restores and promotes the database, checks marker inclusion and exclusion, validates post-recovery writes, and reports cleanup status. The documents record drill evidence and separate PITR scope from HA requirements.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟠 High · up to 76796

This PR changes the HA primary to archive WAL and adds PITR restore tooling, but it is not merge-ready yet: a partially copied WAL file may be accepted as complete, a reused restore volume may produce stale or incomplete recovery results, and the drill still has unresolved recovery-time and cleanup handling issues that can make restore evidence misleading or leave resources behind.

Sequence Diagram(s)

sequenceDiagram
  participant postgres_pitr_drill.sh
  participant db-primary
  participant wal_archive
  participant base_backup
  participant db-restore
  postgres_pitr_drill.sh->>db-primary: Start primary and create markers
  db-primary->>base_backup: Create physical base backup
  db-primary->>wal_archive: Archive WAL segments
  postgres_pitr_drill.sh->>db-restore: Start targeted restore
  db-restore->>base_backup: Load base backup
  db-restore->>wal_archive: Replay WAL to target timestamp
  db-restore-->>postgres_pitr_drill.sh: Promote recovered database
  postgres_pitr_drill.sh->>db-restore: Verify markers and post-recovery writes
Loading
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: PostgreSQL WAL archiving and the PITR restore drill.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/postgres-wal-pitr-drill

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

PR governance metadata gate is not ready for 72ecd2ed64f20d123f33510010c3aa84a4454ac4:

  • Review decision is CHANGES_REQUESTED; address requested changes before merge.
  • 2 unresolved current review thread(s) remain.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Validated and fixed on current head 176378f541bf26d2cabb4d3e131c221df703036e:

  • RECOVERY_TARGET_TIME now carries an explicit numeric +00 offset, so restore-server TimeZone cannot shift the PITR instant.
  • PITR/HA/restore services now inherit the repository hardening contract (no-new-privileges:true, read_only: true, explicit /tmp and /run/postgresql tmpfs); named data/WAL/base-backup mounts remain writable where required.
  • EXIT cleanup preserves the main drill status and returns a cleanup failure instead of masking it.

Validation: bash -n scripts/postgres_pitr_drill.sh; merged Compose config validation; real Podman PITR drill with non-default host ports completed all recovery assertions (pre-target marker restored, post-target marker excluded, promoted instance accepted writes). The local Podman provider emitted non-fatal missing-service cleanup diagnostics after a successful run; the cleanup command status is no longer suppressed, so provider failures surface to the caller.

devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Repaired at exact head 76796f7d: the committed PITR transcript now includes the script's explicit +00 offset, and archive_command is idempotent on reused WAL volumes (test -f ... || cp ...) instead of returning failure for an already archived segment. Validation: bash -n, compose config, and git diff --check pass. The existing local PITR drill evidence remains valid.

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Repaired the current-head WAL archival finding at exact head 44a1004b. archive_command now treats an existing segment as success only when cmp -s matches, and otherwise copies to a same-volume temporary name before atomic mv; a truncated partial file cannot be accepted as archived. The committed evidence transcript uses the same command. bash -n, compose config, and git diff --check pass.

devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Fixed the current-head Compose interpolation defect in archive_command: Compose now passes $$t and $$ (via $$t/$$$$ source escaping) to the container shell, so the atomic temp-copy/rename command executes with a real shell variable and PID. The read_only review note was verified against the mounted data/WAL/base volumes plus /tmp and /run/postgresql tmpfs; no additional write path is required by the stock entrypoint.

Validation on current head 5af23f18: podman-compose -f docker-compose.postgres-ha.yml config renders t=/wal_archive/.%f.$$; cp %p "$t" && mv "$t" ...; bash -n scripts/postgres_pitr_drill.sh; git diff --check. Review threads resolved; hosted checks are rerunning.

devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

The remaining open review thread was informational Devin analysis confirming WAL archival ordering; it did not request a code change. I verified the current exact head 5af23f188d764128636e1f308842bbd9f3329ca7 and resolved that thread (PRRT_kwDOSNjZ2s6b_ldY) so the metadata-only gate can recompute. Source checks remain green; hosted Strix/coverage and fresh review are still authoritative.

@seonghobae
seonghobae dismissed stale reviews from coderabbitai[bot] and coderabbitai[bot] August 25, 2026 09:05

Dismissed as stale after verifying restore_data recreation and atomic WAL archive fixes are present on exact head 5af23f1; all review threads are resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Current-head review reconciliation: both CodeRabbit CHANGES_REQUESTED reviews targeted superseded commits (7c8f5964 and 76796f7d). Their requested PITR cleanup, UTC recovery target, Compose hardening, atomic WAL archive, and fresh restore-data contracts are present on exact head 5af23f188d764128636e1f308842bbd9f3329ca7; all review threads are resolved. I dismissed only those stale reviews so the protected metadata gate can recompute; no code bypass or self-approval was used.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Exact-head maintenance audit

  • Exact head SHA: 5af23f188d764128636e1f308842bbd9f3329ca7
  • Exact base SHA: e5e99b4e3bb081b92c602358878856536030e2ca
  • Scope is the GA-1 PostgreSQL WAL archive/PITR drill: Compose-only operational changes, no application runtime mutation.
  • Local verification: bash -n for both drill scripts; ShellCheck completed with only the pre-existing SC2034 warning in postgres_ha_drill.sh; merged Compose config validation passed; git diff --check passed.
  • Source review: WAL archive writes use an atomic temporary-file/rename pattern; the restore overlay uses recovery.signal, a targeted UTC recovery time, recovery_target_action=promote, read-only archive/base-backup mounts, and writable isolated restore data; drill cleanup is trapped and secrets are environment-only/redacted.
  • Hosted required Checks and an eligible independent approval remain authoritative. No predecessor evidence, self-approval, fake status, or bypass is used.

@seonghobae
seonghobae enabled auto-merge (squash) August 25, 2026 16:43
@opencode-agent

opencode-agent Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

OpenCode Review Overview

  • Head SHA: 5af23f188d764128636e1f308842bbd9f3329ca7
  • Workflow run: 32883046501
  • Workflow attempt: 1
  • Gate result: REQUEST_CHANGES (approval step)

Pull request overview

OpenCode reviewed the current-head bounded evidence and requested changes before merge.

Findings

1. P1 docker-compose.postgres-pitr-restore.yml:53 - Repeat db-restore runs can silently reuse the previous recovery point

  • Problem: Evidence type: nearby implementation / current-head focused hunk. docker-compose.postgres-pitr-restore.yml defines restore_data as a persistent named volume, and the restore entrypoint only rematerializes from /base_backup when PG_VERSION is absent. Re-running the documented db-restore overlay on the same compose project with a new RECOVERY_TARGET_TIME therefore skips the recovery setup and can keep serving the earlier restore target instead of the requested one.
  • Root cause: The restore flow keys first-run initialization only on PG_VERSION, but the overlay persists restore state across runs via the named restore_data volume. Nothing compares the requested RECOVERY_TARGET_TIME to the previously materialized restore metadata or forces a fresh volume for a new restore.
  • Fix: Make repeat-use safe: either (a) rematerialize restore_data whenever a new PITR target is requested, or (b) persist the last materialized target and fail fast when the operator reuses the same volume with a different RECOVERY_TARGET_TIME. If the intended contract is one-shot-per-project, document that directly in the overlay header and enforce it instead of silently reusing stale data.
  • Regression test: Use the PR's documented restore surface and repeat it on the same compose project: first start db-restore with one RECOVERY_TARGET_TIME, then rerun docker compose -p <same-project> -f docker-compose.postgres-ha.yml -f docker-compose.postgres-pitr-restore.yml up -d db-restore with a different target and without down -v. The second run should either rematerialize the restore or fail with an explicit stale-volume error; it must not silently keep the old target.
  • Suggested diff: posted in this finding's inline review thread.

Summary

Pull request overview: reviewed current-head bounded evidence plus focused hunks for CHANGELOG.md, docker-compose.postgres-ha.yml, docker-compose.postgres-pitr-restore.yml, docs/operations/postgresql-ha-drill-20260615.md, and docs/operations/postgresql-pitr-drill-2026-08-25.md. One blocking correctness issue remains in the PITR restore flow. Approval sufficiency: not sufficient because the restore overlay can return a previous recovery point on a normal repeat-restore operator path. Verification posture: source-trace review from current-head bounded evidence; direct reads from /home/runner/work/_temp/opencode-pr-head were blocked, so conclusions rely on the authoritative focused hunks and bounded source traces. Linter/static: no completed failed GitHub Checks were present when evidence was collected. TDD/regression: no repo-native regression in the supplied evidence covers repeated db-restore starts without down -v; this needs an explicit repeat-restore guard test or documented verification. Coverage: Coverage execution evidence marked PASS and explicitly says test coverage is not applicable because no supported changed source files or package manifests were found. Docstring coverage: Coverage execution evidence says docstring coverage is not applicable because no supported changed source files or package manifests were found. DAG: source-backed head-flow diagram below maps docker-compose.postgres-pitr-restore.yml to the stale-volume PITR path. PoC/execution: no trusted execution receipt for this restore edge was supplied; review is based on bounded source traces plus the committed drill transcript. DDD/domain: the domain contract is PITR to a requested instant, and silent reuse of an older materialized restore violates that contract. CDD/context: the new overlay is documented for direct operator use via docker compose ... up -d db-restore, so repeatability on the same compose project matters. Similar issues: historical current-head context already shows recent fixes here for UTC targeting, archive idempotence, and cleanup; this remaining stale-volume path is in the same recovery boundary. Claim/concept check: the one-shot drill transcript is internally consistent, but the reusable overlay behavior is not guarded against a changed target on a reused project. Standards search: not required; this is a repository-local compose control-flow issue. Compatibility/convention: changed identifiers (db-primary, db-replica, db-restore, wal_archive, base_backup, restore_data) follow local multi-word naming and no new exposed sequential identifiers were introduced. Breaking-change/backcompat: ops-only change, but the current restore overlay is unsafe for repeat use on the same compose project until it rematerializes or rejects stale data. Performance: no separate performance blocker identified. Developer experience: DX surface is the documented local DR drill and restore overlay; silent reuse of old restore state makes operator debugging and recovery validation unreliable. User experience: UX surface is operator-facing recovery behavior and evidence accuracy; restoring to the wrong point-in-time is high impact. Visual/DOM: non-web surface reviewed—Compose service definitions, shell-driven recovery flow, and ops evidence docs. Accessibility/i18n: non-web ops surface; no UI-specific accessibility or i18n change reviewed. Supply-chain/license: existing pgvector/pgvector:pg16 image usage continues and no new package manifest was changed in current-head evidence. Packaging: Coverage execution evidence says no supported changed source files or package manifests were found; reviewed as YAML/shell/docs operational surfaces. Security/privacy: no-new-privileges, read_only, and explicit tmpfs are good hardening steps, but stale restore-volume reuse is still a recovery-boundary data-integrity risk.

flowchart TD
A["docker-compose.postgres-pitr-restore.yml"] --> B["persistent named volume \"restore_data\""]
B --> C["startup guard only rematerializes when PG_VERSION is absent"]
C --> D["new RECOVERY_TARGET_TIME can be ignored on repeat run"]
D --> E["verify by starting db-restore twice on the same project without down -v"]
Loading

Adversarial validation

{"status":"failed","probes":[{"path":"docker-compose.postgres-pitr-restore.yml","line":53,"hypothesis":"A second `db-restore` run on the same compose project can silently reuse the first restore state instead of honoring a new `RECOVERY_TARGET_TIME`.","attack_or_counterexample":"Materialize `db-restore` once, then rerun `docker compose -p <same-project> -f docker-compose.postgres-ha.yml -f docker-compose.postgres-pitr-restore.yml up -d db-restore` with a different `RECOVERY_TARGET_TIME` and without `down -v`.","evidence":"Trusted source trace at docker-compose.postgres-pitr-restore.yml:53 observed `restore_data` is a persistent named volume, while the same file's startup guard only copies `/base_backup` and rewrites `recovery_target_time` when `PG_VERSION` is absent; on a repeat run with an already-populated volume, initialization is skipped and stale restore state is retained. Trusted current-head source binding at docker-compose.postgres-pitr-restore.yml:53; source-line-sha256=80bd7d707f755c9a47b5a667bb0988de3a387302d1c6970882cadb1a8238c530","outcome":"confirmed"},{"path":"docker-compose.postgres-ha.yml","line":3,"hypothesis":"The new `read_only` hardening removes required PostgreSQL write paths and obviously breaks the HA services.","attack_or_counterexample":"Trace the writable paths left after enabling `read_only: true` on `db-primary` and `db-replica`.","evidence":"Trusted source trace at docker-compose.postgres-ha.yml:3 observed the hardening is paired with writable named volumes for `/var/lib/postgresql/data`, `/wal_archive`, and `/base_backup` plus explicit `tmpfs` mounts for `/tmp` and `/run/postgresql`, so the compose definition still preserves the required write targets visible in the current-head hunk. Trusted current-head source binding at docker-compose.postgres-ha.yml:3; source-line-sha256=5a1154c2a38c8c30a8e69971614f36ea799425368df154f0b338917f5d971346","outcome":"falsified"}],"residual_risk":"The main unresolved risk is incorrect PITR target selection on repeated `db-restore` use within the same compose project. Other reviewed current-head changes—the UTC offset, archive idempotence/atomicity, and container hardening—look source-consistent in the bounded evidence, but no trusted execution receipt was supplied for this specific repeat-restore edge."}
  • Result: REQUEST_CHANGES

  • Reason: The new PITR restore overlay can silently reuse a stale restore_data volume and ignore a later RECOVERY_TARGET_TIME on repeated db-restore runs.

  • Head SHA: 5af23f188d764128636e1f308842bbd9f3329ca7

  • Workflow run: 32883046501

  • Workflow attempt: 1

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file (4 files)"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file (4 files)"]
  R1 --> V1["required checks"]
  Evidence --> S2["Docs (2 files)"]
  S2 --> I2["operator or user guidance"]
  I2 --> R2["Review risk: Docs (2 files)"]
  R2 --> V2["docs review"]
Loading

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

OpenCode reviewed the current-head bounded evidence and requested changes before merge.

Findings

1. P1 docker-compose.postgres-pitr-restore.yml:53 - Repeat db-restore runs can silently reuse the previous recovery point

  • Problem: Evidence type: nearby implementation / current-head focused hunk. docker-compose.postgres-pitr-restore.yml defines restore_data as a persistent named volume, and the restore entrypoint only rematerializes from /base_backup when PG_VERSION is absent. Re-running the documented db-restore overlay on the same compose project with a new RECOVERY_TARGET_TIME therefore skips the recovery setup and can keep serving the earlier restore target instead of the requested one.
  • Root cause: The restore flow keys first-run initialization only on PG_VERSION, but the overlay persists restore state across runs via the named restore_data volume. Nothing compares the requested RECOVERY_TARGET_TIME to the previously materialized restore metadata or forces a fresh volume for a new restore.
  • Fix: Make repeat-use safe: either (a) rematerialize restore_data whenever a new PITR target is requested, or (b) persist the last materialized target and fail fast when the operator reuses the same volume with a different RECOVERY_TARGET_TIME. If the intended contract is one-shot-per-project, document that directly in the overlay header and enforce it instead of silently reusing stale data.
  • Regression test: Use the PR's documented restore surface and repeat it on the same compose project: first start db-restore with one RECOVERY_TARGET_TIME, then rerun docker compose -p <same-project> -f docker-compose.postgres-ha.yml -f docker-compose.postgres-pitr-restore.yml up -d db-restore with a different target and without down -v. The second run should either rematerialize the restore or fail with an explicit stale-volume error; it must not silently keep the old target.
  • Suggested diff: posted in this finding's inline review thread.

Summary

Pull request overview: reviewed current-head bounded evidence plus focused hunks for CHANGELOG.md, docker-compose.postgres-ha.yml, docker-compose.postgres-pitr-restore.yml, docs/operations/postgresql-ha-drill-20260615.md, and docs/operations/postgresql-pitr-drill-2026-08-25.md. One blocking correctness issue remains in the PITR restore flow. Approval sufficiency: not sufficient because the restore overlay can return a previous recovery point on a normal repeat-restore operator path. Verification posture: source-trace review from current-head bounded evidence; direct reads from /home/runner/work/_temp/opencode-pr-head were blocked, so conclusions rely on the authoritative focused hunks and bounded source traces. Linter/static: no completed failed GitHub Checks were present when evidence was collected. TDD/regression: no repo-native regression in the supplied evidence covers repeated db-restore starts without down -v; this needs an explicit repeat-restore guard test or documented verification. Coverage: Coverage execution evidence marked PASS and explicitly says test coverage is not applicable because no supported changed source files or package manifests were found. Docstring coverage: Coverage execution evidence says docstring coverage is not applicable because no supported changed source files or package manifests were found. DAG: source-backed head-flow diagram below maps docker-compose.postgres-pitr-restore.yml to the stale-volume PITR path. PoC/execution: no trusted execution receipt for this restore edge was supplied; review is based on bounded source traces plus the committed drill transcript. DDD/domain: the domain contract is PITR to a requested instant, and silent reuse of an older materialized restore violates that contract. CDD/context: the new overlay is documented for direct operator use via docker compose ... up -d db-restore, so repeatability on the same compose project matters. Similar issues: historical current-head context already shows recent fixes here for UTC targeting, archive idempotence, and cleanup; this remaining stale-volume path is in the same recovery boundary. Claim/concept check: the one-shot drill transcript is internally consistent, but the reusable overlay behavior is not guarded against a changed target on a reused project. Standards search: not required; this is a repository-local compose control-flow issue. Compatibility/convention: changed identifiers (db-primary, db-replica, db-restore, wal_archive, base_backup, restore_data) follow local multi-word naming and no new exposed sequential identifiers were introduced. Breaking-change/backcompat: ops-only change, but the current restore overlay is unsafe for repeat use on the same compose project until it rematerializes or rejects stale data. Performance: no separate performance blocker identified. Developer experience: DX surface is the documented local DR drill and restore overlay; silent reuse of old restore state makes operator debugging and recovery validation unreliable. User experience: UX surface is operator-facing recovery behavior and evidence accuracy; restoring to the wrong point-in-time is high impact. Visual/DOM: non-web surface reviewed—Compose service definitions, shell-driven recovery flow, and ops evidence docs. Accessibility/i18n: non-web ops surface; no UI-specific accessibility or i18n change reviewed. Supply-chain/license: existing pgvector/pgvector:pg16 image usage continues and no new package manifest was changed in current-head evidence. Packaging: Coverage execution evidence says no supported changed source files or package manifests were found; reviewed as YAML/shell/docs operational surfaces. Security/privacy: no-new-privileges, read_only, and explicit tmpfs are good hardening steps, but stale restore-volume reuse is still a recovery-boundary data-integrity risk.

flowchart TD
A["docker-compose.postgres-pitr-restore.yml"] --> B["persistent named volume \"restore_data\""]
B --> C["startup guard only rematerializes when PG_VERSION is absent"]
C --> D["new RECOVERY_TARGET_TIME can be ignored on repeat run"]
D --> E["verify by starting db-restore twice on the same project without down -v"]
Loading

Adversarial validation

{"status":"failed","probes":[{"path":"docker-compose.postgres-pitr-restore.yml","line":53,"hypothesis":"A second `db-restore` run on the same compose project can silently reuse the first restore state instead of honoring a new `RECOVERY_TARGET_TIME`.","attack_or_counterexample":"Materialize `db-restore` once, then rerun `docker compose -p <same-project> -f docker-compose.postgres-ha.yml -f docker-compose.postgres-pitr-restore.yml up -d db-restore` with a different `RECOVERY_TARGET_TIME` and without `down -v`.","evidence":"Trusted source trace at docker-compose.postgres-pitr-restore.yml:53 observed `restore_data` is a persistent named volume, while the same file's startup guard only copies `/base_backup` and rewrites `recovery_target_time` when `PG_VERSION` is absent; on a repeat run with an already-populated volume, initialization is skipped and stale restore state is retained. Trusted current-head source binding at docker-compose.postgres-pitr-restore.yml:53; source-line-sha256=80bd7d707f755c9a47b5a667bb0988de3a387302d1c6970882cadb1a8238c530","outcome":"confirmed"},{"path":"docker-compose.postgres-ha.yml","line":3,"hypothesis":"The new `read_only` hardening removes required PostgreSQL write paths and obviously breaks the HA services.","attack_or_counterexample":"Trace the writable paths left after enabling `read_only: true` on `db-primary` and `db-replica`.","evidence":"Trusted source trace at docker-compose.postgres-ha.yml:3 observed the hardening is paired with writable named volumes for `/var/lib/postgresql/data`, `/wal_archive`, and `/base_backup` plus explicit `tmpfs` mounts for `/tmp` and `/run/postgresql`, so the compose definition still preserves the required write targets visible in the current-head hunk. Trusted current-head source binding at docker-compose.postgres-ha.yml:3; source-line-sha256=5a1154c2a38c8c30a8e69971614f36ea799425368df154f0b338917f5d971346","outcome":"falsified"}],"residual_risk":"The main unresolved risk is incorrect PITR target selection on repeated `db-restore` use within the same compose project. Other reviewed current-head changes—the UTC offset, archive idempotence/atomicity, and container hardening—look source-consistent in the bounded evidence, but no trusted execution receipt was supplied for this specific repeat-restore edge."}
  • Result: REQUEST_CHANGES

  • Reason: The new PITR restore overlay can silently reuse a stale restore_data volume and ignore a later RECOVERY_TARGET_TIME on repeated db-restore runs.

  • Head SHA: 5af23f188d764128636e1f308842bbd9f3329ca7

  • Workflow run: 32883046501

  • Workflow attempt: 1

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file (4 files)"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file (4 files)"]
  R1 --> V1["required checks"]
  Evidence --> S2["Docs (2 files)"]
  S2 --> I2["operator or user guidance"]
  I2 --> R2["Review risk: Docs (2 files)"]
  R2 --> V2["docs review"]
Loading

volumes:
wal_archive:
base_backup:
restore_data:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Repeat db-restore runs can silently reuse the previous recovery point

  • Location: docker-compose.postgres-pitr-restore.yml:53
  • Problem: Evidence type: nearby implementation / current-head focused hunk. docker-compose.postgres-pitr-restore.yml defines restore_data as a persistent named volume, and the restore entrypoint only rematerializes from /base_backup when PG_VERSION is absent. Re-running the documented db-restore overlay on the same compose project with a new RECOVERY_TARGET_TIME therefore skips the recovery setup and can keep serving the earlier restore target instead of the requested one.
  • Root cause: The restore flow keys first-run initialization only on PG_VERSION, but the overlay persists restore state across runs via the named restore_data volume. Nothing compares the requested RECOVERY_TARGET_TIME to the previously materialized restore metadata or forces a fresh volume for a new restore.
  • Fix: Make repeat-use safe: either (a) rematerialize restore_data whenever a new PITR target is requested, or (b) persist the last materialized target and fail fast when the operator reuses the same volume with a different RECOVERY_TARGET_TIME. If the intended contract is one-shot-per-project, document that directly in the overlay header and enforce it instead of silently reusing stale data.
  • Regression test: Use the PR's documented restore surface and repeat it on the same compose project: first start db-restore with one RECOVERY_TARGET_TIME, then rerun docker compose -p <same-project> -f docker-compose.postgres-ha.yml -f docker-compose.postgres-pitr-restore.yml up -d db-restore with a different target and without down -v. The second run should either rematerialize the restore or fail with an explicit stale-volume error; it must not silently keep the old target.

Suggested diff

```diff
+        current_target_file="$${pg_data}/.pitr_recovery_target_time"
         if [ ! -s "$${pg_data}/PG_VERSION" ]; then
           echo "Materializing restore data directory from base backup."
           find "$${pg_data}" -mindepth 1 -delete
           cp -a /base_backup/. "$${pg_data}/"
           rm -f "$${pg_data}/postmaster.pid" "$${pg_data}/standby.signal"
           touch "$${pg_data}/recovery.signal"
           {
             printf "restore_command = 'cp /wal_archive/%%f %%p'\n"
             printf "recovery_target_time = '%s'\n" "$${RECOVERY_TARGET_TIME}"
             printf "recovery_target_action = 'promote'\n"
           } >> "$${pg_data}/postgresql.auto.conf"
+          printf "%s\n" "$${RECOVERY_TARGET_TIME}" > "$${current_target_file}"
           chown -R postgres:postgres "$${pg_data}"
           chmod 700 "$${pg_data}"
+        elif [ -f "$${current_target_file}" ] && ! grep -Fxq "$${RECOVERY_TARGET_TIME}" "$${current_target_file}"; then
+          printf >&2 "restore_data was already materialized for a different RECOVERY_TARGET_TIME; remove the volume or use a fresh compose project before rerunning PITR.\n"
+          exit 1
         fi

@opencode-agent
opencode-agent Bot disabled auto-merge August 26, 2026 00:49

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 new potential issue.

Open in Devin Review

Comment on lines +169 to +171
echo "Restoring again from the same volume targeting ${second_recovery_target_time} UTC."
export RECOVERY_TARGET_TIME="${second_recovery_target_time}"
"${full_compose[@]}" up -d db-restore

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Re-target depends on compose recreating the container on env change

The second restore assumes up -d db-restore recreates the container so the new RECOVERY_TARGET_TIME takes effect (postgres_pitr_drill.sh). Compose bakes env at create time; a provider that merely restarts the stopped container keeps the old target and the assertion at postgres_pitr_drill.sh fails. This fails loud, not silently, and the stamp logic still re-materializes safely.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

1 participant