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,19 +99,20 @@ 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):
# see https://github.com/ldo/dbussy/issues/15
await asyncio.sleep(0.1)
else:
await asyncio.sleep(sleep) await asyncio.sleep(sleep)
except asyncio.CancelledError: me["sleeping"] = False
return
fn.running = None # From here, we do not expect to be cancelled. Ensure only
# one of us is running.
async with fn.lock:
try: try:
return await fn(*args, **kwargs) return await fn(*args, **kwargs)
except Exception as e: except Exception as e:
@ -124,22 +125,32 @@ def dampen(sleep, *, unless=None, retry=0):
retry, retry,
str(e), str(e),
) )
if fn.running is not None: # Run again, unless we have something already scheduled
if fn.last_task["sleeping"]:
return return
fn.running = asyncio.create_task( fn.last_task = dict(sleeping=True)
fn_now(retry, *args, **kwargs) fn.last_task["task"] = asyncio.create_task(
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?