fix: разовая проверка не затирает более свежий локальный отзыв

lct-42: стейл-ответ SELECT, пришедший после конкурентного invalidate_login
на том же узле, откатывал поколение назад и снова принимал отозванную
cookie. После ожидания перепроверяем кэш и не перезаписываем более свежее
значение. Детерминированный тест гонки; SCALE-OUT.md отделяет старые
замеры от непроверенного кластерным смоуком изменения.
This commit is contained in:
kaifarikman 2026-09-27 17:13:01 +03:00
commit 42f1da7f83
2 changed files with 45 additions and 0 deletions

View file

@ -166,6 +166,12 @@ async def _resolve_unknown_login(login: str) -> int | None:
except Exception as exc: # noqa: BLE001 — fail closed, not FORBIDDEN
log.error("разовая проверка полномочий не удалась (%s)", type(exc).__name__)
raise _AuthStateUnavailable from exc
# A concurrent local revoke or sync tick may have written a newer
# generation while the SELECT above was in flight; the stale read
# must never clobber it back to an older, already-revoked epoch.
cached = _generations.get(login)
if cached is not None:
return cached
if version is None:
return None
_generations[login] = version

View file

@ -527,6 +527,45 @@ def test_middleware_rejects_login_missing_from_users_via_one_shot_query(monkeypa
auth._lookup_locks.pop(login, None)
def test_unknown_login_lookup_does_not_clobber_a_newer_local_revocation(monkeypatch):
"""A stale SELECT reply landing after a concurrent local revoke must not
resurrect the revoked cookie's epoch (regression: lock only deduplicates
concurrent lookups, it does not order a lookup against a write)."""
login = "peer-node-race-login"
started = asyncio.Event()
resume = asyncio.Event()
class FakeDb:
async def scalar(self, _query):
started.set()
await resume.wait()
return 0 # the epoch as it stood before the concurrent revoke below
class FakeSession:
async def __aenter__(self):
return FakeDb()
async def __aexit__(self, *_args):
return None
async def run():
auth.prime_generations({})
task = asyncio.ensure_future(auth._resolve_unknown_login(login))
await started.wait() # the SELECT is in flight, holding the login's lock
auth.invalidate_login(login, 1) # a local logout/edit races the reply
resume.set()
result = await task
assert result == 1, "the newer local revocation must win over the stale SELECT reply"
assert auth._generations[login] == 1
monkeypatch.setattr(auth, "get_sessionmaker", lambda: lambda: FakeSession())
try:
asyncio.run(run())
finally:
auth._generations.pop(login, None)
auth._lookup_locks.pop(login, None)
def test_unknown_login_lookup_deduplicates_concurrent_requests_into_one_select(monkeypatch):
login = "peer-node-concurrent-login"
calls = []