Skip to content

Harden pre-migration backup retention edge cases - #3112

Open
Chris0Jeky wants to merge 8 commits into
mainfrom
codex/1856-sqlite-backup-hardening-20260915
Open

Chris0Jeky wants to merge 8 commits into
mainfrom
codex/1856-sqlite-backup-hardening-20260915

Conversation

@Chris0Jeky

@Chris0Jeky Chris0Jeky commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the three concrete SqlitePreMigrationBackup residuals in #1856 and the edge cases surfaced during review:

  • reclaims only crash-old staging snapshots that match Taskdeck's exact managed filename contract, including sidecar-only .db.tmp-wal / .db.tmp-shm remnants;
  • treats the staging marker and both SQLite sidecars as one liveness unit, using the newest member timestamp and deleting the marker last;
  • uses ordinal filename identity so case-distinct files cannot suppress each other on case-sensitive filesystems;
  • parses sequence numbers with BigInteger, so every syntactically valid sequence remains orderable and prunable beyond long.MaxValue;
  • centralizes canonical filename parsing for both retention and orphan cleanup: ASCII digits, a real round-trippable UTC timestamp, a positive sequence, and canonical D6 padding are required before Taskdeck will delete a file.

Cleanup remains deliberately non-fatal and runs only after the new protective snapshot exists. A one-day age floor prevents reclaiming another live process's staging set.

Regression-first evidence

Sidecar-only orphan discovery

RED head 4bef8d2106bff30a6d9ff294970dd2495f6fe324:

  • Required CI run 34953265511
  • Ubuntu API Integration job 104329316338
  • the new regression showed a stale managed .db.tmp-wal survived when the marker file was already absent.

The implementation now enumerates strict marker, WAL, and SHM shapes and reconstructs the canonical staging set from any surviving member.

Non-canonical managed-name rejection

RED head 2606b5b9b662528e58e8b9e17c633d7267dd567c:

  • Required CI run 34977431119
  • Ubuntu API Integration job 104408910756
  • exactly one failure: Backup_preserves_stale_noncanonical_staging_names (3269 passed / 1 failed / 4 skipped)
  • an Arabic-Indic decimal digit matched .NET \d, so an impossible Taskdeck filename was deleted.

The current parser uses [0-9] and requires exact timestamp/sequence round-trips before a file becomes a managed deletion target.

Verification

Exact head: 607eefaf77dcc865f033529956b015463516107b

  • Smart CI Self-Test 34979490042: success
  • CI Extended 34979490734: success
  • Required CI 34979490636: all completed jobs green, including Linux API Integration and E2E Smoke; the long Windows API Integration lane is still running at the time of this evidence refresh
  • focused backup regressions compile and execute inside the API Integration suite
  • fresh exact-head Codex review requested; the prior inline finding has a regression-backed fix response

Scope

This PR does not alter backup creation, migration ordering, the fail-closed snapshot boundary, database locations, or release configuration. It only hardens retention/cleanup identity and crash-debris handling.

Closes #1856

@Chris0Jeky
Chris0Jeky marked this pull request as ready for review September 15, 2026 08:32
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-15T14:42:16.135169Z 607eefa Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: 23282f1105

ℹ️ 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 backend/src/Taskdeck.Infrastructure/Persistence/SqlitePreMigrationBackup.cs Outdated

Copy link
Copy Markdown
Owner Author

@codex address that feedback

The regression-first head 4bef8d2106bff30a6d9ff294970dd2495f6fe324 now has causal RED evidence: required CI run 34953265511, Ubuntu API job 104329316338, failed exactly Backup_discovers_sidecar_only_orphans_and_preserves_recent_or_unrelated_sidecars because the stale managed *.db.tmp-wal remained present; 3,268 API tests passed and every earlier unrelated lane was green.

Please fix the reviewed root cause without broadening scope:

  1. discover strict managed staging sets from .db.tmp, .db.tmp-wal, and .db.tmp-shm candidates;
  2. canonicalize each candidate back to its .db.tmp base before applying the existing current/legacy strict filename regex and ordinal dedupe;
  3. preserve the one-day age gate using the newest existing member of the set;
  4. delete -wal and -shm before deleting the .tmp marker so any sidecar deletion failure remains discoverable on a later pass;
  5. keep unrelated and recent sidecar-only files untouched.

