#Reasoning
Let’s assume we have an async function that is somewhat expensive and takes a considerable amount of time to execute. We probably want to cache its result. If we used Redis for example, it might look something like this:
We try to get our content from DB, check if the result is truthy and then either return it or generate new content which we subsequently store in the database.
This is all good, but what happens if the function gets called 10 times within
the timeframe of its execution? E.g. the expensiveFn
takes 5 seconds to
execute, and before it completes the first time we call it a few more times.
All of them would check for "mykey"
and get a response. Since this is all
before the first call was done executing, all the responses will be empty and
each of our ten calls will generate new content and store it in the database.
Redis is pretty fast, but we still don’t want to do 20 calls where 2 would
suffice, or even worse, 10 potentially expensive function calls.
Redis is pretty fast, and it can surely handle setting the same key several times, but I feel like that’s what you call a race condition.
You might say that this is an edge case and if the function is inexpensive then who cares if it runs a few more times. On the other hand if it is expensive enough to warrant caching then this scenario might only apply for the first few calls before it gets cached.
And I agree with that, but what even is coding without premature optimization and overengineered solutions?
#Caching a promise
Asynchronous functions in JS return a value of type Promise. Think of it as
a pointer to something that might contain a value in the future. We can use
await
to halt execution until the value is there, but we can also just store
the promise inside a variable and do something with it.
The function checks if promise
is empty and assigns the result of
getContent
to it. Now if we call the function 10 times it will only run once
but return all 10 times. After the promise gets fulfilled we set the variable
to null.
This is not extremely useful as functions usually take parameters, in which case the promises can be stored in a Map with an argument as a key.
#Generic implementation
Instead of writing this manually every time and having a variable for storing a promise above every function I like to define a class, that does it for me and looks like this:
* using TypeScript for type annotations, but it works the same in JS
It stores state in a variable fittingly named store
and has a resolve
method, which takes a key and a function as its arguments. If the key exists
the function doesn’t get called and the existing promise is returned instead.
Given a function that takes a single argument we can:
Most of the time (but not always) the key I wanted to use would be the same as the function’s parameter, so I added some more abstraction for this.
This function initializes a PromiseStore
and then generates a function that
uses said store for caching.
Afterward the cachedAsyncFn
could become just one line. Obviously at that
point we could just change the implementation of asyncFn
and wrap it with
promiseStoreFn
, but that’s beside the point.
|
|
#Putting it together
Let’s go back to the very first code block and redo it:
It has barely changed, but now if we were to call the function 1000 times at
the same time with the same key
, it would only run once instead of a thousand
times.
A simple snippet to verify that this approach works:
|
|
Running it gives us this console output:
|
|