fix: VI 배지 자동 만료 — 해제 1h 메시지 미수신 시 영구 잔류 방지
- 관찰(2026-06-11): VI 발동 1h 이후 해제 메시지가 안 오고 1224(해제시각)도 발동시각과 동일 → active=true 박제, 배지가 안 사라짐 - get_vi/active_vis 읽기 시점에 발동 후 180초(2분+임의연장30s+여유) 경과 시 active=False 처리 (_vi_expired). 저장 원본은 유지 - 1h 원본 필드 stderr 로그 추가 — 9068/1224 매핑 실증용 (VI 드물어 부담 없음) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,8 @@ _BACKOFF_MAX = 30.0
|
|||||||
_MAX_CONN_AGE = 25 * 60
|
_MAX_CONN_AGE = 25 * 60
|
||||||
# recv 타임아웃 — 이 시간 내 아무것도 안 오면 루프 돌며 stop/age 체크
|
# recv 타임아웃 — 이 시간 내 아무것도 안 오면 루프 돌며 stop/age 체크
|
||||||
_RECV_TIMEOUT = 5.0
|
_RECV_TIMEOUT = 5.0
|
||||||
|
# VI 배지 자동 만료 (초) — 해제 1h 메시지 미수신 대비. VI 단일가 2분 + 임의연장 ≤30초 + 여유 30초.
|
||||||
|
_VI_ACTIVE_TTL = 180.0
|
||||||
# 장 마감 시간대 대기 — 개장 임박(평일 07:00~08:00)엔 1분, 그 외엔 다음 영업일 07:00까지 자되 6시간 상한.
|
# 장 마감 시간대 대기 — 개장 임박(평일 07:00~08:00)엔 1분, 그 외엔 다음 영업일 07:00까지 자되 6시간 상한.
|
||||||
_PREOPEN_POLL_SEC = 60.0 # 개장 임박: 1분마다 → 08:00 개장을 1분 안에 포착
|
_PREOPEN_POLL_SEC = 60.0 # 개장 임박: 1분마다 → 08:00 개장을 1분 안에 포착
|
||||||
_CLOSED_WAIT_CAP = 6 * 3600.0 # 밤·주말·휴장: 다음 개장까지 자되 이 상한(자가교정용 — 날짜 계산 오류 시에도 6h마다 재확인)
|
_CLOSED_WAIT_CAP = 6 * 3600.0 # 밤·주말·휴장: 다음 개장까지 자되 이 상한(자가교정용 — 날짜 계산 오류 시에도 6h마다 재확인)
|
||||||
@@ -97,11 +99,31 @@ class RealtimeHub:
|
|||||||
def get_vi(self, code: str) -> dict | None:
|
def get_vi(self, code: str) -> dict | None:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
v = self._vi.get(kc._clean_code(code))
|
v = self._vi.get(kc._clean_code(code))
|
||||||
return dict(v) if v else None
|
return self._vi_expired(v) if v else None
|
||||||
|
|
||||||
def active_vis(self) -> dict[str, dict]:
|
def active_vis(self) -> dict[str, dict]:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
return {c: dict(v) for c, v in self._vi.items() if v.get('active')}
|
out = {}
|
||||||
|
for c, v in self._vi.items():
|
||||||
|
ev = self._vi_expired(v)
|
||||||
|
if ev.get('active'):
|
||||||
|
out[c] = ev
|
||||||
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _vi_expired(v: dict) -> dict:
|
||||||
|
"""VI 자동 만료 — 해제(1h) 메시지가 안 오는 경우 대비.
|
||||||
|
VI 단일가는 2분 + 임의연장 ≤30초라, 발동 후 _VI_ACTIVE_TTL 초과 시 읽기 시점에 active=False.
|
||||||
|
저장 상태는 건드리지 않는다(실증·디버깅용 원본 유지)."""
|
||||||
|
ev = dict(v)
|
||||||
|
if ev.get('active') and ev.get('ts'):
|
||||||
|
try:
|
||||||
|
age = (datetime.now(KST) - datetime.fromisoformat(ev['ts'])).total_seconds()
|
||||||
|
if age > _VI_ACTIVE_TTL:
|
||||||
|
ev['active'] = False
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return ev
|
||||||
|
|
||||||
def is_connected(self) -> bool:
|
def is_connected(self) -> bool:
|
||||||
return self._connected.is_set()
|
return self._connected.is_set()
|
||||||
@@ -328,6 +350,9 @@ class RealtimeHub:
|
|||||||
}
|
}
|
||||||
elif typ == '1h': # VI 발동/해제
|
elif typ == '1h': # VI 발동/해제
|
||||||
# active 판정: 9068 VI발동구분. 해제시각(1224) 유무와 함께 실증 확인 예정(A단계).
|
# active 판정: 9068 VI발동구분. 해제시각(1224) 유무와 함께 실증 확인 예정(A단계).
|
||||||
|
# 실증 로그 — VI는 드물어 부담 없음. 2026-06-11 관찰: 발동 후 해제 1h가 안 오고
|
||||||
|
# 1224(해제시각)가 발동시각과 동일 → 읽기 시점 TTL 만료(_vi_expired)로 방어 중.
|
||||||
|
print(f'[1h raw] {code} {vals}', file=sys.stderr, flush=True)
|
||||||
kind_raw = (vals.get('9068') or '').strip()
|
kind_raw = (vals.get('9068') or '').strip()
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._vi[code] = {
|
self._vi[code] = {
|
||||||
|
|||||||
Reference in New Issue
Block a user