Skip to content

Commit 4e19d2b

Browse files
committed
Stabilize market-data adapter init/reconcile and guard dependency-review on forks
1 parent 54d1b8e commit 4e19d2b

5 files changed

Lines changed: 74 additions & 20 deletions

File tree

.github/workflows/dependency-review.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ permissions:
2525

2626
jobs:
2727
dependency-review:
28+
if: ${{ github.event.pull_request.head.repo.fork == false }}
2829
runs-on: ubuntu-latest
2930
steps:
3031
- name: 'Checkout repository'
3132
uses: actions/checkout@v4
3233
- name: 'Dependency Review'
3334
uses: actions/dependency-review-action@v4
3435
# Commonly enabled options, see https://github.com/actions/dependency-review-action#configuration-options for all available options.
35-
with:
36-
comment-summary-in-pr: always
36+
# Avoid PR comment write requirement that often fails on restricted tokens/forks
3737
# fail-on-severity: moderate
3838
# deny-licenses: GPL-1.0-or-later, LGPL-2.0-or-later
3939
# retry-on-snapshot-warnings: true

src/market_data/schema.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ def from_raw(cls, token_id: str, raw: Dict[str, Any], source: str = "ws") -> "Or
2929
bids_raw = raw.get("bids") or raw.get("buys") or []
3030
asks_raw = raw.get("asks") or raw.get("sells") or []
3131

32-
def parse_levels(arr):
33-
levels = []
32+
def parse_levels(arr: Any) -> List[OrderBookLevel]:
33+
levels: List[OrderBookLevel] = []
3434
for lvl in arr:
3535
try:
3636
price = float(lvl.get("price") if isinstance(lvl, dict) else lvl[0])
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import asyncio
2+
3+
import webhook_server_fastapi as ws
4+
5+
6+
class _DummyState:
7+
def __init__(self):
8+
self.missing_count = {}
9+
10+
11+
def test_reconcile_subscriptions_noop_when_adapter_missing():
12+
# Must not raise when adapter is unavailable.
13+
asyncio.run(ws._reconcile_subscriptions(None, {"to_subscribe": {"t1"}, "to_unsubscribe": {"t2"}}, _DummyState()))
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import importlib
2+
3+
4+
def test_schema_module_imports_cleanly():
5+
mod = importlib.import_module("src.market_data.schema")
6+
assert hasattr(mod, "OrderBookSnapshot")
7+
assert hasattr(mod, "MarketEvent")

webhook_server_fastapi.py

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,41 @@ def _update_subs_gauge():
132132
_market_data_desired_refcount: dict = {}
133133
_market_data_last_warn_ts: float = 0.0
134134

135+
def _get_market_data_adapter(app_obj: FastAPI | None = None):
136+
"""Return the canonical MarketData adapter instance (app.state first, then module global)."""
137+
try:
138+
if app_obj is not None:
139+
state = getattr(app_obj, "state", None)
140+
if state is not None:
141+
adapter = getattr(state, "market_data_adapter", None)
142+
if adapter is not None:
143+
return adapter
144+
except Exception:
145+
pass
146+
return globals().get("_market_data_adapter", None)
147+
148+
149+
async def _reconcile_subscriptions(adapter, reconcile_result: dict, state) -> None:
150+
"""Best-effort subscribe/unsubscribe executor. Safe when adapter is unavailable."""
151+
if adapter is None:
152+
logger.warning("Reconcile: market-data adapter unavailable, skipping subscribe/unsubscribe this interval")
153+
return
154+
155+
for tk in reconcile_result.get("to_subscribe", set()):
156+
try:
157+
await adapter.subscribe(tk)
158+
logger.info("Reconcile: requested subscribe %s", str(tk)[:24])
159+
except Exception:
160+
logger.exception("Reconcile: subscribe failed for %s", tk)
161+
162+
for tk in reconcile_result.get("to_unsubscribe", set()):
163+
try:
164+
await adapter.unsubscribe(tk)
165+
logger.info("Reconcile: requested unsubscribe %s", str(tk)[:24])
166+
state.missing_count.pop(tk, None)
167+
except Exception:
168+
logger.exception("Reconcile: unsubscribe failed for %s", tk)
169+
135170

136171
async def market_data_event_consumer():
137172
"""
@@ -290,6 +325,12 @@ async def startup_event():
290325
# Initialize MarketDataAdapter (if enabled). Start idempotently and register event consumer task.
291326
global _market_data_adapter, _market_data_tasks
292327
try:
328+
# Always expose canonical adapter handle on app.state (can be None)
329+
try:
330+
app.state.market_data_adapter = _get_market_data_adapter(app)
331+
except Exception:
332+
logger.debug("Could not initialize app.state.market_data_adapter")
333+
293334
if getattr(settings, "MARKET_DATA_WS_ENABLED", True):
294335
if _market_data_adapter is None:
295336
if MarketDataAdapterClass is None:
@@ -309,7 +350,7 @@ async def startup_event():
309350
# bind adapter and telemetry to app.state for admin/debug handlers
310351
try:
311352
from src.market_data.telemetry import telemetry as _telemetry
312-
app.state.market_data_adapter = _market_data_adapter
353+
app.state.market_data_adapter = _get_market_data_adapter(app)
313354
app.state.market_data_telemetry = _telemetry
314355
import os
315356
app.state.market_data_pid = os.getpid()
@@ -406,29 +447,22 @@ async def reconcile_loop(interval: int = 30):
406447
globals()["_market_data_desired_refcount"] = desired_refcount
407448
except Exception:
408449
logger.debug("failed to set module-level desired_refcount")
450+
adapter = _get_market_data_adapter(app)
451+
if adapter is None:
452+
logger.warning("Reconcile: adapter unavailable; skipping actions for this interval")
453+
await asyncio.sleep(interval)
454+
continue
455+
409456
# compute actions
410457
try:
411458
missing_threshold = getattr(settings, "MARKET_DATA_RECONCILE_MISSING_THRESHOLD", 3)
412-
res = reconcile_step(_market_data_adapter, desired_refcount, state, missing_threshold=missing_threshold)
459+
res = reconcile_step(adapter, desired_refcount, state, missing_threshold=missing_threshold)
413460
except Exception:
414461
logger.exception("Reconcile step failed")
415462
res = {"to_subscribe": set(), "to_unsubscribe": set()}
416463

417464
# perform subscribe/unsubscribe
418-
for tk in res.get("to_subscribe", set()):
419-
try:
420-
await _market_data_adapter.subscribe(tk)
421-
logger.info("Reconcile: requested subscribe %s", str(tk)[:24])
422-
except Exception:
423-
logger.exception("Reconcile: subscribe failed for %s", tk)
424-
425-
for tk in res.get("to_unsubscribe", set()):
426-
try:
427-
await _market_data_adapter.unsubscribe(tk)
428-
logger.info("Reconcile: requested unsubscribe %s", str(tk)[:24])
429-
state.missing_count.pop(tk, None)
430-
except Exception:
431-
logger.exception("Reconcile: unsubscribe failed for %s", tk)
465+
await _reconcile_subscriptions(adapter, res, state)
432466
# update telemetry gauge after reconcile actions
433467
try:
434468
_update_subs_gauge()

0 commit comments

Comments
 (0)