Skip to content

fix(coordinator): coalesce parent-child sync notifications to dirty locks - #707

Open
tykeal wants to merge 4 commits into
FutureTense:mainfrom
tykeal:fix/coalesce-parent-child-sync-notifications
Open

fix(coordinator): coalesce parent-child sync notifications to dirty locks#707
tykeal wants to merge 4 commits into
FutureTense:mainfrom
tykeal:fix/coalesce-parent-child-sync-notifications

Conversation

@tykeal

@tykeal tykeal commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Delivers both #683 and #684 as a single releasable unit. Issue #683's description explicitly states "Do not ship this without task-10's quick/debounce scoping" (task-10 being #684), so both are bundled here.

What was already done by #695

PR #695 had already delivered 3 of #683's 5 acceptance criteria:

  • override_parent children not mutated/notified (continue guard in _update_child_code_slots; guards in set_pin_on_lock/clear_pin_from_lock)
  • Child-unlock parent access-limit decrement notifies the parent coordinator
  • Deferred, scoped fan-out (_defer_refresh_listener_updates, _active_refresh_count, _pending_notify_entry_ids, flush on refresh boundary)

This PR implements only the genuinely outstanding parts. Reviewers should not expect a larger diff — the foundation was already in place.

Commits

1. f7b9c18 — fix(coordinator): coalesce parent-child sync notifications to dirty locks

Adds _parent_sync_transaction, an @asynccontextmanager that suppresses per-slot async_schedule_keymaster_notifications fan-out during the _sync_child_locks child loop. Entry IDs are accumulated in a transaction-scoped set[str] and flushed as a single notification on exit. The context manager is re-entrant (depth counter), exception-safe (finally block), and preserves all_entry_ids=True semantics through the deferral.

Design decision (repo owner): Intermediate Synced.ADDING/Synced.DELETING states and sync_op_started_at are still written to the model exactly as before. Only the notification fan-out is suppressed. This deliberately preserves stuck-state recovery (TestSyncPinStuckStateRecovery) and provider-retry logic, which depend on those fields being set before the provider I/O call.

2. b47dc6a — fix(coordinator): scope quick and debounced refresh to per-entry timers

Replaces the global _quick_refresh: bool and single _cancel_quick_refresh/_cancel_debounced_refresh handles with per-entry equivalents:

  • _quick_refresh_entry_ids: set[str] — tracks which entries need a follow-up refresh
  • _cancel_quick_refresh: dict[str, Callable] — per-entry quick-refresh timer handles
  • _cancel_debounced_refresh: dict[str, Callable] — per-entry debounce timer handles

Each set_pin_on_lock/clear_pin_from_lock/_sync_pin/_update_child_code_slots now records only the affected entry ID. Per-entry timers fire through async_refresh_lock(entry_id), which serialises via self._debounced_refresh.async_lock() — the same asyncio lock from the parent DataUpdateCoordinator. This guarantees no independent simultaneous polling is reintroduced.

Shutdown and _delete_lock cancel all/per-entry handles respectively, preventing dangling async_call_later callbacks.

3. e534907 — test(coordinator): tighten per-entry refresh assertions and fix stale test name

Replaces truthiness-only assertions on _quick_refresh_entry_ids (e.g. assert coordinator._quick_refresh_entry_ids) with exact set equality (e.g. == {"child_id"}). This catches wrong-entry-ID bugs that the old assertions would miss. Also renames test_scoped_refresh_keeps_shared_debounce_for_other_dirty_lockstest_scoped_refresh_preserves_other_entry_debounce_timer to reflect the per-entry design.

Validation

  • pytest tests/: 1092 passed, 1 deselected
  • ruff check custom_components/ tests/: All checks passed
  • ruff format --check custom_components/ tests/: All formatted
  • mypy custom_components/keymaster/: No issues found

Mutation testing

All new tests were mutation-verified. Key mutations validated:

  • Removing the transaction check from async_schedule_keymaster_notifications
  • Removing the finally depth decrement
  • Removing the coalesced flush at transaction exit
  • Recording the parent entry ID instead of the child's at _update_child_code_slots
  • Skipping individual cancel() calls during shutdown
  • Cancelling ALL handles when only one entry's should be cancelled

