Synchronize critical section in API for each user in JavaScript

Viewed 203

I wanted to swap a profile picture of a user. For this, I have to check the database to see if a picture has already been saved, if so, it should be deleted. Then the new one should be saved and entered into the database.

Here is a simplified (pseudo) code of that:

async function changePic(user, file) {
  // remove old pic
  if (await database.hasPic(user)) {
    let oldPath = await database.getPicOfUser(user);
    filesystem.remove(oldPath);
  }
  // save new pic
  let path = "some/new/generated/path.png";
  file = await Image.modify(file);
  await Promise.all([
   filesystem.save(path, file),
   database.saveThatUserHasNewPic(user, path)
  ]);
  return "I'm done!";
}

I ran into the following problem with it:
If the user calls the API twice in a short time, serious errors occur. The database queries and the functions in between are asynchronous, causing that the changes of the first API call weren't applied when the second API checks for a profile pic to delete. So I'm left with a filesystem.remove request for an already unexisting file and an unremoved image in the filesystem.

I would like to safely handle that situation by synchronizing this critical section of code. I don't want to reject requests only because the server hasn't finished the previous one and I also want to synchronize it for each user, so users aren't bothered by the actions of other users.

Is there a clean way to achieve this in JavaScript? Some sort of monitor like you know it from Java would be nice.

1 Answers

You could use a library like p-limit to control your concurrency. Use a map to track the active/pending requests for each user. Use their ID (which I assume exists) as the key and the limit instance as the value:

const pLimit = require('p-limit');

const limits = new Map();

function changePic(user, file) {
  async function impl(user, file) {
    // your implementation from above
  }

  const { id } = user // or similar to distinguish them

  if (!limits.has(id)) {
    limits.set(id, pLimit(1)); // only one active request per user
  }

  const limit = limits.get(id);
  return limit(impl, user, file); // schedule impl for execution
}

// TODO clean up limits to prevent memory leak?
Related