Created
April 8, 2019 13:35
-
-
Save bengourley/5cf8ad16adf7d4a9da9b78deff8df8a5 to your computer and use it in GitHub Desktop.
Solving the stampede/dog-piling problem in JS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// simple cache implementation provide a key that you're looking for and | |
// a function that will compute the value in case of a cache miss | |
async get (key, expensiveFn) { | |
let result = await storage.get(key) | |
if (result === undefined) { | |
result = await expensiveFn | |
await storage.save(result) | |
} | |
return result | |
} | |
// in the consumer module… | |
get('ab', expensiveFn) | |
get('ab', expensiveFn) | |
get('ab', expensiveFn) | |
// how do we stop these three calls from attempting to run the expensive function three times? | |
// this is a problem known as the "cache stampede" or "dog-piling" problem | |
// other languages that have threads solve this kind of synchronization issue | |
// with locking/semaphore, but what do we do in js? | |
// because the cache get function is async, which creates promises under the hood, | |
// we have low level async flow control which we can piggy back off of that rather | |
// than building something from scratch | |
// use this to save the in-flight get calls | |
const pending = new Map() | |
async get(key, expensiveFn) { | |
// first look for an in-flight call for this key | |
let inFlight = pending.get('key') | |
// if one exists, simply return that promise, which will | |
// be resolved/rejected when the in-flight call ends | |
if (inFlight) return inFlight | |
// if not, call through to the original get logic and have it prime the cache | |
let inFlight = _get(key, expensiveFn) | |
// when this promise completes we need to remove it from the pending map | |
.then(() => { pending.remove(key) }) | |
// and also when it errors, but we don't want to swallow the error, so rethrow it | |
.catch(() => { | |
pending.remove(key) | |
throw e | |
}) | |
// add the promise returned from _get into the pending map | |
pending.set(key, inFlight) | |
// return it so that callers of this function can treat it as "async" | |
return inFlight | |
} | |
// the actual implementation | |
async _get (key, expensiveFn) { | |
let result = await storage.get(key) | |
if (result === undefined) { | |
result = await expensiveFn | |
await storage.save(result) | |
} | |
return result | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment