Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timing bug inside semaphore for isLocked() #54

Closed
CMCDragonkai opened this issue Apr 1, 2022 · 7 comments
Closed

Timing bug inside semaphore for isLocked() #54

CMCDragonkai opened this issue Apr 1, 2022 · 7 comments

Comments

@CMCDragonkai
Copy link

CMCDragonkai commented Apr 1, 2022

I've discovered a timing bug involving withTimeout.

import { Mutex, withTimeout } from 'async-mutex';

async function main () {

  const lock = new Mutex();

  const release = await lock.acquire();

  const lock2 = withTimeout(lock, 100);

  try {
    await lock2.acquire();
  } catch (e) {
    console.log(e.message);
  }

  const lock3 = withTimeout(lock, 100);

  try {
    await lock3.acquire();
  } catch (e) {
    console.log(e.message);
  }

  release();

  console.log(lock.isLocked());

  setTimeout(() => {
    console.log(lock.isLocked());
  }, 0);

}

main();

Running with ts-node shows this:

timeout while waiting for mutex to become available
timeout while waiting for mutex to become available
true
false

We should expect that console.log(lock.isLocked()) to be false as soon as I've called release().

Note that this error goes away as soon as I remove the withTimeout attempts.

I've traced this to:

    Semaphore.prototype.isLocked = function () {
        return this._value <= 0;
    };

The _value property is still 0, and it appears the value isn't properly incremented until some ticks pass by. Which is why the setTimeout shows that the lock becomes unlocked afterwards.

So far, in order to fix this, I had to add a 0 ms sleep after my release calls.

@CMCDragonkai
Copy link
Author

CMCDragonkai commented Apr 1, 2022

My node version:

[nix-shell:~/Projects/js-async-locks]$ node --version
v14.17.3

And ts-node version:

      "ts-node": "^10.4.0",

@CMCDragonkai
Copy link
Author

Could this be related to #52?

@CMCDragonkai
Copy link
Author

I found that yield by the event loop using queueMicrotask is more efficient to solve this problem.

@DirtyHairy
Copy link
Owner

Hm, I wouldn't classify this as a bug, there is no guarantee that the releasing the mutex will make it available immediately as there may be other waiters queued. This is precisely what happens here: withTimeout wraps acquire such that it will first acquire the mutex and then release it immediately if the timeout has passed.

@CMCDragonkai
Copy link
Author

CMCDragonkai commented Apr 1, 2022

there is no guarantee that the releasing the mutex will make it available immediately as there may be other waiters queued

Well that's surprising to me. I thought that would be the case. Anyway I made the changes to my library abstraction to ensure this is the case by waiting for microtasks to finish during release. MatrixAI/js-async-locks#3

In more detail... given that there are no other waiters on the same lock, I expected that it would ensure that it would unlocked at that point.

@DirtyHairy
Copy link
Owner

But that's the point here, there are other waiters, as the withTimeout wrapper is still waiting for the mutex to be released, it's just the code that has called it that has moved on. Do you have a usecase where this is a real problem?

@CMCDragonkai
Copy link
Author

Yea that makes sense, I looked at the withTimeout decorator, even after rejecting the promise with the timeout error, it would still end up waiting for the mutex to be released. Ultimately this is due to the lack of cancellable promises in native JS. So I'll close the issue for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants