i3-companion: fix dampening

Cancellation is asynchronous. So, there was a race condition where we
were throwing away the task we just scheduled. Don't really on
cancellation for synchronization. We also want to have only one
instance running. So, use locks to ensure only one instance is running
and only cancel running functions while in sleeping phase, otherwise,
let them run.

Currently, this OK, however, it is assumed the function has somehow
the same effect whatever the arguments we provide. This is true for
the two callbacks we use `@dampen` on.
This commit is contained in:
Vincent Bernat 2021-07-12 23:29:56 +02:00
parent 2012ba0c15
commit 9b0bb1ce76

View file

@ -99,47 +99,58 @@ def on(*events):
def dampen(sleep, *, unless=None, retry=0): def dampen(sleep, *, unless=None, retry=0):
"""Dampen a function call.""" """Dampen a function call. Optional retry on failure. Ensure only one
instance is executed. It is assumed the arguments provided to the
dampened function have no effect on its execution.
"""
def decorator(fn): def decorator(fn):
async def fn_now(retry, *args, **kwargs): async def fn_now(me, retry, *args, **kwargs):
try: if unless is None or not unless(*args, **kwargs):
if unless is not None and unless(*args, **kwargs): await asyncio.sleep(sleep)
# see https://github.com/ldo/dbussy/issues/15 me["sleeping"] = False
await asyncio.sleep(0.1)
else: # From here, we do not expect to be cancelled. Ensure only
await asyncio.sleep(sleep) # one of us is running.
except asyncio.CancelledError: async with fn.lock:
return try:
fn.running = None return await fn(*args, **kwargs)
try: except Exception as e:
return await fn(*args, **kwargs) if not retry:
except Exception as e: logger.exception(f"while executing {fn}: %s", e)
if not retry: return
logger.exception(f"while executing {fn}: %s", e) retry -= 1
return logger.warning(
retry -= 1 f"while executing {fn} (remaining tries: %d): %s",
logger.warning( retry,
f"while executing {fn} (remaining tries: %d): %s", str(e),
retry, )
str(e), # Run again, unless we have something already scheduled
) if fn.last_task["sleeping"]:
if fn.running is not None: return
return fn.last_task = dict(sleeping=True)
fn.running = asyncio.create_task( fn.last_task["task"] = asyncio.create_task(
fn_now(retry, *args, **kwargs) fn_now(fn.last_task, retry, *args, **kwargs)
) )
@functools.wraps(fn) @functools.wraps(fn)
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
if fn.running is not None: # Initialize a lock (we need an active loop for that)
logger.debug(f"cancel call to previous {fn}") if fn.lock is None:
fn.running.cancel() fn.lock = asyncio.Lock()
fn.running = None
logger.debug(f"dampening call to {fn}")
fn.running = asyncio.create_task(fn_now(retry, *args, **kwargs))
fn.running = None # If possible, cancel last task if it's sleeping
if fn.last_task is not None and fn.last_task["sleeping"]:
logger.debug(f"cancel call to {fn}")
fn.last_task["task"].cancel()
logger.debug(f"dampening call to {fn}")
fn.last_task = dict(sleeping=True)
fn.last_task["task"] = asyncio.create_task(
fn_now(fn.last_task, retry, *args, **kwargs)
)
fn.last_task = None
fn.lock = None
return wrapper return wrapper
return decorator return decorator
@ -254,7 +265,7 @@ async def worksplace_exclusive(i3, event):
# Can the new window just intrude? # Can the new window just intrude?
if can_intrude(w): if can_intrude(w):
logger.debug("window {w.name} can intrude") logger.debug(f"window {w.name} can intrude")
return return
# Does the current workspace contains an exclusive app? # Does the current workspace contains an exclusive app?