1

I use a semaphore for two processes that share a resource (rest api endpoint), that can't be called concurrent. I do:

let tokenSemaphore = null;

class restApi {
    async getAccessToken() {
        let tokenResolve;
        if (tokenSemaphore) {
            await tokenSemaphore;
        }
        tokenSemaphore = new Promise((resolve) => tokenResolve = resolve);

        return new Promise(async (resolve, reject) => {
            // ...

            resolve(accessToken);

            tokenResolve();
            tokenSemaphore = null;
        });
    }
}

But this looks too complicated. Is there a simpler way to achieve the same thing?

And how to do it for more concurrent processes.

Michael
  • 6,823
  • 11
  • 54
  • 84
  • 1
    [Never pass an `async function` as the executor to `new Promise`](https://stackoverflow.com/q/43036229/1048572)! – Bergi May 11 '21 at 13:15
  • 1
    Have a look at [this](https://stackoverflow.com/a/51086910/1048572) – Bergi May 11 '21 at 13:17
  • This question reveals many misunderstandings of promises and `async`/`await`. This wildly popular question, [_"how do I return the response from an asynchronous call?"_](https://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call) should help. See [this Q&A](https://stackoverflow.com/a/67342850/633183) to unravel some of your confusions about `async` and `await`. – Mulan May 11 '21 at 13:19

2 Answers2

1

This is not a server side Semaphore. You need interprocess communication for locking processes which are running independently in different threads. In that case the API must support something like that on the server side and this here is not for you.

As this was the first hit when googling for "JavaScript Promise Semaphore", here is what I came up with:

function Semaphore(max, fn, ...a1)
{
  let run = 0;
  const waits = [];
  function next(x)
    {
      if (run<max && waits.length)
        waits.shift()(++run);
      return x;
    }
  return (...a2) => next(new Promise(ok => waits.push(ok)).then(() => fn(...a1,...a2)).finally(_ => run--).finally(next));
}

Example use (above is (nearly) copied from my code, following was typed in directly and hence is not tested):

// do not execute more than 20 fetches in parallel:
const fetch20 = Semaphore(20, fetch);

async function retry(...a)
{
  for (let retries=0;; retries++)
    {
      if (retries)
        await new Promise(ok => setTimeout(ok, 100*retries));
      try {
        return await fetch20(...a)
      } catch (e) {
        console.log('retry ${retries}', url, e);
      }
    }
}

and then

for (let i=0; ++i<10000000; ) retry(`https://example.com/?${i}`);

My Browser handles thousands of asynchronous parallel calls to retry very well. However when using fetch directly, the Tabs crash nearly instantly.

For your usage you probably need something like:

async function access_token_api_call()
{
  // assume this takes 10s and must not be called in parallel for setting the Cookie
  return fetch('https://api.example.com/nonce').then(r => r.json());
}

const get_access_token = Semaphore(1, access_token_api_call);

// both processes need to use the same(!) Semaphore, of course

async function process(...args)
{
  const token = await get_access_token();
  // processing args here
  return //something;
}

proc1 = process(1);
proc2 = process(2);
Promise.all([proc1, proc2]).then( //etc.

YMMV.

Notes:

This assumes that your two processes are just asynchronous functions of the same single JS script (i.E. running in the same Tab).

A Browser usually does not open more than 5 concurrent connects to a backend and then pipelines excess requests. fetch20 is my workaround for a real-world problem when a JS-Frontend needs to queue, say, 5000 fetches in parallel, which crashes my Browser (for unknown reason). We have 2021 and that should not be any problem, right?

Tino
  • 9,583
  • 5
  • 55
  • 60
0

But this looks too complicated.

Not complicated enough, I'm afraid. Currently, if multiple code paths call getAccessToken when the semaphore is taken, they'll all block on the same tokenSemaphore instance, and when the semaphore is released, they'll all be released and resolve roughly at the same time, allowing concurrent access to the API.

In order to write an asynchronous lock (or semaphore), you'll need a collection of futures (tokenResolvers). When one is released, it should only remove and resolve a single future from that collection.

I played around with it a bit in TypeScript a few years ago, but never tested or used the code. My Gist is also C#-ish (using "dispoables" and whatnot); it needs some updating to use more natural JS patterns.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810