The login cache is shared by every mdrs process, but the refresh was guarded by a lock that only reaches inside one. Concurrent runs each sent the same refresh token, and a provider that rotates them accepts the first and refuses the rest. - hold a lock that spans processes across the whole read-refresh- write, checking cheaply first so ordinary requests never take it - write the cache through a temporary file: opening it for writing truncates it, and a reader landing in that window found it empty and threw the session away - take the lock for every write, not just the refresh, so a login running beside one cannot be silently reverted - wait for a busy lock on Windows rather than giving up after the ten attempts msvcrt allows, and retry the rename it refuses while a reader still holds the file open - bound the refresh request on its own, so a provider that goes quiet cannot hold the lock indefinitely - accept bare hostnames such as localhost, store URLs without the trailing slash, and join download paths through one helper
39 lines
1.6 KiB
Python
39 lines
1.6 KiB
Python
from mdrsclient.api.users import UsersApi
|
|
from mdrsclient.connection import MDRSConnection
|
|
from mdrsclient.exceptions import UnauthorizedException
|
|
|
|
|
|
def token_check(connection: MDRSConnection) -> None:
|
|
"""
|
|
Bring the access token up to date before a request goes out.
|
|
|
|
Refreshing is a read-modify-write over a cache shared with every other client
|
|
process on this machine, and a rotating provider stops honouring the refresh token
|
|
it replaces. Two processes reaching this at once would otherwise both send the same
|
|
token, and the loser would be left holding one the server no longer accepts, so the
|
|
whole sequence runs under a lock that spans processes and the cache is re-read
|
|
inside it.
|
|
"""
|
|
with connection.lock:
|
|
token = connection.token
|
|
if token is None or not (token.is_refresh_required or token.is_expired):
|
|
# Nothing to do, which is the answer for almost every request. The lock below
|
|
# reaches across processes and is held for a round trip, so it is worth
|
|
# knowing that before taking it.
|
|
return
|
|
with connection.cache_lock():
|
|
connection.reload_cache()
|
|
token = connection.token
|
|
if token is None:
|
|
return
|
|
if token.is_expired:
|
|
connection.logout()
|
|
return
|
|
if not token.is_refresh_required:
|
|
return
|
|
user_api = UsersApi(connection)
|
|
try:
|
|
connection.token = user_api.tokenRefresh(token)
|
|
except UnauthorizedException:
|
|
connection.logout()
|