Skip to content

feat(cloud-backups): rotation-interval-days for audit-rotate (cadence-independent rotation) - #8

Merged
logicflakes merged 1 commit into
mainfrom
2026-07-audit-rotation-interval
Jul 24, 2026
Merged

feat(cloud-backups): rotation-interval-days for audit-rotate (cadence-independent rotation)#8
logicflakes merged 1 commit into
mainfrom
2026-07-audit-rotation-interval

Conversation

@logicflakes

Copy link
Copy Markdown
Collaborator

What

Adds --rotation-interval-days / ROTATION_INTERVAL_DAYS (default 0 = OFF = today's rotate-every-run) to pg audit-rotate. When set, it decouples archive rotation from the cron cadence: the cron reconciles every run (recover interrupted backups, drop aged archives) but cuts a new archive only when the newest existing one is >= N days old. So the CronJob can run at any frequency (per-minute … daily) while the number of coexisting audit_archive_* tables stays bounded by the interval, not by how often the cron fires. Set = audit-retention-days for a single archive at a time (one bucket object per interval → simplest month-granular audit restore).

Why

Customer wants a single audit archive table at a time (optics + easier bucket restore) at 30-day retention, with the freedom to run a fast reconcile cron. Only a monthly cron gives ~1 archive today, and it can't retry a failed backup for a month; this knob gives ~1 archive with a frequent reconcile/backup-retry cron. Full reasoning + the R+C cadence law in rearm-core/ai-plans/audit-rotate-backup-drop-design.md.

How

  • Only Pass 2 (rotate) is gated (rotationDecision); Pass 1 (reconcile/drop) runs every run, unchanged. The gate is stateless — derived from the newest archive's name-timestamp, the same oracle the drop gate trusts — so any run/replay/missed-then-resumed run computes the same decision.
  • Because Pass 1 drops aged archives before Pass 2 rotates and both use the same cutoff, interval == retention holds exactly one archive (no transient second).

Guardrails (from a 5-lens adversarial design review)

  • Concurrency: the rotate txn is self-guarding — pg_try_advisory_xact_lock + a supersession check (a newer archive already exists) before the rename, so two overlapping runs (a manual job racing the cron) can't both cut an archive; the loser aborts with a benign skip. The client shells out a fresh psql per call, so a session lock can't span the run — the txn-scoped guard is the robust equivalent.
  • DROP TABLE IF EXISTS makes a concurrent Pass-1 double-drop a no-op, not a spurious quarantine + non-zero exit.
  • Observability: audit_rotate_summary now emits rotated_this_run, rotation_interval_days, newest_archive_age_days, rotation_skipped_reason — so alerting can shift to "was due but did not rotate" (the stall that, under interval rotation, the old oldest>retention signal can't see).
  • Validation: reject interval < 0 and interval > retention (degenerate); retention/interval day counts capped so the now - N days cutoff can't overflow int64.

Testing

  • Unit: TestRotationDecision (off/drain/bootstrap/under/over boundaries), TestNewestArchive, TestAdvisoryLockKey, TestIsRotateSkip, guard-before-rename ordering, DROP … IF EXISTS, config validation. go test ./... green; gofmt clean.
  • Live on PG17 + MinIO: bootstrap rotate, skip-when-not-due, single-archive drop+rotate cycle (interval=retention), and two overlapping runs → exactly one archive (guard aborts one, both exit 0).

Rollout

Merge + build the cloud-backup image before the helm knob (rearm-saas #325) does anything. Default 0 is a no-op for existing deployers, and an old image simply ignores the new env.

🤖 Generated with Claude Code

…-independent rotation)

Decouple archive rotation from the cron cadence so the audit-rotate CronJob can
run at ANY frequency (per-minute..daily) while the number of coexisting
audit_archive_* tables stays bounded by an INTERVAL, not by how often the cron
fires. rotation-interval-days=N cuts a new archive only when the newest existing
one is >= N days old (0 = OFF = rotate every run, unchanged). Set = retention for
a single archive at a time (one monthly bucket object for easy audit restore).

The cron still reconciles every run (recover interrupted backups, drop aged
archives); only Pass 2 (rotate) is gated. The gate is stateless -- derived from
the newest archive's name-timestamp, the same oracle the drop gate trusts -- so
any run/replay/missed-then-resumed run computes the same decision.

Guardrails (from a 5-lens adversarial design review):
- Concurrency: the rotate txn is self-guarding -- pg_try_advisory_xact_lock +
  a supersession check (a newer archive already exists) BEFORE the rename, so two
  overlapping runs (a manual job racing the cron) can't both cut an archive; the
  loser aborts with a benign skip. Without this the second run would rotate the
  fresh EMPTY table into a stray archive squatting a full retention window.
- DROP TABLE IF EXISTS makes a concurrent Pass-1 double-drop a no-op, not a
  spurious quarantine + non-zero exit.
- Observability: audit_rotate_summary now emits rotated_this_run,
  rotation_interval_days, newest_archive_age_days, rotation_skipped_reason so a
  monitor can alert on 'was due but did not rotate' (the stall that, under
  interval rotation, the old oldest>retention signal can't see).
- Validation: reject interval<0, interval>retention (degenerate), and cap both
  retention and interval at 100y so the now-Nd cutoff can't overflow int64.

Live-validated on PG17 + MinIO: bootstrap rotate, skip-when-not-due, single-
archive drop+rotate cycle (interval=retention), and two overlapping runs
producing exactly one archive (the guard aborts one, both exit 0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ReARM-Agent: 1420896f-adf5-4843-896f-d863cfcc6528
ReARM-Agentic-Session: 7fa98c2d-b192-4b4a-b1a8-ac64349a89b2
@logicflakes
logicflakes merged commit a5e73f2 into main Jul 24, 2026
14 of 18 checks passed
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