Run the focused backup hardening tests, leave the PR unmerged, and publish the exact new head for hosted qualification and re-review.

Copy link
Copy Markdown
Owner Author

@codex review

Please re-review exact head 74ce65e83605d06c66d56f750b8f45c6396090c0 after the sidecar-only orphan fix. Focus on strict filename canonicalization, live-set age calculation, sidecar-before-marker deletion, unrelated-file safety, BigInteger sequence retention, and whether the new regression actually closes review thread PRRT_kwDOQXwLG86icEOJ. The regression-first RED receipt is required CI run 34953265511, Ubuntu API job 104329316338.

@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

Testing

  • PATH=/tmp/dotnet:$PATH dotnet test backend/tests/Taskdeck.Api.Tests/Taskdeck.Api.Tests.csproj -c Release -m:1 --filter "FullyQualifiedName~PreMigrationBackupHardeningTests" — 4 passed, 0 failed.
  • PATH=/tmp/dotnet:$PATH dotnet test backend/Taskdeck.sln -c Release -m:1 — full backend solution passed: Domain 1,687; Application 4,609; API 3,269 passed and 4 skipped; CLI 243; Architecture 28 passed and 1 skipped; Integration 7 passed and 29 skipped.
  • git diff --check
  • git status --short --branch — clean current branch after commit.

View task →

@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: 74ce65e836

ℹ️ 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 on lines +575 to +576
var snapshotFileName = temporaryFileName[..^TemporaryExtension.Length];
if (!currentPattern.IsMatch(snapshotFileName) && !legacyPattern.IsMatch(snapshotFileName))

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 Restrict orphan deletion to canonical ASCII filenames

When a stale candidate contains non-ASCII decimal digits in its timestamp or sequence, .NET's \d in both filename patterns still matches it even with RegexOptions.CultureInvariant, and this branch performs no subsequent parse or canonicalization before deleting the artifact set. BuildFileName only emits invariant ASCII digits, so such a file was not created under the helper's canonical contract but will nevertheless be removed after one day; use ASCII digit classes and validate the canonical timestamp/sequence before treating a staging file or sidecar as managed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 607eefaf77dcc865f033529956b015463516107b after a regression-first run.

RED evidence: test-only head 2606b5b9b662528e58e8b9e17c633d7267dd567c, Required CI run 34977431119, Ubuntu API job 104408910756 failed only Backup_preserves_stale_noncanonical_staging_names (3269 passed / 1 failed / 4 skipped). The first stale noncanonical target used an Arabic-Indic decimal digit and was incorrectly deleted.

The fix makes [0-9] explicit and centralizes canonical parsing for both retention and orphan cleanup: timestamp values must parse and round-trip through the emitted UTC format; sequences must be positive and round-trip through D6, while retaining arbitrary-precision support. The same parser now defines managed identity in both paths. Exact-head CI is running.

Copy link
Copy Markdown
Owner Author

@codex review

Please review exact head 607eefaf77dcc865f033529956b015463516107b, including the canonical filename parser added in response to the unresolved Unicode-digit deletion finding. The preceding test-only head and failing hosted run are recorded in the inline thread.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 607eefaf77

ℹ️ 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".

Copy link
Copy Markdown
Owner Author

@codex review

Exact head 607eefaf77dcc865f033529956b015463516107b is now fully qualified:

  • Required CI 34979490636: success, including both API Integration legs and E2E Smoke
  • CI Extended 34979490734: success
  • Smart CI Self-Test 34979490042: success

Please review the exact current head, especially the shared canonical filename parser and the regression-backed response to the Unicode-digit deletion finding.

@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: 607eefaf77

