jeremija
jeremija

Reputation: 2528

Python: Is there a thread safe way to know if lock.acquire() has blocked (and continue blocking)?

I'm writing an API library for my REST service. At some point, access token will need to be renewed. I'm trying to implement a thread-safe way to do it so only one renew request is sent, even though multiple threads may want to renew it at the same time.

Here is a simplified version of my code:

import requests

class Api():
    def _renew(self):
        # send a request which renews token in self._headers

    def do_something(self, url, params=None):
        r = requests(url, params=params, headers=self._headers)
        if r.status_code == 401 and r.json()['error'] == 'Token expired':
             # renew the access token
             self._renew()
             # repeat request with updated headers
             r = requests(url, params=params, headers=self._headers)
        return r.json()

I need to know if a current renew request is in progress. My idea was to write the renew function like this:

def _renew(self):
    lock.acquire()
    # i want to check here if the lock.acquire() call blocked
    # the thread and return immediately if it has
    try:
        # send a request to renew token header in self._headers
    finally:
        lock.release()

I want other threads which may call do_something() (and subsequently _renew()) method to wait until the first really renews the tokens and make others use it's result.

How can I tell if my lock.acquire() call was blocking or not?

Checking the result of lock.locked() before calling acquire() is not thread-safe and sometimes more than one thread send renew request to the server.

Upvotes: 1

Views: 2157

Answers (1)

WorldSEnder
WorldSEnder

Reputation: 5044

You can call lock.acquire(False) for a nonblocking call and use the return value to determine if a lock has been acquired. This would look like this:

def _renew(self):
    # calling when the lock is already acquired
    # will not block and return False
    if not lock.acquire(False):
        event.wait()
        return
    # else we acquired the lock 
    event.clear()
    try:
        # send a request to renew token header in self._headers
    finally:
        event.set()
        lock.release()

See the threading-docs for python.

Another approach is to wrap the token in another class:

from threading import Event, RLock

class Token:
    _internal = RLock()
    _marker = False
    def __init__(self):
        # TODO set header
        self._header = None

    def _renew(self):
        # TODO refresh the header
        pass

    def get(self):
        with self._internal:
            if self._marker:
                self._renew()
                self._marker = False
            return self._header

    # Marks the header to be refreshed at the next get()
    def set_renew(self):
        with self._internal:
            self._marker = True

This has several advantages. First of all the token is responsible for itself. In the best possible environment it would only refresh itself whenever needed and NOT get refreshed by other classes. This should be decided in Token#get(self). This also solves thread-safety by wrapping all get-calls into a lock, preventing unwanted modifications.

Upvotes: 2

Related Questions