Closes #683
Closes #684

@tykeal
tykeal requested a lite review from Copilot August 8, 2026 17:10
@github-actions github-actions Bot added the bugfix Fixes a bug label Aug 8, 2026
@codecov-commenter

codecov-commenter commented Aug 8, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.25%. Comparing base (cdb4922) to head (17e1ba6).
⚠️ Report is 230 commits behind head on main.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@             Coverage Diff             @@
##             main     #707       +/-   ##
===========================================
+ Coverage   84.14%   94.25%   +10.10%     
===========================================
  Files          10       42       +32     
  Lines         801     5480     +4679     
  Branches        0       30       +30     
===========================================
+ Hits          674     5165     +4491     
- Misses        127      315      +188     
Flag Coverage Δ
python 94.15% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This comment was marked as outdated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

@tykeal
tykeal force-pushed the fix/coalesce-parent-child-sync-notifications branch from e0381cf to 16d633a Compare August 10, 2026 11:34
tykeal and others added 4 commits August 10, 2026 07:27
…ocks

Add a _parent_sync_transaction async context manager that suppresses
per-slot async_schedule_keymaster_notifications fan-out during the
_sync_child_locks loop. Instead, entry IDs are accumulated in a
transaction-scoped set and flushed as a single notification on exit.

The transaction is:
- Re-entrant via a depth counter (nested uses share the accumulator)
- Exception-safe via try/finally (depth always decrements)
- Preserves all_entry_ids=True semantics through the deferral

This eliminates O(slots × children) notification scheduling during
parent→child synchronization, replacing it with exactly one coalesced
notification per sync cycle.

Closes FutureTense#683

Assisted-by: GitHub Copilot CLI 1.0.75 (Claude Opus 5, model claude-opus-5)
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the global _quick_refresh bool and single cancel handles with
per-entry equivalents:

- _quick_refresh_entry_ids: set[str] tracks which entries need refresh
- _cancel_quick_refresh: dict[str, Callable] holds per-entry timer handles
- _cancel_debounced_refresh: dict[str, Callable] holds per-entry debounce handles

Each set_pin_on_lock / clear_pin_from_lock / _sync_pin / _update_child_code_slots
now records only the affected entry ID. Timers fire per-entry through
async_refresh_lock(entry_id) which serialises via _debounced_refresh.async_lock(),
preventing independent simultaneous polling.

Shutdown and _delete_lock cancel all/per-entry handles respectively,
preventing dangling timers.

Closes FutureTense#684

Assisted-by: GitHub Copilot CLI 1.0.75 (Claude Opus 5, model claude-opus-5)
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… test name

- Replace truthiness-only assertions on _quick_refresh_entry_ids with
  exact set equality (e.g. == {"child_id"}) to catch wrong-entry bugs
- Rename test_scoped_refresh_keeps_shared_debounce_for_other_dirty_locks
  to test_scoped_refresh_preserves_other_entry_debounce_timer to reflect
  the per-entry design

Assisted-by: GitHub Copilot CLI 1.0.75 (Claude Opus 5, model claude-opus-5)
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add tests for three previously-uncovered lines:
- _trigger_debounced_refresh_for_entry: "_global" sentinel and
  lock-removed-before-timer-fires both fall back to full refresh
- _clear_pending_quick_refresh(None): cancels ALL per-entry handles
- _schedule_quick_refresh_if_needed: dedup guard skips entries that
  already have a pending timer (identity assertion on stored handle)

Assisted-by: GitHub Copilot CLI 1.0.75 (Claude Opus 5, model claude-opus-5)
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tykeal
tykeal force-pushed the fix/coalesce-parent-child-sync-notifications branch from 16d633a to 17e1ba6 Compare August 10, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fixes a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix: Scope quick and debounced refresh to Keymaster lock entries Fix: Coalesce parent-child sync notifications to dirty locks

3 participants