DirtyHairy/async-mutex

Cancel doesn't work as expected

ivstiv opened this issue · 1 comments

It looks like when I cancel a mutex it doesn't throw the E_CANCELLED error and the runExclusive callback finishes.
Here is the code to reproduce it:

const mutex = new Mutex();

const longOperation = async () => {
  // wait 5 seconds
  await new Promise(res => setTimeout(res, 5000));
};

const execute = async (id: number) => {
  console.log("Executing", id);
  if(mutex.isLocked()) {
    console.log("Mutex is locked", id);
    mutex.cancel();
    mutex.release();
  }
  await mutex.runExclusive(async () => {
    await longOperation();
  });
  console.log("Execute finished running", id);
};


void execute(1);
void execute(2);

Output:

Executing 1
Executing 2
Mutex is locked 2
// 5 seconds pass...
Execute finished running 1
Execute finished running 2

Expected output:

Executing 1
Executing 2
Mutex is locked 2
thrown E_CANCELLED error by the first execution
// 5 seconds pass...
Execute finished running 2

Note 1: I am calling the release() because of this behaviour:

Note that while all pending locks are cancelled, a currently held lock will not be revoked. In consequence, the mutex may not be available even after cancel() has been called.

Hi @ivstiv!

What you describe is expected behaviour. Cancelling the mutex will reject all promises that have been acquired by waiters, but will (and cannot) cancel locks that already have been acquired. So, what happens in your example:

  • First call to execute (first timeslice): runExclusive calls acquire, which locks the mutex and returns a resolved promise
  • Second call to execute (still in the same timeslice): all waiters are cancelled (but none are actually cancelled, as acquire already locked the mutex and resolved the promise). The call to release() will force the mutex to become available again, but it cannot cancel the already acquired lock either. runExclusive calls acquire, which locks the mutex again and returns a resolved promise
  • Second and third timeslice: the two invocations of runExclusive continue execution and await the timeout
  • fourth and fifth timeslice: the two timeouts have passed, and console.log is executed twice.

The important parts are:

  • Although acquire returns a promise, the lock actually happens in the same timeslice if the mutex is available when acquire is called.
  • Calling release just forces the mutex to become available again, it cannot cancel resolved promises that were returned by acquire. This is why release should only be called as the consequence of a previously acquired lock.