Skip to content

Instantly share code, notes, and snippets.

@bengourley
Created April 8, 2019 13:35
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bengourley/5cf8ad16adf7d4a9da9b78deff8df8a5 to your computer and use it in GitHub Desktop.
Save bengourley/5cf8ad16adf7d4a9da9b78deff8df8a5 to your computer and use it in GitHub Desktop.
Solving the stampede/dog-piling problem in JS
// 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