i3-companion: make debounce decorator composable

This allows us to move the retry behavior into a separate decorator.
The semantic is a bit different as, now, if there is another iteration
pending, it is lost.
This commit is contained in:
Vincent Bernat 2021-07-16 07:56:42 +02:00
parent a5e5b23ec8
commit bf08c327a3

View file

@ -135,14 +135,39 @@ def on(*events):
return decorator return decorator
def debounce(sleep, *, unless=None, retry=0): def retry(max_retries):
"""Debounce a function call (batch successive calls into only one). """Retry an async function."""
Optional immediate execution. Optional retry on failure. Ensure
only one instance is executed. It is assumed the arguments
provided to the debounced function have no effect on its
execution.
""" def decorator(fn):
@functools.wraps(fn)
async def wrapper(*args, **kwargs):
retries = max_retries
while True:
try:
logger.debug(f"execute {fn} (remaining tries: {retries})")
return await fn(*args, **kwargs)
except Exception as e:
if retries > 0:
retries -= 1
logger.warning(
f"while executing {fn} (remaining tries: %d): %s",
retries,
e,
)
else:
logger.exception(f"while executing {fn}: %s", e)
return
return wrapper
return decorator
def debounce(sleep, *, unless=None):
"""Debounce a function call (batch successive calls into only one).
Optional immediate execution. Ensure only one instance is
executed. It is assumed the arguments provided to the debounced
function have no effect on its execution."""
def decorator(fn): def decorator(fn):
async def worker(): async def worker():
@ -155,7 +180,7 @@ def debounce(sleep, *, unless=None, retry=0):
logger.debug(f"urgent work received for {fn}") logger.debug(f"urgent work received for {fn}")
except asyncio.TimeoutError: except asyncio.TimeoutError:
pass pass
retry, args, kwargs = workers[fn].queue args, kwargs = workers[fn].queue
workers[fn].queue = None workers[fn].queue = None
workers[fn].urgent.clear() workers[fn].urgent.clear()
@ -164,26 +189,10 @@ def debounce(sleep, *, unless=None, retry=0):
try: try:
await fn(*args, **kwargs) await fn(*args, **kwargs)
except Exception as e: except Exception as e:
if not retry: logger.debug(f"while running {fn}, worker got %s", e)
logger.exception(f"while executing {fn}: %s", e) workers[fn] = None
return raise
retry -= 1
logger.warning(
f"while executing {fn} (remaining tries: %d): %s",
retry,
str(e),
)
# Retry, unless we have something already scheduled
if workers[fn].queue is not None:
logger.debug(f"retry now with queued event for {fn}")
workers[fn].urgent.set()
continue
logger.debug(f"reschedule retry for {fn}")
workers[fn].queue = (retry, args, kwargs)
if unless is not None and unless(*args, **kwargs):
logger.debug(f"wake up now for retry of {fn}")
workers[fn].urgent.set()
# Do we still have something to do? # Do we still have something to do?
if workers[fn].queue is None: if workers[fn].queue is None:
break break
@ -199,12 +208,13 @@ def debounce(sleep, *, unless=None, retry=0):
workers[fn] = types.SimpleNamespace() workers[fn] = types.SimpleNamespace()
workers[fn].task = asyncio.create_task(worker()) workers[fn].task = asyncio.create_task(worker())
workers[fn].urgent = asyncio.Event() workers[fn].urgent = asyncio.Event()
workers[fn].queue = (retry, args, kwargs) workers[fn].queue = (args, kwargs)
else: else:
logger.debug(f"enqueue new work for {fn}") logger.debug(f"enqueue new work for {fn}")
if unless is not None and unless(*args, **kwargs): if unless is not None and unless(*args, **kwargs):
logger.debug(f"wake up now for {fn}") logger.debug(f"wake up now for {fn}")
workers[fn].urgent.set() workers[fn].urgent.set()
return await workers[fn].task
workers[fn] = None workers[fn] = None
return wrapper return wrapper
@ -624,7 +634,8 @@ async def bluetooth_notifications(
), ),
) )
@static(last=None) @static(last=None)
@debounce(0.2, retry=2) @retry(2)
@debounce(0.2)
async def bluetooth_status(i3, event, *args): async def bluetooth_status(i3, event, *args):
"""Update bluetooth status for polybar.""" """Update bluetooth status for polybar."""
if event is StartEvent: if event is StartEvent:
@ -797,12 +808,12 @@ async def network_manager_notifications(i3, event, path, state, reason):
), ),
) )
@static(last=None) @static(last=None)
@retry(2)
@debounce( @debounce(
1, 1,
unless=lambda i3, event, *args: ( unless=lambda i3, event, *args: (
isinstance(event, DBusSignal) and event.interface.endswith(".Active") isinstance(event, DBusSignal) and event.interface.endswith(".Active")
), ),
retry=2,
) )
async def network_manager_status(i3, event, *args): async def network_manager_status(i3, event, *args):
"""Compute network manager status.""" """Compute network manager status."""