feat(alarm): trex-core 알람 emitter + trex.alerts exchange (dcy-033 BAT-049, Implemented)

AlarmEmitter(application, best-effort 비차단)+trex.alerts topic exchange 가산·격리+8 CRITICAL code 배선. 608 passed, opus QC PASS, 팀장 e2e(텔레그램 HTTP200) 통과. REQ-098~101 Implemented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
This commit is contained in:
2026-06-21 19:25:55 +09:00
co-authored by Claude Opus 4.8
parent b2165eac25
commit d4c5fcbba1
29 changed files with 1195 additions and 47 deletions
@@ -12,6 +12,7 @@ import threading
from datetime import datetime, time, timedelta, timezone
from typing import Callable, Optional
from trex_core.application.alarm_emitter import AlarmEmitter
from trex_core.application.broker_port import BrokerPort
from trex_core.application.portfolio import (
DegradedReason,
@@ -40,6 +41,7 @@ class SnapshotScheduler:
slot_repo=None,
slot_service=None,
instance_key: str = "default",
alarm_emitter: Optional[AlarmEmitter] = None,
) -> None:
self._read = read_port
self._store = store
@@ -55,6 +57,8 @@ class SnapshotScheduler:
self._slot_service = slot_service
self._instance_key = instance_key
self._last_universe_size: Optional[int] = None
# dcy-033 / REQ-100: best-effort alarm emitter.
self._alarm_emitter = alarm_emitter
# ---- callable from a daemon thread loop --------------------------------
def run_forever(self, stop: threading.Event, *, tick_sec: float = 60.0) -> None:
@@ -88,6 +92,13 @@ class SnapshotScheduler:
DegradedReason.PORTFOLIO_HUB_UNAVAILABLE, str(exc)
)
_log.warning("whoami failed: %s", exc)
# dcy-033 / REQ-100: CRITICAL alarm — portfolio hub unavailable.
if self._alarm_emitter is not None:
self._alarm_emitter.publish_alarm(
"portfolio_hub_unavailable",
"portfolio-hub whoami 실패: 포트폴리오 정보 부실화",
fields={"reason": "whoami_failed"},
)
return False
try:
items = self._read.get_portfolios(self._daemon_id)
@@ -96,6 +107,13 @@ class SnapshotScheduler:
DegradedReason.PORTFOLIO_HUB_UNAVAILABLE, str(exc)
)
_log.warning("get_portfolios failed: %s", exc)
# dcy-033 / REQ-100: CRITICAL alarm — portfolio hub unavailable.
if self._alarm_emitter is not None:
self._alarm_emitter.publish_alarm(
"portfolio_hub_unavailable",
"portfolio-hub get_portfolios 실패: 포트폴리오 정보 부실화",
fields={"daemon_id": self._daemon_id, "reason": "get_portfolios_failed"},
)
return False
except Exception as exc: # noqa: BLE001 - shape/parsing failure
self._store.set_degraded(
@@ -120,6 +138,7 @@ class SnapshotScheduler:
reconcile_core_holdings(
slot_repo=self._slot_repo, slot_service=self._slot_service,
broker=self._broker, instance_key=self._instance_key,
alarm_emitter=self._alarm_emitter, # dcy-033 / REQ-100
)
except Exception: # noqa: BLE001
_log.exception("core-holding divergence reconcile failed")