fix: serialize the admin login lookup so concurrent first calls send one request
This commit is contained in:
1 parent
e139ae826f
commit
c73a7cc3ce
2 files changed
+32
-2
No files matched your search
@@ -9,6 +9,7 @@ token to the :class:`~aegis_gitea_mcp.identity.Identity` a tool call runs as.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import time
|
import time
|
||||||
@@ -103,6 +104,7 @@ class AdminIdentityResolver:
|
|||||||
self._executor = executor
|
self._executor = executor
|
||||||
self._login: str | None = None
|
self._login: str | None = None
|
||||||
self._retry_at = 0.0
|
self._retry_at = 0.0
|
||||||
|
self._lookup_lock = asyncio.Lock()
|
||||||
|
|
||||||
async def resolve(self, access: AccessToken | None) -> Identity:
|
async def resolve(self, access: AccessToken | None) -> Identity:
|
||||||
"""Return the admin identity, or raise for an access token of another kind."""
|
"""Return the admin identity, or raise for an access token of another kind."""
|
||||||
@@ -112,6 +114,9 @@ class AdminIdentityResolver:
|
|||||||
if secret is None:
|
if secret is None:
|
||||||
raise ToolError("admin-token mode needs GITEA_TOKEN, which is not configured")
|
raise ToolError("admin-token mode needs GITEA_TOKEN, which is not configured")
|
||||||
token = secret.get_secret_value()
|
token = secret.get_secret_value()
|
||||||
|
if self._login is None and time.monotonic() >= self._retry_at:
|
||||||
|
# One lookup for concurrent first calls: later callers wait, then see the result.
|
||||||
|
async with self._lookup_lock:
|
||||||
if self._login is None and time.monotonic() >= self._retry_at:
|
if self._login is None and time.monotonic() >= self._retry_at:
|
||||||
self._login = await self._lookup_login(token)
|
self._login = await self._lookup_login(token)
|
||||||
if self._login is None:
|
if self._login is None:
|
||||||
|
|||||||
+27
-2
@@ -17,8 +17,13 @@ from aegis_gitea_mcp.auth import (
|
|||||||
StaticBearerVerifier,
|
StaticBearerVerifier,
|
||||||
)
|
)
|
||||||
from aegis_gitea_mcp.errors import ToolError
|
from aegis_gitea_mcp.errors import ToolError
|
||||||
from aegis_gitea_mcp.gitea import GiteaExecutor
|
from aegis_gitea_mcp.gitea import GiteaExecutor, ToolOutcome
|
||||||
from tests.test_server_helpers import API, BEARER_TOKEN, GITEA_TOKEN, server_settings
|
from tests.test_server_helpers import (
|
||||||
|
API,
|
||||||
|
BEARER_TOKEN,
|
||||||
|
GITEA_TOKEN,
|
||||||
|
server_settings,
|
||||||
|
)
|
||||||
|
|
||||||
# Static bearer verifier
|
# Static bearer verifier
|
||||||
|
|
||||||
@@ -171,3 +176,23 @@ async def test_missing_gitea_token_is_refused() -> None:
|
|||||||
|
|
||||||
with pytest.raises(ToolError, match="GITEA_TOKEN"):
|
with pytest.raises(ToolError, match="GITEA_TOKEN"):
|
||||||
await resolver.resolve(None)
|
await resolver.resolve(None)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_concurrent_first_calls_send_one_login_lookup() -> None:
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
lookups: list[str] = []
|
||||||
|
|
||||||
|
class SlowExecutor(GiteaExecutor):
|
||||||
|
async def request(self, method: str, path: str, *args: Any, **kwargs: Any) -> ToolOutcome:
|
||||||
|
lookups.append(path)
|
||||||
|
await asyncio.sleep(0.01) # a real network round trip yields to other calls
|
||||||
|
return ToolOutcome(False, {"data": {"login": "admin"}}, 200)
|
||||||
|
|
||||||
|
settings = server_settings()
|
||||||
|
resolver = AdminIdentityResolver(settings, SlowExecutor(settings))
|
||||||
|
|
||||||
|
identities = await asyncio.gather(*(resolver.resolve(None) for _ in range(10)))
|
||||||
|
|
||||||
|
assert {identity.login for identity in identities} == {"admin"}
|
||||||
|
assert lookups == ["/user"]
|
||||||
Reference in new issue
Block a user