Skip to content

Preserve repeated corrupt-DB quarantines instead of dropping the newest (#661) - #681

Merged
ryanbr merged 1 commit into
mainfrom
fix/corrupt-db-quarantine-661
Jul 20, 2026
Merged

Preserve repeated corrupt-DB quarantines instead of dropping the newest (#661)#681
ryanbr merged 1 commit into
mainfrom
fix/corrupt-db-quarantine-661

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Fixes #661.

The bug

The #1014 corruption handler (CorruptionPreservingOpenHelperFactory.onCorruption) kept a single fixed .corrupt slot: the first corrupt copy was preserved and every later one deleted (the else { original.delete() } branch). But a later corruption is a rebuilt store that has banked new, non-resendable strap history since — so the copy it discarded was the most recovery-worthy one, silently defeating the handler's whole purpose. tigercraft4 flagged this in a static data-integrity audit.

The behaviour was a deliberate disk bound ("repeated failed opens can't multiply 100 MB files") — but it picked disk-safety over the newest data.

The fix

  • Timestamped quarantine names whoop.db.corrupt.<epochMillis> + keep newest MAX_CORRUPT_QUARANTINES (3), pruning older ones. Caps disk (a crash loop can't multiply files) and preserves the latest data plus a couple of priors for diagnosing a recurring corruption path. Naming/pruning is a pure, unit-tested function (planCorruptQuarantine).
  • Move WAL/SHM sidecars alongside the quarantine rather than deleting them — the WAL can hold the most recent un-checkpointed writes. The live path is still cleared (rename, or copy+delete on the cross-storage fallback), so the fresh rebuild can never inherit a stale write-ahead log. (A stale live WAL could otherwise be applied to the new DB by salt match and re-inject corrupt pages — hence live-path clearing always wins over preserving an un-movable sidecar.)
  • Doc fix: the class-doc claimed the factory "deletes NOTHING", but the old else-branch deleted the original and the sidecars were always dropped.

Scope / parity

Android-only. iOS/GRDB has no delete-on-corruption default (this Android handler exists to neutralise the platform default), and WhoopStore.quarantineIncompatibleDatabase already uses timestamped names — so this converges the two rather than diverging. No analytics/stored-data change → parity contract untouched.

Verification

  • ./gradlew compileFullDebugKotlin
  • ./gradlew testFullDebugUnitTest --tests com.noop.data.CorruptDbQuarantineTest ✓ (first/under-cap/keep-newest-N/eviction-order/never-evict-new/dedup)
  • ./gradlew testFullDebugUnitTest --tests com.noop.data.* ✓ (no existing regressions)
    The filesystem move/prune is I/O and validated by inspection; the decision logic is unit-tested.

Reported-by: tigercraft4

…st (#661)

The #1014 corruption handler kept a single fixed `.corrupt` slot: the FIRST
corrupt copy was preserved and every later one deleted (the `else` branch).
But a later corruption is a rebuilt store that has banked new, non-resendable
strap history since — so the copy it discarded was the most recovery-worthy
one, silently defeating the handler's whole purpose.

Quarantine under a TIMESTAMPED name (`whoop.db.corrupt.<epochMillis>`) and keep
the newest MAX_CORRUPT_QUARANTINES (3), pruning older ones. This caps disk (a
crash loop can't multiply 100 MB files — the original concern) AND preserves
the latest data plus a couple of priors for diagnosing a recurring corruption
path. The naming/pruning is a pure, unit-tested function.

Also move the WAL/SHM sidecars ALONGSIDE the quarantine rather than deleting
them: the WAL can hold the most recent un-checkpointed writes. The live path is
still cleared (rename, or copy+delete on the cross-storage fallback), so the
fresh rebuild can never inherit a stale write-ahead log — a stale live WAL
could otherwise be applied to the new DB by salt and re-inject corrupt pages.

Corrected the class-doc, which claimed the factory 'deletes NOTHING' — the old
else-branch deleted the original and the sidecars were always dropped.

Android-only: iOS/GRDB has no delete-on-corruption default (this handler exists
to neutralise Android's), and already quarantines with timestamped names.

Reported-by: tigercraft4 (static data-integrity audit). Verified:
compileFullDebugKotlin + CorruptDbQuarantineTest + com.noop.data.*.

Author: ryanbr <mp3geek@gmail.com>
@ryanbr
ryanbr merged commit cb4f8eb into main Jul 20, 2026
1 check passed
@ryanbr
ryanbr deleted the fix/corrupt-db-quarantine-661 branch July 20, 2026 17:28
ryanbr added a commit that referenced this pull request Jul 20, 2026
Marketing version stays 9.0.3; Android versionCode 296->297 and iOS
CURRENT_PROJECT_VERSION 206->207 so the new staging build installs over the
previous one. Content since the last testing build: Health Connect writeback
failure surfacing (#672), SpO2/respiration empty-track explanation (#673),
WHOOP 5/MG raw-IMU offload persist (#675), hide stale sleep recompute entries
(#679), Oura activity MET research corpus + cap/rotation (#676/#680), trend
chart date alignment (#528), and corrupt-DB quarantine preservation (#681).
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
…st (ryanbr#661) (ryanbr#681)

Timestamped .corrupt.<epoch> quarantine + keep-newest-3 pruning replaces the single fixed .corrupt slot that deleted every later corrupt copy (the most recovery-worthy one). WAL/SHM moved alongside the quarantine; live path always cleared so the rebuild can't inherit a stale WAL (ryanbr#1014 crash-loop invariant preserved). Pure planCorruptQuarantine + unit test. Android-only; iOS/GRDB has no delete-on-corruption default. Reported-by: tigercraft4.
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
Marketing version stays 9.0.3; Android versionCode 296->297 and iOS
CURRENT_PROJECT_VERSION 206->207 so the new staging build installs over the
previous one. Content since the last testing build: Health Connect writeback
failure surfacing (ryanbr#672), SpO2/respiration empty-track explanation (ryanbr#673),
WHOOP 5/MG raw-IMU offload persist (ryanbr#675), hide stale sleep recompute entries
(ryanbr#679), Oura activity MET research corpus + cap/rotation (ryanbr#676/ryanbr#680), trend
chart date alignment (ryanbr#528), and corrupt-DB quarantine preservation (ryanbr#681).
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.

Data integrity: preserve multiple corrupt database quarantines instead of deleting repeated corrupt originals

1 participant