Reputation: 2528
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
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