Skip to content

Instantly share code, notes, and snippets.

@nrkn
Last active December 6, 2023 01:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nrkn/e6d3f7fe526713c9ccc570ad9f1e1ecd to your computer and use it in GitHub Desktop.
Save nrkn/e6d3f7fe526713c9ccc570ad9f1e1ecd to your computer and use it in GitHub Desktop.
Try to prevent cache stampedes when using promises
// try to prevent cache stampedes
//
// if you don't do this, then when we get a lot of simultaneous requests for the
// same thing and it's not cached yet, every single request will think it has
// to generate the resource, whereas using this makes it so that the very first
// one triggers the work, and the others either wait on the same promise, or get
// served the cached version if it's already resolved
//
// the effect of having every request try to generate something is to overload
// the server and cause weird side effects, like files not being read or written
// properly
//
// https://en.wikipedia.org/wiki/Cache_stampede
export const cachedPromise = <K, T>(
// the actual function that gets the data
get: (key: K) => Promise<T>,
// should return true if the current key is no longer valid, eg for TTL where
// the item is now too old etc
//
// some items never need to be invalidated, so this fn is optional, omitting
// it creates a cache that persists for the lifecycle of the app, so don't
// use it with ephemeral data or the cache will eat the server's memory
isInvalid?: (key: K, current: K) => boolean
) => {
const cache = new Map<K, Promise<T>>()
// we invalidate previous keys, using the current key as a comparator
const invalidate = (current: K) => {
if( isInvalid === undefined ) return
const keys = cache.keys()
for (const key of keys) {
if (isInvalid(key, current)) {
console.debug('cachedPromise', 'cache expired', key)
cache.delete(key)
}
}
}
// return the existing cached promise or create it if it doesn't exist
const getData = async (key: K) => {
// invalidate if no longer valid, eg older keys when doing TTL etc
invalidate(key)
const data = cache.get(key)
// sweet - this is what we want to see in the logs as much as possible
if (data !== undefined) {
console.debug('cachedPromise', 'cache hit', key)
return data
}
// misses can't be helped though! expect them the first time we access
console.debug('cachedPromise', 'cache miss', key)
// ok, looks like we have to do the thing for the first time
const promise = get(key).then(
result => {
// once resolved, replace the promise with the result
cache.set(key, Promise.resolve(result))
console.debug('cachedPromise', 'promise resolved', key)
return result
}
).catch(
err => {
// if rejected, remove the promise from the cache
cache.delete(key)
console.warn('cachedPromise', 'promise rejected', key)
throw err
}
)
// cache the unresolved promise, subsequent callers will either get this or
// the resolved promise depending on timing
cache.set(key, promise)
return promise
}
return getData
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment