fix(auth): serialise the token refresh across processes
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
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import platform
|
||||
import threading
|
||||
from contextlib import AbstractContextManager
|
||||
from typing import TypedDict
|
||||
|
||||
from requests import Response, Session
|
||||
@@ -23,6 +24,7 @@ class _KwArgsMDRSConnectionPost(TypedDict, total=False):
|
||||
params: dict[str, str | int]
|
||||
data: dict[str, str | int] | MultipartEncoder
|
||||
headers: dict[str, str]
|
||||
timeout: float | tuple[float, float]
|
||||
|
||||
|
||||
class _KwArgsMDRSConnectionPut(TypedDict, total=False):
|
||||
@@ -61,6 +63,14 @@ class MDRSConnection:
|
||||
def delete(self, url: str, **kwargs: Unpack[_KwArgsMDRSConnectionDelete]) -> Response:
|
||||
return self.session.delete(self.__build_url(url), **kwargs)
|
||||
|
||||
def cache_lock(self) -> AbstractContextManager[None]:
|
||||
"""Hold exclusive access to the login cache across every process using it."""
|
||||
return self.__cache.lock()
|
||||
|
||||
def reload_cache(self) -> None:
|
||||
"""Re-read the login cache, discarding anything held from an earlier read."""
|
||||
self.__cache.reload()
|
||||
|
||||
def logout(self) -> None:
|
||||
del self.__cache.user
|
||||
del self.__cache.token
|
||||
@@ -96,7 +106,9 @@ class MDRSConnection:
|
||||
return path
|
||||
if self.url == "":
|
||||
raise MissingConfigurationException("remote host is not configured")
|
||||
return f"{self.url}/{path}"
|
||||
# The path brings its own separator, and a configuration written before the URL
|
||||
# was normalised may still carry a trailing slash of its own.
|
||||
return f"{self.url.rstrip('/')}/{path}"
|
||||
|
||||
def __prepare_headers(self) -> None:
|
||||
self.session.headers.update(
|
||||
|
||||
Reference in New Issue
Block a user