ℹ️ 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 on lines +286 to 288
if (!File.Exists(candidate) && !File.Exists(candidate + TemporaryExtension))
{
return candidate;

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 Account for sidecars when reserving the next snapshot name

When a recent sidecar-only staging set has the next sequence (for example, completed snapshots end at 000005 and ...-000006.db.tmp-wal remains), this check ignores the sidecar and reserves 000006. WriteSnapshot then deletes that WAL/SHM before the new age-aware cleanup runs, bypassing the one-day liveness protection and contradicting the new recent-sidecar preservation contract. Include candidate + ".tmp-wal" and candidate + ".tmp-shm" in collision handling so the backup selects another sequence rather than deleting a potentially live sidecar.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

@codex address that feedback with a regression-only remote commit first; do not change production code yet.

On branch codex/1856-sqlite-backup-hardening-20260915, extend PreMigrationBackupHardeningTests with a focused test that:

  1. creates the source SQLite database and backup directory;
  2. creates a recent sidecar-only managed staging file at the next sequence, e.g. taskdeck.db-pre-migration-<canonical timestamp>-000001.db.tmp-wal, with no .db or .db.tmp marker;
  3. calls SqlitePreMigrationBackup.Create;
  4. asserts the recent sidecar still exists; and
  5. asserts the newly created snapshot uses sequence 000002, not 000001.

Commit and push only that test to this PR branch, report the GitHub-resolvable head, and leave the expected RED for hosted API Integration. The production fix must wait until the causal RED is recorded.

@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

  • Added a regression-only test that:
  • Made no production-code changes.
  • Committed the test on codex/1856-sqlite-backup-hardening-20260915:
    • a158fb1aeab879e19ea50ddebb891c6930a9d376
    • test: reserve sequence for live backup sidecar
  • Prepared the required follow-up PR metadata titled “Add regression for live backup sidecar sequence reservation.”
  • The commit could not be pushed, so there is not yet a GitHub-resolvable head. This environment has neither GitHub CLI authentication nor HTTPS Git credentials; git push failed before contacting the PR branch.
  • Canonical STATUS/master-plan documentation was not changed because this is deliberately a regression-only commit.
  • Open human-action items remain in OUTSTANDING_TASKS.md, including prompt-rail fixture binding, native Italian/Spanish review, real-device keyboard verification, credential rotation, and repository control-plane actions. OUTSTANDING_TASKS.mdL120-L122 OUTSTANDING_TASKS.mdL143-L144 OUTSTANDING_TASKS.mdL165-L168 OUTSTANDING_TASKS.mdL178-L189

Testing

  • git diff --check
  • git diff HEAD^ --name-only — only backend/tests/Taskdeck.Api.Tests/PreMigrationBackupHardeningTests.cs changed.
  • git diff HEAD^ -- backend/src/Taskdeck.Infrastructure/Persistence/SqlitePreMigrationBackup.cs — empty; production code remains unchanged.
  • git status --short --branch — clean branch after commit.
  • ⚠️ dotnet test backend/tests/Taskdeck.Api.Tests/Taskdeck.Api.Tests.csproj -c Release -m:1 --filter "FullyQualifiedName~PreMigrationBackupHardeningTests" — could not run because dotnet is not installed in this container. Hosted API Integration should retain the expected causal RED until production collision handling is changed.
  • ⚠️ git push -u origin codex/1856-sqlite-backup-hardening-20260915 — failed with could not read Username for 'https://github.com'; no GitHub credentials are available in the environment.

View task →

@Chris0Jeky Chris0Jeky left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fresh-context review: no merge-blocking correctness finding on this head.

The design is conservative in the right places: deletion eligibility is gated by one canonical parser shared by retention and orphan cleanup; sequence ordering no longer has an exclusion cliff; staging marker/WAL/SHM are treated as one liveness unit; sidecar-only debris remains rediscoverable; and cleanup stays after the durable replacement snapshot and non-fatal. The regression set exercises real SQLite creation plus stale/recent/unrelated, sidecar-only, non-ASCII/impossible names, zero/noncanonical sequences, and values beyond long.MaxValue.

I also rechecked the destructive boundary specifically: the deletion order is WAL, SHM, then marker, so a partial failure leaves a discoverable member; case identity is ordinal while strict parsing prevents case variants becoming managed targets; timestamp and sequence both round-trip before deletion. Exact-head Required CI, CI Extended, and Smart CI are now all green.

Residual risks are pre-existing/accepted rather than introduced here: same-key deployments sharing one absolute backup directory can still interfere, and last-write-time liveness cannot eliminate every cross-process TOCTOU on Unix. Neither is widened by this PR, and both are outside #1856's stated scope.

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

Labels

None yet

Projects

Status: Pending

Development

Successfully merging this pull request may close these issues.

[Backend] SqlitePreMigrationBackup follow-ups from the PR #1849 review: .tmp orphans, case dedupe, sequence overflow

1 participant