mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 02:24:51 -05:00
Cancel previous Debouncer timer handle in _schedule_timer (#170339)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
Martin Hjelmare
Copilot Autofix powered by AI
parent
3393598d91
commit
a215b82bd9
@@ -193,8 +193,9 @@ class Debouncer[_R_co]:
|
||||
|
||||
@callback
|
||||
def _schedule_timer(self) -> None:
|
||||
"""Schedule a timer."""
|
||||
if not self._shutdown_requested:
|
||||
self._timer_task = self.hass.loop.call_later(
|
||||
self.cooldown, self._on_debounce
|
||||
)
|
||||
"""Schedule a timer, cancelling any previously-scheduled handle."""
|
||||
if self._shutdown_requested:
|
||||
return
|
||||
if self._timer_task is not None:
|
||||
self._timer_task.cancel()
|
||||
self._timer_task = self.hass.loop.call_later(self.cooldown, self._on_debounce)
|
||||
|
||||
@@ -578,3 +578,71 @@ async def test_shutdown_releases_parent_class(hass: HomeAssistant) -> None:
|
||||
# Debouncer shutdown releases the class
|
||||
debouncer.async_shutdown()
|
||||
assert my_class_weak_ref() is None
|
||||
|
||||
|
||||
async def test_schedule_timer_cancels_previous_handle(hass: HomeAssistant) -> None:
|
||||
"""Ensure _schedule_timer cancels any previously-scheduled handle."""
|
||||
# Use a large cooldown so the scheduled timer can't fire mid-test on a slow
|
||||
# event loop; the timer is only inspected and cancelled, never awaited.
|
||||
debouncer = debounce.Debouncer(
|
||||
hass,
|
||||
_LOGGER,
|
||||
cooldown=3600.0,
|
||||
immediate=True,
|
||||
function=AsyncMock(),
|
||||
)
|
||||
|
||||
debouncer._schedule_timer()
|
||||
first_handle = debouncer._timer_task
|
||||
assert first_handle is not None
|
||||
assert not first_handle.cancelled()
|
||||
|
||||
debouncer._schedule_timer()
|
||||
second_handle = debouncer._timer_task
|
||||
assert second_handle is not None
|
||||
assert second_handle is not first_handle
|
||||
assert first_handle.cancelled()
|
||||
|
||||
debouncer.async_shutdown()
|
||||
|
||||
|
||||
async def test_concurrent_async_call_does_not_orphan_timer(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Concurrent async_call during in-flight execution must not orphan a timer."""
|
||||
started = asyncio.Event()
|
||||
can_finish = asyncio.Event()
|
||||
|
||||
async def slow_function() -> None:
|
||||
started.set()
|
||||
await can_finish.wait()
|
||||
|
||||
# Use a large cooldown so the T1 timer scheduled below can't fire before
|
||||
# the in-flight call completes; cancellation is verified deterministically.
|
||||
debouncer = debounce.Debouncer(
|
||||
hass,
|
||||
_LOGGER,
|
||||
cooldown=3600.0,
|
||||
immediate=True,
|
||||
function=slow_function,
|
||||
)
|
||||
|
||||
in_flight = hass.async_create_task(debouncer.async_call())
|
||||
await started.wait()
|
||||
assert debouncer._timer_task is None
|
||||
|
||||
# The concurrent call hits the locked-immediate branch and schedules T1.
|
||||
await debouncer.async_call()
|
||||
first_timer = debouncer._timer_task
|
||||
assert first_timer is not None
|
||||
assert not first_timer.cancelled()
|
||||
|
||||
# Letting the in-flight call complete schedules T2.
|
||||
can_finish.set()
|
||||
await in_flight
|
||||
second_timer = debouncer._timer_task
|
||||
assert second_timer is not None
|
||||
assert second_timer is not first_timer
|
||||
assert first_timer.cancelled()
|
||||
|
||||
debouncer.async_shutdown()
|
||||
|
||||
Reference in New Issue
Block a user