BattlefyBlogHistoryOpen menu
Close menuHistory

Limiting concurrency with promises

Ronald Chen September 26th 2022

One of the reasons why Javascript has become such a success is due to its lack of concurrency problems as a result of the awesome event loop.

However, when talking to external systems one may run into concurrency problems all over again.

For example, let's say we have some-command that does not support concurrency and we happen to be calling in a request.

app.get(async (req, res, next) => {
  ...
  const {stdout} = await exec('some-command');
  ...
});

How do we ensure our Node.js backend serializes calls to some-command?

Whenever I have a concurrency problem, the first thing I reach for is throat. But throat is tricky to use here. In order to use throat we would need to gather all the requests to some-command into a common queue, then re-map it back to the request. This is doable, but there is a simpler way.

Return of the mutex

What we really want is mutual exclusion, aka a mutex. What would be nice is if the mutex was just a simple promise.

app.get(async (req, res, next) => {
  ...
  try {
    await acquire();
    const {stdout} = await exec('some-command');
    ...
  } finally {
    release();
  }
  ...
});

Now if there are multiple concurrent requests, they would all attempt to resolve acquire, but if we only let one through at a time, we have achieved our goal.

There are a few ways to implement acquire/release and the implementations I've found tend to be really bad. Often they are overly complicated using EventEmitter or pointlessly implemented with class.

Here is how I would implement acquire/release

let semaphore;
let unlock;
const acquire = async () => {
  await semaphore;
  semaphore = new Promise((resolve) => {
    unlock = resolve;
  });
};

const release = () => {
  unlock();
  semaphore = undefined;
  unlock = undefined;
};

Note await undefined does resolve, which is why await semaphore is fine.

Making it nice

That showed the mechanics, but this code isn't reusable. Also the try/finally is annoying to write every single time. We can clean all this up with a factory function.

const Mutex = () => {
  let semaphore;
  let unlock;
  const acquire = async () => {
    await semaphore;
    semaphore = new Promise((resolve) => {
      unlock = resolve;
    });
  };

  const release = () => {
    unlock();
    semaphore = undefined;
    unlock = undefined;
  };

  return async (closure) => {
    try {
      await acquire();
      return closure();
    } finally {
      release();
    }
  };
};

Then the usage would look like,

const someCommandMutex = Mutex();

app.get(async (req, res, next) => {
  ...
  await someCommandMutex(async () => {
    const {stdout} = await exec('some-command');
    ...
  });
  ...
});

Can you gork race conditions? We'd like to hear from you, Battlefy is hiring.

Don't break the Internet
September 19th 2022
Smuggling web standards
October 3rd 2022

2024

  • MLBB Custom Lobby Feature
    Stefan Wilson - April 16th 2024

2023

2022

Powered by
BATTLEFY