fix(health): KIS 스트림 무수신 경보를 등록 0건(대시보드 미접속)일 때 억제

스트림은 대시보드 구독 종목만 등록하므로 브라우저가 닫히면 수신 0이 정상이다.
10/12 장중 오경보 3건(12:54·13:29·13:46)이 모두 구독 해제 10분 뒤였다.
재구독 직후에는 등록 시각을 기준점으로 써 옛 last_msg 로 즉시 오경보하지 않게 했다.
태스크 사망은 등록 수와 무관하게 계속 경보한다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y83Za98XsNw9zuA4FTUytN
This commit is contained in:
2026-10-12 13:52:01 +09:00
parent a264561576
commit 2e1a6e688c
2 changed files with 109 additions and 2 deletions

View File

@@ -262,6 +262,52 @@ def _kis_stream_health_v2(application, now: float | None = None, market_open: bo
return {"alive": alive, "last_msg_age": None if age is None else round(age, 1), "silent": silent}
# 등록 0건 → 1건 이상으로 바뀐 시각(monotonic). 재구독 직후 옛 last_msg 로 오경보하지 않게 기준점으로 쓴다.
_kis_stream_regs_since: float | None = None
def _kis_stream_registrations_v2() -> int | None:
try:
from app.services.kis_realtime_stream import kis_stream
return len(kis_stream._registered)
except Exception as _swallowed:
warn_throttled(logger, "main:ks_regs", "[health] KIS 등록 수 확인 실패: %s", _swallowed)
return None
def _kis_stream_health_v3(application, now: float | None = None, market_open: bool | None = None,
registrations: int | None = -1) -> dict | None:
"""v2 + 등록 0건이면 무수신을 정상으로 본다(2026-10-12).
스트림은 대시보드 구독 종목만 KIS 에 등록한다(매매 경로는 스트림을 쓰지 않는다).
브라우저가 닫혀 등록이 0건이면 수신이 없는 것이 정상인데, v2 는 이를 장중 무수신으로
error·텔레그램 경보했다(10/12 12:54·13:29·13:46 — 전부 구독 해제 10분 뒤).
태스크 사망은 등록 수와 무관하게 계속 silent 다. 등록 수를 알 수 없으면(None) v2 판정을 따른다.
"""
global _kis_stream_regs_since
base = _kis_stream_health_v2(application, now=now, market_open=market_open)
if base is None:
return None
regs = _kis_stream_registrations_v2() if registrations == -1 else registrations
n = now if now is not None else time.monotonic()
base["registrations"] = regs
if regs is None:
return base
if regs <= 0:
_kis_stream_regs_since = None
if base["alive"]:
base["silent"] = False
return base
if _kis_stream_regs_since is None:
_kis_stream_regs_since = n
if base["silent"] and base["alive"]:
last = _kis_stream_last_msg_mono
ref = max(last if last is not None else -1e18, _kis_stream_regs_since)
base["silent"] = (n - ref) > float(_HB_KIS_STREAM_SILENT_SECONDS)
base["last_msg_age"] = round(n - ref, 1)
return base
def _db_worker_health_v2(application) -> dict:
task = getattr(application.state, "db_worker_task", None)
try:
@@ -343,7 +389,7 @@ async def _heartbeat_watchdog_v2(application):
_notify_system_warning_bg(
"SCHEDULER", f"'{name}' 장중 사이클 정체 {info['last_cycle_age']}초 (alive={info['alive']})"
)
ks = _kis_stream_health_v2(application)
ks = _kis_stream_health_v3(application)
if ks is None or not ks["silent"]:
_stale_alerted_at.pop("kis_stream", None)
elif now - _stale_alerted_at.get("kis_stream", -1e9) >= realert:
@@ -1288,7 +1334,7 @@ def _runtime_health_v2(market_open: bool | None = None) -> tuple[dict, list]:
detail["swing"] = "unavailable"
reasons.append("swing_health_unavailable")
try:
ks = _kis_stream_health_v2(app, market_open=market_open)
ks = _kis_stream_health_v3(app, market_open=market_open)
if ks is not None:
detail["kis_stream"] = ks
if ks["silent"]: