Background Info
My web app is built on top of async FastAPI. The server deploying this app will start 4 workers and each worker will start 2 threads for handling concurrent requests.
Description
Most services in my web app rely heavily on a token required from an external API. The problem is that this token has a certain expiration duration, meaning I’ll have to fetch it again when the existing one expires.
To prevent the token from being refreshed multiple times, I did the following things:
- Cache the token in the MySQL database so that I can access the token within the expiration duration without fetching the API.
- When the existing token expires, I apply an
asyncio.Lock
on all coroutines so that it’s ensured only one coroutine can exclusively do the fetching operation.
Problems
The above approach did solve the problem but buried a hidden issue. Since I stored the token in MySQL database, I’ll have to keep the connections open during the entire refresh operation (This is a bad approach as stated in SQLAlchemy docs). After referencing Python docs, I came to realize it’s not an appropriate solution to apply asyncio.Lock
in a web app with multiple threads.
As a result, I’m considering caching the token in redis. But then, how should I properly lock the refreshing operation so that only one token exists in redis even under high concurrency environment?
2
Answers
You could run a separate script to fetch the token then save it in sql using a cron job.
A cron job the is probably the best solution where periodically the cached token is refreshed before it can possibly expire guaranteeing whoever reads the token has a non-expired one. If that is not doable or convenient, then here is a way of implementing a quasi-lock in your MySQL database:
For the sake of generality, I will assume you have some table called
credentials
with a row with columnsservice_name
(the name of a service for which a token is required) andtoken
(the actual token required for the service),refresh_token
(the refresh token required for getting a new token) andexpiration
(a timestamp of when the token expires or preferably a bit before). Then whenever the token is required, the SQL issued is:I have assumed that the connection being used is not in auto-commit mode (otherwise you will need an initial BEGIN TRANSACTION). This will force one-at-a-time access for every client trying to read or refresh the token and only the first client will get in and the remaining clients will be blocked.
After the row is read, you can check whether
expired
is 1 or 0. If it is 1 (which will be the case for the first client who is executing this SQL when the token is expired), you proceed to use the refresh token to get a new token and then update thetoken
andexpiration
columns:If, however,
expired
is 0, then just execute: