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:
2026-08-14 16:36:50 +09:00
parent 1a4023ba47
commit 0cac30ccf8
9 changed files with 332 additions and 41 deletions
+7 -1
View File
@@ -8,6 +8,9 @@ from mdrsclient.api.base import BaseApi
from mdrsclient.exceptions import UnauthorizedException
from mdrsclient.models import Token, User
# (connect, read) seconds for the token refresh.
TOKEN_REFRESH_TIMEOUT: Final[tuple[float, float]] = (5.0, 30.0)
@dataclass(frozen=True)
class UsersCurrentResponseLaboratory:
@@ -61,7 +64,10 @@ class UsersApi(BaseApi):
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT + "token/refresh/"
data: dict[str, str | int] = {"refresh": token.refresh}
response = self.connection.post(url, data=data)
# Bounded on its own: the caller holds a lock that spans processes while this runs,
# so a provider that accepts the connection and then goes quiet would otherwise
# stall every other request on this machine rather than just this one.
response = self.connection.post(url, data=data, timeout=TOKEN_REFRESH_TIMEOUT)
if response.status_code == requests.codes.unauthorized:
raise UnauthorizedException("Token is invalid or expired.")
self._raise_response_error(response)