Improve debounce cooldown (#32161)

This commit is contained in:
Paulus Schoutsen 2020-02-26 11:27:37 -08:00 committed by GitHub
parent 853d6cda25
commit 2a88ae559e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 67 additions and 12 deletions

View File

@ -31,6 +31,7 @@ class Debouncer:
self.immediate = immediate
self._timer_task: Optional[asyncio.TimerHandle] = None
self._execute_at_end_of_timer: bool = False
self._execute_lock = asyncio.Lock()
async def async_call(self) -> None:
"""Call the function."""
@ -42,15 +43,23 @@ class Debouncer:
return
if self.immediate:
await self.hass.async_add_job(self.function) # type: ignore
else:
self._execute_at_end_of_timer = True
# Locked means a call is in progress. Any call is good, so abort.
if self._execute_lock.locked():
return
self._timer_task = self.hass.loop.call_later(
self.cooldown,
lambda: self.hass.async_create_task(self._handle_timer_finish()),
)
if not self.immediate:
self._execute_at_end_of_timer = True
self._schedule_timer()
return
async with self._execute_lock:
# Abort if timer got set while we're waiting for the lock.
if self._timer_task:
return
await self.hass.async_add_job(self.function) # type: ignore
self._schedule_timer()
async def _handle_timer_finish(self) -> None:
"""Handle a finished timer."""
@ -63,10 +72,21 @@ class Debouncer:
self._execute_at_end_of_timer = False
try:
await self.hass.async_add_job(self.function) # type: ignore
except Exception: # pylint: disable=broad-except
self.logger.exception("Unexpected exception from %s", self.function)
# Locked means a call is in progress. Any call is good, so abort.
if self._execute_lock.locked():
return
async with self._execute_lock:
# Abort if timer got set while we're waiting for the lock.
if self._timer_task:
return # type: ignore
try:
await self.hass.async_add_job(self.function) # type: ignore
except Exception: # pylint: disable=broad-except
self.logger.exception("Unexpected exception from %s", self.function)
self._schedule_timer()
@callback
def async_cancel(self) -> None:
@ -76,3 +96,11 @@ class Debouncer:
self._timer_task = None
self._execute_at_end_of_timer = False
@callback
def _schedule_timer(self) -> None:
"""Schedule a timer."""
self._timer_task = self.hass.loop.call_later(
self.cooldown,
lambda: self.hass.async_create_task(self._handle_timer_finish()),
)

View File

@ -15,20 +15,24 @@ async def test_immediate_works(hass):
function=CoroutineMock(side_effect=lambda: calls.append(None)),
)
# Call when nothing happening
await debouncer.async_call()
assert len(calls) == 1
assert debouncer._timer_task is not None
assert debouncer._execute_at_end_of_timer is False
# Call when cooldown active setting execute at end to True
await debouncer.async_call()
assert len(calls) == 1
assert debouncer._timer_task is not None
assert debouncer._execute_at_end_of_timer is True
# Canceling debounce in cooldown
debouncer.async_cancel()
assert debouncer._timer_task is None
assert debouncer._execute_at_end_of_timer is False
# Call and let timer run out
await debouncer.async_call()
assert len(calls) == 2
await debouncer._handle_timer_finish()
@ -36,6 +40,14 @@ async def test_immediate_works(hass):
assert debouncer._timer_task is None
assert debouncer._execute_at_end_of_timer is False
# Test calling doesn't execute/cooldown if currently executing.
await debouncer._execute_lock.acquire()
await debouncer.async_call()
assert len(calls) == 2
assert debouncer._timer_task is None
assert debouncer._execute_at_end_of_timer is False
debouncer._execute_lock.release()
async def test_not_immediate_works(hass):
"""Test immediate works."""
@ -48,23 +60,38 @@ async def test_not_immediate_works(hass):
function=CoroutineMock(side_effect=lambda: calls.append(None)),
)
# Call when nothing happening
await debouncer.async_call()
assert len(calls) == 0
assert debouncer._timer_task is not None
assert debouncer._execute_at_end_of_timer is True
# Call while still on cooldown
await debouncer.async_call()
assert len(calls) == 0
assert debouncer._timer_task is not None
assert debouncer._execute_at_end_of_timer is True
# Canceling while on cooldown
debouncer.async_cancel()
assert debouncer._timer_task is None
assert debouncer._execute_at_end_of_timer is False
# Call and let timer run out
await debouncer.async_call()
assert len(calls) == 0
await debouncer._handle_timer_finish()
assert len(calls) == 1
assert debouncer._timer_task is not None
assert debouncer._execute_at_end_of_timer is False
# Reset debouncer
debouncer.async_cancel()
# Test calling doesn't schedule if currently executing.
await debouncer._execute_lock.acquire()
await debouncer.async_call()
assert len(calls) == 1
assert debouncer._timer_task is None
assert debouncer._execute_at_end_of_timer is False
debouncer._execute_lock.release()