fix(시세): LS spill-only·토큰 파일캐시·호가 snap_time 3초컷
- _feed_fallback 미러 OFF, LS cap/grace/hold RAM을 KIS·키움 spill과 정합 - LS 접근토큰 .ls_token_cache_*.json (재시작 재사용, revoke 루프 없음) - 호가 RAM을 틱과 동일 LIVE_FEED_FALLBACK(snap_time)로 컷, 필터 max_age=0은 유지 - 익절 지정가 로그에 실제 호가 벤더(kis/kiwoom/ls 1·2·3차) 표기 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -39,6 +39,8 @@ import requests
|
||||
|
||||
logger = logging.getLogger("LSWebSocket")
|
||||
|
||||
from .ws_reconnect_backoff import ws_reconnect_delay_for_attempt
|
||||
|
||||
try:
|
||||
from kis_trader.utils.env import get_env_bool, get_env_float, get_env_from_db, get_env_int
|
||||
except ImportError:
|
||||
@@ -153,6 +155,8 @@ class LSWebSocketPriceCache:
|
||||
self._sub_lock = threading.Lock()
|
||||
self._send_lock = threading.Lock() # SSL write 직렬화 (BAD_LENGTH 레이스 방지)
|
||||
self._force_closing = False # UNREG→close 재진입 방지
|
||||
self._hold_ram_extra_codes_fn: Optional[Any] = None # WSManager → 보유 pin
|
||||
self._ws_manager_bound: bool = False
|
||||
self._subscribed: Set[str] = set() # 6자리 KR 코드 (소켓 실구독)
|
||||
# code → {"permanent","condition","default",...} — 해제는 owner 전부일 때만
|
||||
self._sub_owners: Dict[str, Set[str]] = {}
|
||||
@@ -168,6 +172,13 @@ class LSWebSocketPriceCache:
|
||||
self._last_tick_mono: float = 0.0
|
||||
self._opened_mono: float = 0.0
|
||||
self._watchdog_empty_streak: int = 0
|
||||
self._ws_reconnect_step: int = 0
|
||||
# Bye/조기 CLOSE 서킷 — OPEN 직후 끊김 시 백오프 리셋 금지·토큰 1회 갱신
|
||||
self._early_close_streak: int = 0
|
||||
self._last_close_lived_sec: float = 0.0
|
||||
self._last_close_early: bool = False
|
||||
self._pending_bye: bool = False
|
||||
self._stable_open: bool = False
|
||||
|
||||
# JIF 장운영정보 (앱 하트비트 아님 — 세션 판정용)
|
||||
self._jif_lock = threading.Lock()
|
||||
@@ -319,12 +330,18 @@ class LSWebSocketPriceCache:
|
||||
)
|
||||
return True
|
||||
|
||||
def _graceful_unreg_all(self, *, clear_ram: bool = True) -> int:
|
||||
def _graceful_unreg_all(
|
||||
self,
|
||||
*,
|
||||
clear_ram: bool = True,
|
||||
ram_keep_codes: Optional[Set[str]] = None,
|
||||
) -> int:
|
||||
"""종료 직전 서버 구독 UNREG — 재시작 시 세션 꼬임/한도 거부 완화.
|
||||
|
||||
REG 워커 큐가 아니라 동기 전송(갭 준수). 타임아웃 초과 시 남은 종목은
|
||||
TCP close 에 맡긴다 (systemd stop 지연·폭주 방지).
|
||||
``clear_ram=False``: 장 양보 시 서버만 해지, RAM 은 재접속 REG 용으로 유지.
|
||||
``clear_ram=True`` + ``ram_keep_codes``: 서버 UNREG 후 RAM 은 keep 만 유지
|
||||
(hold reopen 시 07:00 mass REG 방지 — permanent·보유 pin).
|
||||
"""
|
||||
if self._ws is None or not self._opened.is_set():
|
||||
return 0
|
||||
@@ -389,11 +406,26 @@ class LSWebSocketPriceCache:
|
||||
break
|
||||
|
||||
if clear_ram:
|
||||
keep = {
|
||||
str(c).strip()
|
||||
for c in (ram_keep_codes or set())
|
||||
if str(c).strip()
|
||||
}
|
||||
with self._sub_lock:
|
||||
self._subscribed.clear()
|
||||
self._sub_owners.clear()
|
||||
self._us_subscribed.clear()
|
||||
self._us_sub_owners.clear()
|
||||
if keep:
|
||||
for c in list(self._subscribed):
|
||||
if c not in keep:
|
||||
self._subscribed.discard(c)
|
||||
self._sub_owners.pop(c, None)
|
||||
for c in list(self._us_subscribed):
|
||||
if c not in keep:
|
||||
self._us_subscribed.discard(c)
|
||||
self._us_sub_owners.pop(c, None)
|
||||
else:
|
||||
self._subscribed.clear()
|
||||
self._sub_owners.clear()
|
||||
self._us_subscribed.clear()
|
||||
self._us_sub_owners.clear()
|
||||
|
||||
if timed_out:
|
||||
logger.warning(
|
||||
@@ -460,7 +492,7 @@ class LSWebSocketPriceCache:
|
||||
# spill 즉시성: 미연결이면 대기 없이 False (일반 owner 는 기존처럼 집합 적재 후 REG 큐)
|
||||
if own == "spill" and not self.is_connected():
|
||||
return False
|
||||
max_n = int(get_env_int("LS_WS_MAX_SUBSCRIPTIONS", 0) or 0)
|
||||
max_n = int(get_env_int("LS_WS_MAX_SUBSCRIPTIONS", 100) or 0)
|
||||
if code.isdigit() and len(code) == 6:
|
||||
need_reg = False
|
||||
with self._sub_lock:
|
||||
@@ -873,6 +905,58 @@ class LSWebSocketPriceCache:
|
||||
self._token_at = time.time()
|
||||
return self._token
|
||||
|
||||
def _stable_open_sec(self) -> float:
|
||||
return max(5.0, float(get_env_float("LS_WS_STABLE_OPEN_SEC", 30.0) or 30.0))
|
||||
|
||||
def _mark_stable_open(self, why: str) -> None:
|
||||
"""REG 완료·장시간 OPEN 후에만 재연결 step/Bye streak 리셋."""
|
||||
if self._stable_open and self._ws_reconnect_step == 0 and self._early_close_streak == 0:
|
||||
return
|
||||
self._stable_open = True
|
||||
self._ws_reconnect_step = 0
|
||||
self._early_close_streak = 0
|
||||
self._pending_bye = False
|
||||
logger.info(
|
||||
"LS WS 안정 OPEN 확정 — reconnect/Bye streak 리셋 (%s)",
|
||||
why,
|
||||
)
|
||||
|
||||
def _refresh_token_after_bye(self) -> None:
|
||||
"""조기 Bye 서킷: 로컬 캐시 무효화 후 1회 재발급 (min_gap 존중)."""
|
||||
from kis_trader.network.ls_token import invalidate_ls_access_token
|
||||
|
||||
invalidate_ls_access_token(
|
||||
self.app_key, self.app_secret, reason="ls_ws_early_bye",
|
||||
)
|
||||
self._token = ""
|
||||
self._token = fetch_ls_access_token(
|
||||
self.app_key,
|
||||
self.app_secret,
|
||||
force=True,
|
||||
reason="ls_ws_early_bye",
|
||||
)
|
||||
self._token_at = time.time()
|
||||
|
||||
def _hold_ram_keep_codes(self) -> Set[str]:
|
||||
"""hold 양보 후 RAM 유지 집합 — _permanent owner + (옵션) 보유."""
|
||||
keep: Set[str] = set()
|
||||
with self._sub_lock:
|
||||
for code, ow in self._sub_owners.items():
|
||||
if "_permanent" in ow:
|
||||
keep.add(code)
|
||||
for code, ow in self._us_sub_owners.items():
|
||||
if "_permanent" in ow:
|
||||
keep.add(code)
|
||||
if get_env_bool("LS_WS_HOLD_RAM_KEEP_HOLDINGS", True):
|
||||
fn = getattr(self, "_hold_ram_extra_codes_fn", None)
|
||||
if callable(fn):
|
||||
try:
|
||||
extra = fn()
|
||||
keep |= {str(c).strip() for c in (extra or set()) if str(c).strip()}
|
||||
except Exception:
|
||||
pass
|
||||
return keep
|
||||
|
||||
def _force_close_socket(self, reason: str) -> None:
|
||||
"""hold 이탈·갭 — 구독 UNREG 후 소켓 양보. 토큰 revoke/재발급 없음."""
|
||||
ws = self._ws
|
||||
@@ -883,7 +967,8 @@ class LSWebSocketPriceCache:
|
||||
self._force_closing = True
|
||||
try:
|
||||
try:
|
||||
self._graceful_unreg_all(clear_ram=False)
|
||||
keep = self._hold_ram_keep_codes()
|
||||
self._graceful_unreg_all(clear_ram=True, ram_keep_codes=keep)
|
||||
except Exception as e:
|
||||
logger.warning("LS WS 세션 양보 전 UNREG 실패: %s", e)
|
||||
try:
|
||||
@@ -1150,6 +1235,8 @@ class LSWebSocketPriceCache:
|
||||
except Exception as e:
|
||||
logger.debug("LS 호가 파싱 실패 %s: %s", code, e)
|
||||
return
|
||||
if snap is None:
|
||||
return
|
||||
if not self._orderbook_recorder:
|
||||
return
|
||||
if not get_env_bool("LS_WS_ORDERBOOK_SAVE", True):
|
||||
@@ -1299,44 +1386,72 @@ class LSWebSocketPriceCache:
|
||||
kr = list(self._subscribed)
|
||||
us = list(self._us_subscribed)
|
||||
n_ok = 0
|
||||
# 관측 전용(동작 동일): 루프 break 원인·기대 send 수. 복구/재REG 로직 변경 없음.
|
||||
abort_why = ""
|
||||
also_hoga = bool(self.also_hoga)
|
||||
uvi_on = get_env_bool("LS_WS_UVI_ENABLED", True)
|
||||
jif_on = get_env_bool("LS_WS_JIF_ENABLED", True)
|
||||
per_kr = 1 + (1 if also_hoga else 0) + (1 if uvi_on else 0)
|
||||
expected = (1 if jif_on else 0) + len(kr) * per_kr + len(us)
|
||||
# JIF 장운영 — 스펙 예: tr_key=0 (앱 하트비트 아님)
|
||||
if get_env_bool("LS_WS_JIF_ENABLED", True):
|
||||
if jif_on:
|
||||
jif_key = (get_env_from_db("LS_WS_JIF_TR_KEY", "0") or "0").strip() or "0"
|
||||
self._send_typed("3", "JIF", jif_key)
|
||||
n_ok += 1
|
||||
time.sleep(gap_ms / 1000.0)
|
||||
# replay 는 직접 전송 (큐 재투입 폭주 방지)
|
||||
for code in kr:
|
||||
if not self._running or not self._opened.is_set():
|
||||
if not self._running:
|
||||
abort_why = "not_running"
|
||||
break
|
||||
if not self._opened.is_set():
|
||||
abort_why = "opened_cleared"
|
||||
break
|
||||
tr_cd, tr_key, hoga_cd, hoga_key = self._kr_tr_pair(code)
|
||||
self._send_typed("3", tr_cd, tr_key)
|
||||
n_ok += 1
|
||||
if self.also_hoga:
|
||||
if also_hoga:
|
||||
self._send_typed("3", hoga_cd, hoga_key)
|
||||
n_ok += 1
|
||||
if get_env_bool("LS_WS_UVI_ENABLED", True):
|
||||
if uvi_on:
|
||||
vi_cd, vi_key = self._vi_tr_pair(code)
|
||||
self._send_typed("3", vi_cd, vi_key)
|
||||
n_ok += 1
|
||||
time.sleep(gap_ms / 1000.0)
|
||||
for sym in us:
|
||||
if not self._running or not self._opened.is_set():
|
||||
if not self._running:
|
||||
abort_why = abort_why or "not_running"
|
||||
break
|
||||
if not self._opened.is_set():
|
||||
abort_why = abort_why or "opened_cleared"
|
||||
break
|
||||
self._send_typed("3", "GSC", overseas_tr_key("82", sym))
|
||||
n_ok += 1
|
||||
time.sleep(gap_ms / 1000.0)
|
||||
ping_iv = int(get_env_int("LS_WS_PING_INTERVAL_SEC", 20) or 0)
|
||||
reg_ok = (not abort_why) and self._opened.is_set() and n_ok >= expected
|
||||
logger.info(
|
||||
"LS WS OPEN — 구독 복구 KR=%d US=%d sends≈%d (delay=%dms gap=%dms ping=%s)",
|
||||
"LS WS OPEN — 구독 복구 KR=%d US=%d sends≈%d expected≈%d "
|
||||
"abort_why=%s opened_now=%s also_hoga=%s reg_ok=%s "
|
||||
"(delay=%dms gap=%dms ping=%s)",
|
||||
len(kr),
|
||||
len(us),
|
||||
n_ok,
|
||||
expected,
|
||||
abort_why or "-",
|
||||
self._opened.is_set(),
|
||||
also_hoga,
|
||||
reg_ok,
|
||||
delay_ms,
|
||||
gap_ms,
|
||||
"off" if ping_iv <= 0 else f"{ping_iv}s",
|
||||
)
|
||||
self._set_recovering(False)
|
||||
# 미완료 REG 인데 recovering=False → 워치독/게이트가 정상으로 착각
|
||||
if reg_ok:
|
||||
self._mark_stable_open("REG replay complete")
|
||||
self._set_recovering(False)
|
||||
else:
|
||||
self._set_recovering(True)
|
||||
continue
|
||||
# send
|
||||
_, tr_type, tr_cd, tr_key = item
|
||||
@@ -1404,7 +1519,8 @@ class LSWebSocketPriceCache:
|
||||
pass
|
||||
self._set_recovering(True)
|
||||
try:
|
||||
self._graceful_unreg_all(clear_ram=False)
|
||||
keep = self._hold_ram_keep_codes()
|
||||
self._graceful_unreg_all(clear_ram=True, ram_keep_codes=keep)
|
||||
except Exception as e:
|
||||
logger.debug("LS watchdog UNREG: %s", e)
|
||||
try:
|
||||
@@ -1414,7 +1530,6 @@ class LSWebSocketPriceCache:
|
||||
logger.debug("LS watchdog close: %s", e)
|
||||
|
||||
def _run_forever(self) -> None:
|
||||
backoff = 1.0
|
||||
while self._running:
|
||||
# ── hold 외 → 소켓 양보 후 다음 국내/해외 창까지 대기 (재발급 없음) ──
|
||||
if not self._should_hold_socket():
|
||||
@@ -1436,7 +1551,7 @@ class LSWebSocketPriceCache:
|
||||
)
|
||||
self._opened.clear()
|
||||
self._set_recovering(True)
|
||||
backoff = 1.0
|
||||
self._ws_reconnect_step = 0
|
||||
self._watchdog_empty_streak = 0
|
||||
for _ in range(int(wait_sec // 60)):
|
||||
if not self._running:
|
||||
@@ -1453,8 +1568,8 @@ class LSWebSocketPriceCache:
|
||||
self._ensure_token()
|
||||
except Exception as e:
|
||||
logger.warning("LS 토큰 ensure 실패(캐시/한도): %s", e)
|
||||
time.sleep(min(backoff, 60.0))
|
||||
backoff = min(backoff * 2, 60.0)
|
||||
self._ws_reconnect_step += 1
|
||||
time.sleep(ws_reconnect_delay_for_attempt(self._ws_reconnect_step))
|
||||
continue
|
||||
self._ws = self._websocket.WebSocketApp(
|
||||
self.ws_url,
|
||||
@@ -1474,6 +1589,11 @@ class LSWebSocketPriceCache:
|
||||
self._ws.run_forever(**run_kw)
|
||||
except Exception as e:
|
||||
logger.warning("LS WS run 예외: %s", e)
|
||||
# 관측: run_forever 종료 후 clear (동작 동일)
|
||||
logger.warning(
|
||||
"LS WS run_forever 종료 → opened clear (ws_id=%s)",
|
||||
id(self._ws) if self._ws is not None else None,
|
||||
)
|
||||
self._opened.clear()
|
||||
self._set_recovering(True)
|
||||
if not self._running:
|
||||
@@ -1481,35 +1601,188 @@ class LSWebSocketPriceCache:
|
||||
# hold 이탈로 끊긴 경우 즉시 대기 분기로 (불필요 재발급·재연결 폭주 방지)
|
||||
if not self._should_hold_socket():
|
||||
continue
|
||||
time.sleep(min(backoff, 60.0))
|
||||
backoff = min(backoff * 2, 60.0)
|
||||
|
||||
# 조기 Bye/CLOSE 서킷 — OPEN마다 step=0 리셋하던 버그 보정 + 토큰 1회 갱신
|
||||
early = bool(self._last_close_early or self._pending_bye)
|
||||
if early:
|
||||
self._early_close_streak += 1
|
||||
elif self._last_close_lived_sec >= self._stable_open_sec():
|
||||
self._early_close_streak = 0
|
||||
|
||||
streak_need = max(2, int(get_env_int("LS_WS_EARLY_BYE_STREAK", 5) or 5))
|
||||
if early and self._early_close_streak >= streak_need:
|
||||
circuit_sleep = max(
|
||||
15.0,
|
||||
float(get_env_float("LS_WS_BYE_CIRCUIT_SLEEP_SEC", 60.0) or 60.0),
|
||||
)
|
||||
logger.warning(
|
||||
"LS WS Bye 서킷 — early_streak=%d lived=%.1fs bye=%s "
|
||||
"→ 토큰 무효화·재발급 후 %.0fs 대기",
|
||||
self._early_close_streak,
|
||||
self._last_close_lived_sec,
|
||||
self._pending_bye,
|
||||
circuit_sleep,
|
||||
)
|
||||
try:
|
||||
self._refresh_token_after_bye()
|
||||
except Exception as e:
|
||||
logger.warning("LS Bye 서킷 토큰 갱신 실패: %s", e)
|
||||
self._early_close_streak = 0
|
||||
self._pending_bye = False
|
||||
self._ws_reconnect_step = max(self._ws_reconnect_step, 1)
|
||||
time.sleep(circuit_sleep)
|
||||
continue
|
||||
|
||||
self._ws_reconnect_step += 1
|
||||
time.sleep(ws_reconnect_delay_for_attempt(self._ws_reconnect_step))
|
||||
|
||||
def _on_open(self, _ws: Any) -> None:
|
||||
# 콜백에서 sleep/REG 연타 금지 → 워커에 replay 위임
|
||||
# ⚠️ flapping OPEN 에서 reconnect_step=0 리셋 금지 (Bye 루프 1초 연타 원인)
|
||||
self._opened_mono = time.monotonic()
|
||||
self._last_tick_mono = time.monotonic()
|
||||
self._stable_open = False
|
||||
self._last_close_early = False
|
||||
self._opened.set()
|
||||
self._set_recovering(True)
|
||||
# 관측 전용: 소켓 신분(레이스 가설). clear/REG 동작은 그대로.
|
||||
logger.info(
|
||||
"LS WS OPEN cb ws_id=%s active_id=%s same=%s reconnect_step=%d early_streak=%d",
|
||||
id(_ws) if _ws is not None else None,
|
||||
id(self._ws) if self._ws is not None else None,
|
||||
_ws is self._ws,
|
||||
self._ws_reconnect_step,
|
||||
self._early_close_streak,
|
||||
)
|
||||
self._enqueue_replay()
|
||||
|
||||
def _on_close(self, _ws: Any, status: Any, msg: Any) -> None:
|
||||
# 관측 전용: 옛 소켓 close 여부. 동작은 기존과 동일하게 무조건 clear.
|
||||
same = _ws is self._ws
|
||||
msg_s = msg
|
||||
try:
|
||||
if isinstance(msg, (bytes, bytearray)):
|
||||
msg_s = bytes(msg).decode("utf-8", errors="replace")
|
||||
except Exception:
|
||||
msg_s = repr(msg)
|
||||
lived = 0.0
|
||||
if self._opened_mono > 0:
|
||||
lived = max(0.0, time.monotonic() - self._opened_mono)
|
||||
stable = self._stable_open_sec()
|
||||
msg_blob = str(msg_s or "")
|
||||
is_bye = ("Bye" in msg_blob) or self._pending_bye
|
||||
# 강제 세션 양보(hold 외)는 early Bye 서킷에 넣지 않음
|
||||
early = (lived < stable) and (not self._force_closing)
|
||||
self._last_close_lived_sec = lived
|
||||
self._last_close_early = bool(early)
|
||||
if is_bye:
|
||||
self._pending_bye = True
|
||||
logger.warning(
|
||||
"LS WS CLOSE status=%s msg=%s ws_same=%s "
|
||||
"close_id=%s active_id=%s opened_before=%s "
|
||||
"lived=%.1fs early=%s bye=%s stable_was=%s",
|
||||
status,
|
||||
msg_s,
|
||||
same,
|
||||
id(_ws) if _ws is not None else None,
|
||||
id(self._ws) if self._ws is not None else None,
|
||||
self._opened.is_set(),
|
||||
lived,
|
||||
early,
|
||||
is_bye,
|
||||
self._stable_open,
|
||||
)
|
||||
self._stable_open = False
|
||||
self._opened.clear()
|
||||
self._set_recovering(True)
|
||||
logger.warning("LS WS CLOSE status=%s msg=%s", status, msg)
|
||||
|
||||
def _on_error(self, _ws: Any, err: Any) -> None:
|
||||
logger.warning("LS WS ERROR %s", err)
|
||||
err_s = ""
|
||||
try:
|
||||
if isinstance(err, (bytes, bytearray)):
|
||||
err_s = bytes(err).decode("utf-8", errors="replace")
|
||||
else:
|
||||
err_s = str(err)
|
||||
except Exception:
|
||||
err_s = repr(err)
|
||||
if "Bye" in err_s or "opcode=8" in err_s or "\\x03\\xe8" in err_s:
|
||||
self._pending_bye = True
|
||||
logger.warning(
|
||||
"LS WS ERROR %s ws_same=%s err_id=%s active_id=%s bye=%s",
|
||||
err,
|
||||
_ws is self._ws,
|
||||
id(_ws) if _ws is not None else None,
|
||||
id(self._ws) if self._ws is not None else None,
|
||||
self._pending_bye,
|
||||
)
|
||||
|
||||
def _on_message(self, _ws: Any, message: Any) -> None:
|
||||
try:
|
||||
data = json.loads(message) if isinstance(message, str) else message
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"LS WS MSG non-json type=%s head=%r",
|
||||
type(message).__name__,
|
||||
(str(message)[:200] if message is not None else ""),
|
||||
)
|
||||
return
|
||||
if not isinstance(data, dict):
|
||||
logger.warning("LS WS MSG not-dict type=%s", type(data).__name__)
|
||||
return
|
||||
header = data.get("header") or {}
|
||||
body = data.get("body") or {}
|
||||
if not isinstance(body, dict) or not body:
|
||||
return
|
||||
if not isinstance(header, dict):
|
||||
header = {}
|
||||
body_raw = data.get("body")
|
||||
body: Dict[str, Any] = body_raw if isinstance(body_raw, dict) else {}
|
||||
tr_cd = str(header.get("tr_cd") or "")
|
||||
tr_key = str(header.get("tr_key") or "")
|
||||
tr_type = str(header.get("tr_type") or "")
|
||||
rsp_cd = str(
|
||||
header.get("rsp_cd")
|
||||
or header.get("rspCd")
|
||||
or body.get("rsp_cd")
|
||||
or ""
|
||||
).strip()
|
||||
rsp_msg = str(
|
||||
header.get("rsp_msg")
|
||||
or header.get("rspMsg")
|
||||
or body.get("rsp_msg")
|
||||
or ""
|
||||
).strip()
|
||||
# REG/UNREG 응답은 종종 body=null + header.rsp_cd/rsp_msg.
|
||||
# 예전: body 없으면 즉시 return → 서버 거절 코드를 삼킴 (관측 공백).
|
||||
if rsp_cd or rsp_msg or (
|
||||
tr_type in ("1", "2", "3", "4") and not body
|
||||
):
|
||||
line = (
|
||||
"LS WS RSP tr_type=%s tr_cd=%s tr_key=%s rsp_cd=%s rsp_msg=%s"
|
||||
% (
|
||||
tr_type or "-",
|
||||
tr_cd or "-",
|
||||
tr_key or "-",
|
||||
rsp_cd or "-",
|
||||
rsp_msg or "-",
|
||||
)
|
||||
)
|
||||
if rsp_cd and rsp_cd not in ("00000", "0"):
|
||||
logger.warning(line)
|
||||
try:
|
||||
from kis_trader.network.ls_token import is_ls_auth_error
|
||||
|
||||
if is_ls_auth_error(rsp_cd=rsp_cd, rsp_msg=rsp_msg):
|
||||
self._pending_bye = True
|
||||
logger.warning(
|
||||
"LS WS auth RSP → Bye 서킷 후보 (rsp_cd=%s)",
|
||||
rsp_cd,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
logger.info(line)
|
||||
if not body:
|
||||
return
|
||||
if not body:
|
||||
return
|
||||
if tr_cd == "JIF":
|
||||
self._on_jif(body)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user