Skip to content

ble: stop the auto-continue empty-offload storm — guard 2a on real rows - #1145

Merged
ryanbr merged 1 commit into
mainfrom
fix/autocontinue-empty-offload-storm
Aug 8, 2026
Merged

ble: stop the auto-continue empty-offload storm — guard 2a on real rows#1145
ryanbr merged 1 commit into
mainfrom
fix/autocontinue-empty-offload-storm

Conversation

@ryanbr

@ryanbr ryanbr commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Fixes #1144. From a real WHOOP 4.0 capture (9.3.2-staging) I analysed today.

The bug (measured)

The offload auto-continue (#364/#451) fired 7 bursts of 24 empty offloads (~168 in 27 min), each with a 2M-PHY round-trip — a sustained battery/radio storm (the same capture showed ~4%/hr overnight background drain).

Root cause: shouldAutoContinue guard 2a (strapNewest − frontier > 300s → return true) fires regardless of whether the offload pulled any rows. When the strap advertises a newest ahead of our frontier but the offload returns chunkRows=0 (a phantom gap), the frontier can't advance without rows, so newest − frontier stays > 300s and 2a re-fires to the full 24 cap. Guard 3 (lastTrimAdvanced) doesn't catch it — the trim u32 climbs on empty ENDs. Only guard 2b checked rows.

The fix

Both platforms (WhoopBleClient.shouldAutoContinue + Swift BackfillContinuation.shouldAutoContinue, kept byte-for-behaviour): an empty session (rowsPersistedThisSession <= 0) never auto-continues, whatever the reported gap — making 2b's row check the one authority for both cases. Each 24-spin collapses to a single empty offload + fall back to the 15-min floor. Healthy deep backlogs persist 245–251 rows/chunk, so they're unaffected.

Verification

  • Kotlin BackfillContinuationTest — green locally (24 tests, incl. the new phantom-gap regression).
  • Swift shouldAutoContinue — validated via an isolated harness (StrandTests runs only under xcodebuild): happy-path continue, phantom-gap stop, stale-epoch-with-rows continue, caught-up stop all correct.
  • App targets (macOS + iOS) compiling via app-build.
  • BLE-path change: needs a real-strap validation pass before release — but this is strictly more conservative (adds a stop condition; can't cause a new spin), and the reporter (me) has the exact repro.

Tests updated: continue-expecting cases now pass rows>0 (a frontier can't advance with 0 rows, so the old rows=0 happy-paths modelled an impossible state).

…ws (#1144)

A real WHOOP 4.0 capture showed the offload auto-continue (#364/#451) firing 7 bursts
of 24 empty offloads (~168 in 27 min), each with a 2M-PHY round-trip — a battery/radio
storm. Root cause: guard 2a (strapNewest - frontier > 300s -> continue) returns true
regardless of whether the offload pulled any rows. When the strap advertises a newest
ahead of our frontier but the offload hands back chunkRows=0 (a phantom gap), the
frontier can't advance, the gap stays > 300s, and 2a re-fires to the full 24 cap. Guard
3 (lastTrimAdvanced) doesn't catch it — the trim u32 climbs on empty ENDs.

Fix (both platforms, shouldAutoContinue kept byte-for-behaviour): an EMPTY session
(rowsPersistedThisSession <= 0) never auto-continues, whatever the reported gap. Collapses
each 24-spin into a single empty offload + fall back to the 15-min floor. Healthy deep
backlogs persist real rows (245-251/chunk), so they're unaffected.

Tests: added the phantom-gap regression (gap holds, rows=0 -> stop) both platforms;
continue-expecting cases now pass rows>0 (a frontier can't advance with 0 rows).
Verified: Kotlin testFullDebugUnitTest green; Swift shouldAutoContinue via isolated harness.
@ryanbr
ryanbr merged commit 74454a4 into main Aug 8, 2026
5 checks passed
@ryanbr
ryanbr deleted the fix/autocontinue-empty-offload-storm branch August 8, 2026 04:07
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.

WHOOP 4.0: offload auto-continue (#364/#451) spins empty-offload storms — guard 2a ignores rows

1 participant