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.