Skip to content

fix: tolerate divergent discovery-cache dicts during expiry#533

Draft
bluetoothbot wants to merge 3 commits into
Bluetooth-Devices:mainfrom
bluetoothbot:koan/storage-expiry-divergent-dict-guard
Draft

fix: tolerate divergent discovery-cache dicts during expiry#533
bluetoothbot wants to merge 3 commits into
Bluetooth-Devices:mainfrom
bluetoothbot:koan/storage-expiry-divergent-dict-guard

Conversation

@bluetoothbot

@bluetoothbot bluetoothbot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

What: Harden discovery-cache expiry so a single corrupt scanner blob can't abort the whole cache load.

Why: expire_stale_scanner_discovered_device_advertisement_data runs across every scanner blob before the cache reaches discovered_device_advertisement_data_from_dict (which already discards-and-rebuilds on malformed data). The expiry pass had two unguarded failure modes that a divergent/corrupt on-disk blob could trip, each raising KeyError and aborting expiry for every scanner → cold adapter start for all devices:

  1. Divergent companion dictstimestamps drives expiry but the entry was del-ed from discovered_device_advertisement_datas unconditionally; an address present only in timestamps raised KeyError.
  2. Missing top-level keysexpire_seconds / timestamps / ad-datas were hard-subscripted, so a scanner blob missing any of them (or not a mapping at all) raised KeyError/TypeError.

How:

  • (1) del discovered_device_advertisement_datas[address].pop(address, None); timestamps stays the authoritative delete.
  • (2) Wrap the per-scanner key fetch in try/except (KeyError, TypeError); on failure, log a warning and drop just that scanner (queued via the existing expired_scanners path) instead of aborting the loop. This mirrors _from_dict's discard-and-rebuild philosophy — the cache is rebuildable within seconds of scanning.

Testing: tests/test_storage.py::test_expire_stale_scanner_with_divergent_dicts (case 1) and ::test_expire_stale_scanner_with_missing_keys (case 2, asserts the healthy scanner survives and the bad one is dropped). Full suite: 533 passed, ruff + format clean.


Quality Report

Changes: 10 files changed, 140 insertions(+), 760 deletions(-)

Code scan: clean

Tests: failed (FAILED)

Branch hygiene: clean

Generated by Kōan

The cache-expiry pass is driven by the timestamps dict, but deleted
matching entries from the ad-datas dict with an unconditional del. A
corrupt or shape-divergent stored blob (address in timestamps, missing
from ad-datas) raised KeyError, aborting the entire discovery-cache load
and forcing a slow cold adapter start for every device.

Use pop(address, None) so one stale entry is dropped instead of taking
down the whole load.
@codecov

codecov Bot commented Jun 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.59%. Comparing base (0f1ff3e) to head (2841bb6).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #533      +/-   ##
==========================================
+ Coverage   98.66%   99.59%   +0.93%     
==========================================
  Files          15       15              
  Lines        2689     2694       +5     
  Branches      386      386              
==========================================
+ Hits         2653     2683      +30     
+ Misses         12        1      -11     
+ Partials       24       10      -14     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@codspeed-hq

codspeed-hq Bot commented Jun 7, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 5 skipped benchmarks1


Comparing bluetoothbot:koan/storage-expiry-divergent-dict-guard (2841bb6) with main (ac56ebe)

Open in CodSpeed

Footnotes

  1. 5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

The expiry pass hard-subscripted the per-scanner top-level keys
(expire_seconds / timestamps / ad-datas), so a single corrupt or
partial scanner blob raised KeyError and aborted expiry for every
other scanner before the cache reached _from_dict. Wrap the per-scanner
fetch in try/except and discard just the bad scanner, mirroring the
discard-and-rebuild strategy _from_dict already uses for malformed data.
The regression test's data dict was inferred as dict[str, object], which
made the typeddict-item ignore unused, broke the arg type to
expire_stale_scanner_discovered_device_advertisement_data, and made the
'in' membership check operate on object. Annotate it explicitly as
dict[str, DiscoveredDeviceAdvertisementDataDict] so the malformed-scanner
literal is checked against the TypedDict (using the ignore) and the
healthy scanner indexes cleanly.
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