fix(agent-office): node_monitor _beat_age가 epoch ts 허용 → naver-fetch 영구 down 오탐 해소
매물알림 스펙 §4.4는 fetcher heartbeat ts를 epoch 정수로 정의했는데 _beat_age가 ISO 문자열만 파싱해 int.replace AttributeError → age=None → naver-fetch가 heartbeat 정상 발신 중에도 /nodes에서 영구 alive=false. isinstance(int|float) 분기로 epoch·ISO 둘 다 지원 (신규 테스트 2건). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019jBG2fz48w1bMfaQo8YC6M
This commit is contained in:
@@ -28,9 +28,13 @@ def _get_redis():
|
|||||||
return _redis
|
return _redis
|
||||||
|
|
||||||
|
|
||||||
def _beat_age(ts_str, now):
|
def _beat_age(ts, now):
|
||||||
|
"""ts는 ISO-8601 문자열 또는 epoch 숫자(매물알림 스펙 §4.4) 둘 다 허용."""
|
||||||
try:
|
try:
|
||||||
beat = dt.datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
|
if isinstance(ts, (int, float)):
|
||||||
|
beat = dt.datetime.fromtimestamp(ts, tz=dt.timezone.utc)
|
||||||
|
else:
|
||||||
|
beat = dt.datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
||||||
return max(0, int((now - beat).total_seconds()))
|
return max(0, int((now - beat).total_seconds()))
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -224,3 +224,25 @@ async def test_naver_fetch_http_pull_link():
|
|||||||
st = await node_monitor.collect_status(redis=r)
|
st = await node_monitor.collect_status(redis=r)
|
||||||
link = next(l for l in st["links"] if l["from"] == "naver-fetch")
|
link = next(l for l in st["links"] if l["from"] == "naver-fetch")
|
||||||
assert link["type"] == "http-pull" and link["to"] == "nas-realestate" and link["status"] == "healthy"
|
assert link["type"] == "http-pull" and link["to"] == "nas-realestate" and link["status"] == "healthy"
|
||||||
|
|
||||||
|
|
||||||
|
# ── ts epoch 포맷 지원 (매물알림 스펙 §4.4: naver-fetch는 ts를 epoch 정수로 발신) ──
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_epoch_ts_fresh_is_alive():
|
||||||
|
"""ts가 epoch 정수(스펙 §4.4)여도 신선하면 alive=True."""
|
||||||
|
epoch_now = int(dt.datetime.now(dt.timezone.utc).timestamp())
|
||||||
|
r = FakeRedis(kv={"worker:naver-fetch:heartbeat": _hb("naver-fetch", "fetcher", "idle", ts=epoch_now)})
|
||||||
|
st = await node_monitor.collect_status(redis=r)
|
||||||
|
nf = next(w for w in st["workers"] if w["name"] == "naver-fetch")
|
||||||
|
assert nf["alive"] is True and nf["last_beat_age_s"] is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_epoch_ts_stale_is_dead():
|
||||||
|
"""epoch ts가 90s 초과 과거면 staleness 판정으로 alive=False."""
|
||||||
|
epoch_old = int(dt.datetime.now(dt.timezone.utc).timestamp()) - 300
|
||||||
|
r = FakeRedis(kv={"worker:naver-fetch:heartbeat": _hb("naver-fetch", "fetcher", "idle", ts=epoch_old)})
|
||||||
|
st = await node_monitor.collect_status(redis=r)
|
||||||
|
nf = next(w for w in st["workers"] if w["name"] == "naver-fetch")
|
||||||
|
assert nf["alive"] is False and nf["last_beat_age_s"] >= 300
|
||||||
|
|||||||
Reference in New Issue
Block a user