Preserve repeated corrupt-DB quarantines instead of dropping the newest (#661) - #681
Merged
Conversation
…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
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #661.
The bug
The #1014 corruption handler (
CorruptionPreservingOpenHelperFactory.onCorruption) kept a single fixed.corruptslot: the first corrupt copy was preserved and every later one deleted (theelse { 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
whoop.db.corrupt.<epochMillis>+ keep newestMAX_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).Scope / parity
Android-only. iOS/GRDB has no delete-on-corruption default (this Android handler exists to neutralise the platform default), and
WhoopStore.quarantineIncompatibleDatabasealready 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