Redfour: Binary Semaphores in Redis and Node

SHARE ON

We just released a small module called Redfour that could be very helpful to you if you’re using Node in a distributed system. It implements a binary semaphore using Redis.

What’s a binary semaphore? It’s a pattern that ensures that only one process (e.g. server in your cloud) has access to a critical section of code at a time. All other processes must wait for access. When the first process is done, another process will be notified and given access.

Example

Let’s say you have Node code that checks for expired OAuth2 access tokens and refreshes them, like this:

function getAccessToken(userId, done) {
  var token = getTokenForUser(userId);
  if (token.expires < Date.now()) {
    done(null, token);
  } else {
    refreshToken((err, res) => {
       if (err) done(err);
       else done(null, newToken);
    });
  }
}

Now let’s say your user is loading part of your app that uses several APIs behind-the-scenes, each of which need a fresh OAuth2 access token to a service. Those APIs (which might be implemented in different services) are all going to call getAccessToken at the same time – resulting in refreshToken being called needlessly. A better approach is for the processes to take turns asking for the token, so the first calls refreshToken and the rest get the cached result.

This is where Redfour can help. This same code using Redfour ensures only one process has access to the critical codepath at once, so refreshToken will only be called once.

var accessTokenLock = new Lock({
  redis: 'redis://localhost:6846',
  namespace: 'getAccessToken'
});

function getAccessToken(userId, done) {
  // Make sure only one user is being refreshed at a time.
  var lockTTL = 60 * 1000;
  var waitTTL = 30 * 1000;
  accessTokenLock.waitAcquireLock(userId, lockTTL, waitTTL, (err, lock) => {
    if (err || !lock.success) return done(new Error('Could not get lock!'));

    var token = getTokenForUser(userId);
    if (token.expires < Date.now()) {
      accessTokenLock.releaseLock(lock, (err) => {
        if (err) done(err);
        else done(null, token);
      });

    } else {
      refreshToken((err, newToken) => {
        if (err) return done(err);

        accessTokenLock.releaseLock(lock, (err) => {
          if (err) done(err);
          else done(null, newToken);
        });
      });
    }
  });
}

How does this differ from other locking solutions?

Other locking solutions such as the popular warlock use polling to wait for a lock to be released. This is inefficient and slow, as there might be a delay when being notified that the lock is available.

Credit

Credit for this design goes to Andris Reinman who worked with us on our email backend fixing several tricky race conditions.

SHARE ON

Written By

Chuy Martinez

Chuy Martinez

From Your Friends At