-
Notifications
You must be signed in to change notification settings - Fork 0
fix(torghut): stabilize scheduler dependency health #13011
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,16 +3,21 @@ | |
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| import threading | ||
| from collections.abc import Mapping, Sequence | ||
| from concurrent.futures import ThreadPoolExecutor, TimeoutError | ||
| from typing import cast | ||
|
|
||
| from sqlalchemy.exc import SQLAlchemyError | ||
| from sqlalchemy.orm import Session | ||
|
|
||
| from app.config import settings | ||
| from app.db import ping | ||
| from app.trading.tigerbeetle_client import check_tigerbeetle_health | ||
| from app.trading.tigerbeetle_client import ( | ||
| RealTigerBeetleClient, | ||
| check_tigerbeetle_health, | ||
| create_tigerbeetle_client, | ||
| parse_replica_addresses, | ||
| ) | ||
| from app.trading.tigerbeetle_reconcile import ( | ||
| BLOCKER_RECONCILIATION_STALE, | ||
| latest_tigerbeetle_reconciliation_payload, | ||
|
|
@@ -22,6 +27,39 @@ | |
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| _protocol_health_lock = threading.Lock() | ||
| _protocol_health_client: RealTigerBeetleClient | None = None | ||
|
|
||
|
|
||
| def _close_tigerbeetle_protocol_health_client_locked() -> None: | ||
| global _protocol_health_client | ||
|
|
||
| client = _protocol_health_client | ||
| _protocol_health_client = None | ||
| if client is None: | ||
| return | ||
| client.close() | ||
|
|
||
|
|
||
| def close_tigerbeetle_protocol_health_client() -> None: | ||
| """Close the process-owned status probe client.""" | ||
|
|
||
| with _protocol_health_lock: | ||
| _close_tigerbeetle_protocol_health_client_locked() | ||
|
|
||
|
|
||
| def _protocol_health_client_for_settings( | ||
| timeout_seconds: float, | ||
| ) -> RealTigerBeetleClient: | ||
| global _protocol_health_client | ||
|
|
||
| if _protocol_health_client is None: | ||
| _protocol_health_client = create_tigerbeetle_client( | ||
| settings, | ||
| rpc_timeout_seconds=timeout_seconds, | ||
| ) | ||
| return _protocol_health_client | ||
|
|
||
|
|
||
| def apply_status_read_statement_timeout( | ||
| session: Session, | ||
|
|
@@ -54,18 +92,16 @@ def check_postgres(session: Session) -> dict[str, object]: | |
|
|
||
| def check_tigerbeetle_protocol_health() -> dict[str, object]: | ||
| if not settings.tigerbeetle_enabled: | ||
| close_tigerbeetle_protocol_health_client() | ||
| health = check_tigerbeetle_health(settings) | ||
| payload = health.as_dict() | ||
| payload["protocol_ok"] = True | ||
| payload["protocol_probe_skipped"] = False | ||
| return payload | ||
|
|
||
| replica_addresses = [ | ||
| item.strip() | ||
| for item in settings.tigerbeetle_replica_addresses.split(",") | ||
| if item.strip() | ||
| ] | ||
| replica_addresses = parse_replica_addresses(settings.tigerbeetle_replica_addresses) | ||
| if not (settings.tigerbeetle_required or settings.tigerbeetle_reconcile_required): | ||
| close_tigerbeetle_protocol_health_client() | ||
| return { | ||
| "enabled": True, | ||
| "required": settings.tigerbeetle_required, | ||
|
|
@@ -78,26 +114,11 @@ def check_tigerbeetle_protocol_health() -> dict[str, object]: | |
| } | ||
|
|
||
| timeout_seconds = max(0.1, float(settings.tigerbeetle_health_timeout_seconds)) | ||
| executor = ThreadPoolExecutor(max_workers=1) | ||
| future = executor.submit(check_tigerbeetle_health, settings) | ||
| try: | ||
| health = future.result(timeout=timeout_seconds) | ||
| except TimeoutError: | ||
| return { | ||
| "enabled": True, | ||
| "required": settings.tigerbeetle_required, | ||
| "ok": not settings.tigerbeetle_required, | ||
| "protocol_ok": False, | ||
| "protocol_probe_skipped": False, | ||
| "cluster_id": settings.tigerbeetle_cluster_id, | ||
| "replica_addresses": replica_addresses, | ||
| "last_error": ( | ||
| f"TimeoutError: tigerbeetle protocol health timed out after " | ||
| f"{timeout_seconds:.2f}s" | ||
| ), | ||
| } | ||
| finally: | ||
| executor.shutdown(wait=False, cancel_futures=True) | ||
| with _protocol_health_lock: | ||
| client = _protocol_health_client_for_settings(timeout_seconds) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On the first probe after startup, a failed probe, or a configuration change, this factory call runs synchronously before the timeout-protected Useful? React with 👍 / 👎. |
||
| health = check_tigerbeetle_health(settings, client=client) | ||
| if not health.ok: | ||
| _close_tigerbeetle_protocol_health_client_locked() | ||
|
|
||
| payload = health.as_dict() | ||
| protocol_ok = bool(payload.get("ok")) | ||
|
|
@@ -432,6 +453,7 @@ def build_tigerbeetle_ledger_status(session: Session) -> dict[str, object]: | |
| "build_tigerbeetle_ledger_status", | ||
| "check_postgres", | ||
| "check_tigerbeetle_protocol_health", | ||
| "close_tigerbeetle_protocol_health_client", | ||
| "empty_tigerbeetle_ref_counts", | ||
| "latest_reconciliation_ref_counts", | ||
| "sqlalchemy_error_indicates_statement_timeout", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When TigerBeetle is unresponsive and status requests overlap, the first caller can hold this global lock for the configured 5-second RPC timeout while every later caller waits without any deadline; after acquiring it, each waiter starts another full timeout. Three concurrent
/trading/statusreads can therefore exceed the 12-second_TradingStatusReadBudget, and a larger burst can occupy the shared FastAPI worker pool even after API proxies have timed out. Use a timed acquisition or share/cache the in-flight probe result so each request has an end-to-end bound, and cover the concurrent-outage case with a regression test.AGENTS.md reference: AGENTS.md:L94-L96
Useful? React with 👍 / 👎.