mirror of
https://github.com/usestrix/strix.git
synced 2026-10-09 03:18:31 +00:00
Retry the Caido client connection
This commit is contained in:
parent
5ea5129b67
commit
efcef9fc1a
2 changed files with 72 additions and 30 deletions
|
|
@ -28,7 +28,7 @@ _LOGIN_AS_GUEST_BODY = (
|
||||||
'{"query":"mutation LoginAsGuest { loginAsGuest { token { accessToken } } }"}'
|
'{"query":"mutation LoginAsGuest { loginAsGuest { token { accessToken } } }"}'
|
||||||
)
|
)
|
||||||
_PROJECT_SETUP_TIMEOUT_MS = 45_000
|
_PROJECT_SETUP_TIMEOUT_MS = 45_000
|
||||||
_PROJECT_SETUP_ATTEMPTS = 3
|
_BOOTSTRAP_ATTEMPTS = 3
|
||||||
|
|
||||||
|
|
||||||
async def _login_as_guest(
|
async def _login_as_guest(
|
||||||
|
|
@ -103,7 +103,7 @@ async def _setup_project(host_url: str, access_token: str) -> None:
|
||||||
|
|
||||||
project_id: str | None = None
|
project_id: str | None = None
|
||||||
last_exc: Exception | None = None
|
last_exc: Exception | None = None
|
||||||
for attempt in range(1, _PROJECT_SETUP_ATTEMPTS + 1):
|
for attempt in range(1, _BOOTSTRAP_ATTEMPTS + 1):
|
||||||
client = Client(
|
client = Client(
|
||||||
host_url,
|
host_url,
|
||||||
auth=TokenAuthOptions(token=access_token),
|
auth=TokenAuthOptions(token=access_token),
|
||||||
|
|
@ -127,10 +127,10 @@ async def _setup_project(host_url: str, access_token: str) -> None:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Caido project setup attempt %d/%d failed: %s",
|
"Caido project setup attempt %d/%d failed: %s",
|
||||||
attempt,
|
attempt,
|
||||||
_PROJECT_SETUP_ATTEMPTS,
|
_BOOTSTRAP_ATTEMPTS,
|
||||||
exc,
|
exc,
|
||||||
)
|
)
|
||||||
if attempt < _PROJECT_SETUP_ATTEMPTS:
|
if attempt < _BOOTSTRAP_ATTEMPTS:
|
||||||
await asyncio.sleep(min(2.0 * attempt, 8.0))
|
await asyncio.sleep(min(2.0 * attempt, 8.0))
|
||||||
else:
|
else:
|
||||||
logger.info("Caido project selected: %s", project_id)
|
logger.info("Caido project selected: %s", project_id)
|
||||||
|
|
@ -139,7 +139,7 @@ async def _setup_project(host_url: str, access_token: str) -> None:
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
await client.aclose()
|
await client.aclose()
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Caido project setup failed after {_PROJECT_SETUP_ATTEMPTS} attempts"
|
f"Caido project setup failed after {_BOOTSTRAP_ATTEMPTS} attempts"
|
||||||
) from last_exc
|
) from last_exc
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -161,16 +161,32 @@ async def bootstrap_caido(
|
||||||
|
|
||||||
await _setup_project(host_url, access_token)
|
await _setup_project(host_url, access_token)
|
||||||
|
|
||||||
client = Client(host_url, auth=TokenAuthOptions(token=access_token))
|
last_exc: Exception | None = None
|
||||||
try:
|
for attempt in range(1, _BOOTSTRAP_ATTEMPTS + 1):
|
||||||
# connect() is inside the guard as well: a cancellation there (scan
|
client = Client(host_url, auth=TokenAuthOptions(token=access_token))
|
||||||
# teardown while the bootstrap is still in flight) would otherwise
|
try:
|
||||||
# leave the half-connected transport behind.
|
# A cancellation while connecting can leave a half-connected
|
||||||
await client.connect()
|
# transport behind, so close the client before propagating it.
|
||||||
except BaseException:
|
await client.connect()
|
||||||
# The client never reaches the session bundle if connect or project
|
except Exception as exc: # noqa: BLE001
|
||||||
# setup fails, so close it here to avoid leaking the transport.
|
with contextlib.suppress(Exception):
|
||||||
with contextlib.suppress(Exception):
|
await client.aclose()
|
||||||
await client.aclose()
|
last_exc = exc
|
||||||
raise
|
logger.warning(
|
||||||
return client
|
"Caido client connect attempt %d/%d failed: %s",
|
||||||
|
attempt,
|
||||||
|
_BOOTSTRAP_ATTEMPTS,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
if attempt < _BOOTSTRAP_ATTEMPTS:
|
||||||
|
await asyncio.sleep(min(2.0 * attempt, 8.0))
|
||||||
|
except BaseException:
|
||||||
|
# Teardown can cancel the bootstrap at any await; do not retry.
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await client.aclose()
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
return client
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Caido client connect failed after {_BOOTSTRAP_ATTEMPTS} attempts"
|
||||||
|
) from last_exc
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,17 @@ import asyncio
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from strix.runtime.caido_bootstrap import bootstrap_caido
|
from strix.runtime.caido_bootstrap import bootstrap_caido
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
|
||||||
class _FakeExecResult:
|
class _FakeExecResult:
|
||||||
stderr = b""
|
stderr = b""
|
||||||
exit_code = 0
|
exit_code = 0
|
||||||
|
|
@ -129,33 +133,55 @@ def _setup_clients(
|
||||||
|
|
||||||
|
|
||||||
async def _bootstrap_expecting(
|
async def _bootstrap_expecting(
|
||||||
monkeypatch: pytest.MonkeyPatch, error: BaseException
|
monkeypatch: pytest.MonkeyPatch, errors: Sequence[BaseException]
|
||||||
) -> _FakeClient:
|
) -> tuple[list[_FakeClient], list[float], BaseException]:
|
||||||
"""Run a bootstrap whose ``connect()`` fails with ``error``."""
|
"""Run a bootstrap whose scan-client connections fail."""
|
||||||
setup_client = _FakeClient()
|
setup_client = _FakeClient()
|
||||||
client = _FakeClient(error)
|
scan_clients = [_FakeClient(error) for error in errors]
|
||||||
_install_sdk(monkeypatch, [setup_client, client])
|
_install_sdk(monkeypatch, [setup_client, *scan_clients])
|
||||||
|
sleep_calls: list[float] = []
|
||||||
|
|
||||||
with pytest.raises(type(error)):
|
async def _sleep(delay: float) -> None:
|
||||||
|
sleep_calls.append(delay)
|
||||||
|
|
||||||
|
monkeypatch.setattr("strix.runtime.caido_bootstrap.asyncio.sleep", _sleep)
|
||||||
|
|
||||||
|
with pytest.raises(BaseException) as exc_info:
|
||||||
await bootstrap_caido(
|
await bootstrap_caido(
|
||||||
_FakeSession(), # type: ignore[arg-type]
|
_FakeSession(), # type: ignore[arg-type]
|
||||||
host_url="http://host",
|
host_url="http://host",
|
||||||
container_url="http://container",
|
container_url="http://container",
|
||||||
)
|
)
|
||||||
assert setup_client.closed
|
assert setup_client.closed
|
||||||
return client
|
return [setup_client, *scan_clients], sleep_calls, exc_info.value
|
||||||
|
|
||||||
|
|
||||||
async def test_cancellation_during_connect_closes_the_client(
|
async def test_cancellation_during_connect_closes_the_client(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
client = await _bootstrap_expecting(monkeypatch, asyncio.CancelledError())
|
clients, sleep_calls, error = await _bootstrap_expecting(
|
||||||
assert client.closed
|
monkeypatch,
|
||||||
|
[asyncio.CancelledError()],
|
||||||
|
)
|
||||||
|
assert isinstance(error, asyncio.CancelledError)
|
||||||
|
assert len(clients) == 2
|
||||||
|
assert all(client.closed for client in clients)
|
||||||
|
assert sleep_calls == []
|
||||||
|
|
||||||
|
|
||||||
async def test_failed_connect_closes_the_client(monkeypatch: pytest.MonkeyPatch) -> None:
|
async def test_failed_connect_closes_the_client(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
client = await _bootstrap_expecting(monkeypatch, RuntimeError("no listener"))
|
errors = [
|
||||||
assert client.closed
|
RuntimeError("first"),
|
||||||
|
RuntimeError("second"),
|
||||||
|
RuntimeError("last"),
|
||||||
|
]
|
||||||
|
clients, sleep_calls, error = await _bootstrap_expecting(monkeypatch, errors)
|
||||||
|
assert isinstance(error, RuntimeError)
|
||||||
|
assert str(error) == "Caido client connect failed after 3 attempts"
|
||||||
|
assert error.__cause__ is errors[-1]
|
||||||
|
assert len(clients) == 4
|
||||||
|
assert all(client.closed for client in clients)
|
||||||
|
assert sleep_calls == [2.0, 4.0]
|
||||||
|
|
||||||
|
|
||||||
async def test_select_retries_without_creating_another_project(
|
async def test_select_retries_without_creating_another_project(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue