skip to Main Content

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:

  1. Cache the token in the MySQL database so that I can access the token within the expiration duration without fetching the API.
  2. 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


  1. You could run a separate script to fetch the token then save it in sql using a cron job.

    Login or Signup to reply.
  2. 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 columns service_name (the name of a service for which a token is required) and token (the actual token required for the service), refresh_token (the refresh token required for getting a new token) and expiration (a timestamp of when the token expires or preferably a bit before). Then whenever the token is required, the SQL issued is:

    SELECT token, refresh_token, expiration < now() as expired FROM credentials
    WHERE service_name = 'some service name'
    FOR UPDATE;
    

    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 the token and expiration columns:

    UPDATE credentials SET token = 'some new value', expiration = some_timestamp
    WHERE service_name = 'some_service_name';
    COMMIT;
    

    If, however, expired is 0, then just execute:

    ROLLBACK;
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search