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

@secondof9 secondof9 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📋 Review Summary

Tip

Review Status: 🟢 APPROVED
Change Type: 🛠️ Refactor / 🐛 Bug Fix
Review Effort: 🟢 Low
Core Impact: Clean coalescing of per-entry refresh timers and parent-child sync notifications. Defensive design with zero runtime hazards detected.


🚦 CI & Pipeline Health Summary

Check / Workflow Name Status Impact on Review
coverage ✅ PASSED 94.25% — no regressions
Pytest (3.14) ✅ PASSED All 1092 tests passed, 1 deselected
Autolabel PR ⏩ SKIPPED Non-blocking
Prek ✅ PASSED Static analysis clean
Hassfest Validation ✅ PASSED Integration schema valid
HACS Validation ✅ PASSED Custom component compliant
Autolabel PR (2nd) ✅ PASSED Non-blocking

Note

CI Pipeline Clear: All GitHub Actions workflows completed successfully.


🔍 Architectural Walkthrough

custom_components/keymaster/coordinator.py
  • _parent_sync_transaction context manager (L396-417)

    • Exception-safe re-entrant depth counter
    • Accumulates dirty entry IDs during parent-child sync loop
    • Flushes notification exactly once on outermost exit
    • Preserves all_entry_ids=True semantics through transaction
    • ✅ No silent resource leaks — finally block handles depth decrement
  • Per-entry refresh state (L224-228, 235-238)

    • Replaces global _quick_refresh: bool with dict[str, Callable] for quick-refresh and debounced-refresh
    • Prevents independent simultaneous polling of locks
    • Shutdown cleanly cancels all per-entry handles
    • ✅ Correct asyncio task lifecycle — no dangling timers
  • _cancel_entry_refresh_timers callback (L2629-2640)

    • Single-point cleanup for quick-refresh + debounced-refresh + pending sets
    • Called from _delete_lock to prevent dangling timers
    • ✅ Proper cleanup boundary
  • _async_refresh_lock_data and async_refresh_all_locks

    • Clear per-entry timers before/after refresh operations
    • _async_update_data calls _clear_pending_quick_refresh(None) for full refresh
    • ✅ No orphaned async_call_later callbacks

🔗 Related / Outside-Diff Context

  • tests/test_coordinator.py: Per-entry refresh assertions upgraded from truthiness (assert coordinator._quick_refresh_entry_ids) to exact set equality (assert coordinator._quick_refresh_entry_ids == {"child_id"}). Tighter test coverage, catches wrong-entry-ID bugs the old assertions would miss.

  • tests/test_coordinator_sync.py: New TestParentSyncTransaction class with 6 tests covering re-entrancy, exception safety, override-parent guard, and all_entry_ids preservation. Comprehensive coverage of the new transaction mechanism.

  • tests/test_debounce.py: New TestPerEntryQuickRefresh and TestPerEntryRefreshEdgeCases classes. Tests for dedup guards, reschedule prevention, and global-fallback paths. ✅ Mutation testing verified all new tests.


Review: APPROVED — fresh review of FutureTense/keymaster PR #707 (fix/coalesce-parent-child-sync-notifications). All CI green, no unresolved review threads, well-structured code with proper async boundaries and cleanup.

@firstof9 firstof9 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good.

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

5 participants