macro_series_collector silent fail 버그 패치
macro_series_collector silent fail 버그 패치
결론
macro_series_collector.py의 silent fail 버그를 패치했다.
핵심 변경:
- 수집 종료 후
ok/total성공률을 계산한다. 0개 성공이면sys.exit(1).- 성공률이 50% 미만이면
sys.exit(1). - 출력에
X/Y succeeded, Z failed (success_rate=...)요약을 추가했다.
수정 파일은 1개만 변경:
/Users/ron/.openclaw/workspace/scripts/pipeline/macro_series_collector.py
진단
collector 내부 코드 경로
문제 위치는 49개 FRED 시리즈 루프 이후 종료부였다.
패치 전 구조:
client = FredClient()
ok, fail = 0, 0
failed_ids: list[str] = []
for sid in series_to_run:
meta = FRED_SERIES[sid]
try:
records = fetch_fred_series(client, sid, meta, backfill=args.backfill)
if not records:
print(f" ⚠️ {sid}: 데이터 없음")
fail += 1
failed_ids.append(sid)
update_freshness(sid, "", "no_data")
except Exception as e:
print(f" ❌ {sid}: {e}")
fail += 1
failed_ids.append(sid)
try:
update_freshness(sid, "", "error")
except Exception:
pass
print(f"\n완료: {ok}개 성공, {fail}개 실패")
if fail > 0:
print(f" 실패 목록: {', '.join(failed_ids)}")
if not args.dry_run:
try:
from shared.telegram import send_dm
패치 전에는 루프에서 모든 시리즈가 실패해도 예외를 잡고 fail += 1만 수행했다. 이후 main() 끝까지 정상 도달했고, 파일 말미도 단순 호출이었다.
if __name__ == "__main__":
main()
즉, sys.exit(1) 또는 non-zero return이 없어 Python 프로세스 종료코드는 0이 됐다.
Hermes가 성공으로 기록한 이유
Hermes shell cron 판단 경로는 stdout 문구가 아니라 subprocess exit code를 본다.
~/.hermes/hermes-agent/cron/scheduler.py 확인 코드:
proc = subprocess.run(
run_command,
shell=shell_flag,
cwd=str(cwd),
env=env,
text=True,
capture_output=True,
timeout=timeout,
)
success = proc.returncode == 0
error = None if success else f"Exit code {proc.returncode}"
이후 tick 처리에서 mark_job_run(job["id"], success, error)를 호출한다.
success, output, final_response, error = run_job(job)
output_file = save_job_output(job["id"], output)
...
mark_job_run(job["id"], success, error)
cron/jobs.py의 기록 방식:
job["last_run_at"] = now
job["last_status"] = "ok" if success else "error"
job["last_error"] = error if not success else None
따라서 collector가 실패 메시지를 stdout에 찍어도 exit code가 0이면 Hermes는 last_status=ok로 기록한다.
패치 diff
diff --git a/scripts/pipeline/macro_series_collector.py b/scripts/pipeline/macro_series_collector.py
index f6a933a667..92682f9a1b 100644
--- a/scripts/pipeline/macro_series_collector.py
+++ b/scripts/pipeline/macro_series_collector.py
@@ -279,7 +279,10 @@ def main() -> None:
# FRED rate limit 대응 (무료 키: 120 req/min → 0.5초 간격이면 안전)
time.sleep(0.6)
+ total = len(series_to_run)
+ success_rate = (ok / total) if total else 0.0
print(f"\n완료: {ok}개 성공, {fail}개 실패")
+ print(f"요약: {ok}/{total} succeeded, {fail} failed (success_rate={success_rate:.1%})")
if fail > 0:
print(f" 실패 목록: {', '.join(failed_ids)}")
@@ -300,6 +303,13 @@ def main() -> None:
for p in saved:
print(f" {p.name}")
+ if total > 0 and ok == 0:
+ print("❌ macro_series_collector 실패: 성공한 시리즈가 0개입니다.")
+ sys.exit(1)
+ if total > 0 and success_rate < 0.5:
+ print(f"❌ macro_series_collector 실패: 성공률 {success_rate:.1%} < 50.0%")
+ sys.exit(1)
+
if __name__ == "__main__":
main()
before / after 원문
Before:
print(f"\n완료: {ok}개 성공, {fail}개 실패")
if fail > 0:
print(f" 실패 목록: {', '.join(failed_ids)}")
After:
total = len(series_to_run)
success_rate = (ok / total) if total else 0.0
print(f"\n완료: {ok}개 성공, {fail}개 실패")
print(f"요약: {ok}/{total} succeeded, {fail} failed (success_rate={success_rate:.1%})")
Before:
if not args.dry_run and ok > 0:
print(f"\n저장 위치: {MACRO_DIR}")
saved = sorted(MACRO_DIR.glob("*.json"))
print(f"저장된 파일 {len(saved)}개:")
for p in saved:
print(f" {p.name}")
After:
if not args.dry_run and ok > 0:
print(f"\n저장 위치: {MACRO_DIR}")
saved = sorted(MACRO_DIR.glob("*.json"))
print(f"저장된 파일 {len(saved)}개:")
for p in saved:
print(f" {p.name}")
if total > 0 and ok == 0:
print("❌ macro_series_collector 실패: 성공한 시리즈가 0개입니다.")
sys.exit(1)
if total > 0 and success_rate < 0.5:
print(f"❌ macro_series_collector 실패: 성공률 {success_rate:.1%} < 50.0%")
sys.exit(1)
검증 결과
1. 문법 검증
py_compile RC=0
2. 실제 FRED smoke — 현재 실행 환경 DNS 차단 확인
실제 FRED 호출은 현재 Codex 실행 환경에서 DNS가 막혀 실패했다. 이 실패가 이제 exit 1로 잡히는지 확인했다.
[macro_series_collector] 시작 — incremental(최근 60일)
⚠️ dry-run 모드: 실제 저장 없음
❌ DFF: FRED API 요청 실패: <urlopen error [Errno 8] nodename nor servname provided, or not known>
완료: 0개 성공, 1개 실패
요약: 0/1 succeeded, 1 failed (success_rate=0.0%)
실패 목록: DFF
❌ macro_series_collector 실패: 성공한 시리즈가 0개입니다.
RC=1
3. 정상 케이스 — 49/49 성공 시 exit 0 유지
운영 파일을 건드리지 않기 위해 MACRO_DIR, DATA_QUALITY_DIR를 임시 디렉터리로 바꾸고 fetch 성공을 주입해 정상 exit path를 검증했다.
[macro_series_collector] 시작 — incremental(최근 60일)
OK BAMLH0A0HYM2 1건 latest=2026-04-14 1.23
OK BAMLC0A0CM 1건 latest=2026-04-14 1.23
OK NFCI 1건 latest=2026-04-14 1.23
OK STLFSI4 1건 latest=2026-04-14 1.23
OK DFF 1건 latest=2026-04-14 1.23
...
OK RRPONTSYD 1건 latest=2026-04-14 1.23
OK WTREGEN 1건 latest=2026-04-14 1.23
OK WRESBAL 1건 latest=2026-04-14 1.23
...
OK BOGZ1FL663067003Q 1건 latest=2026-04-14 1.23
완료: 49개 성공, 0개 실패
요약: 49/49 succeeded, 0 failed (success_rate=100.0%)
저장 위치: /var/folders/7m/5k4m6j951nzd8fzsp4ds12580000gn/T/macro_collector_success_zi0lb76h/macro-timeseries
저장된 파일 49개:
ADPWNUSNERSA.json
BAMLC0A0CM.json
BAMLH0A0HYM2.json
...
WALCL.json
WRESBAL.json
WTREGEN.json
RC=0
4. 강제 실패 케이스 — FRED_API_KEY 임시 override + 49/49 실패 시 exit 1
FRED_API_KEY='__invalid_forced_test__'를 해당 subprocess에만 주입했다. shell 환경은 이 명령 종료 후 자동 원복된다. 운영 데이터 파일 변경을 피하기 위해 update_freshness와 sleep은 테스트 프로세스 내부에서만 monkeypatch했다.
[macro_series_collector] 시작 — incremental(최근 60일)
❌ BAMLH0A0HYM2: FRED API 오류 400: Bad Request (forced invalid FRED_API_KEY test)
❌ BAMLC0A0CM: FRED API 오류 400: Bad Request (forced invalid FRED_API_KEY test)
❌ NFCI: FRED API 오류 400: Bad Request (forced invalid FRED_API_KEY test)
❌ STLFSI4: FRED API 오류 400: Bad Request (forced invalid FRED_API_KEY test)
❌ DFF: FRED API 오류 400: Bad Request (forced invalid FRED_API_KEY test)
...
❌ RRPONTSYD: FRED API 오류 400: Bad Request (forced invalid FRED_API_KEY test)
❌ WTREGEN: FRED API 오류 400: Bad Request (forced invalid FRED_API_KEY test)
❌ WRESBAL: FRED API 오류 400: Bad Request (forced invalid FRED_API_KEY test)
...
❌ BOGZ1FL663067003Q: FRED API 오류 400: Bad Request (forced invalid FRED_API_KEY test)
완료: 0개 성공, 49개 실패
요약: 0/49 succeeded, 49 failed (success_rate=0.0%)
실패 목록: BAMLH0A0HYM2, BAMLC0A0CM, NFCI, STLFSI4, DFF, T10Y2Y, T10YIE, T5YIE, CPIAUCSL, PCEPILFE, PPIACO, PAYEMS, UNRATE, ICSA, INDPRO, RSAFS, UMCSENT, M2SL, WALCL, DGS2, DGS10, DGS30, MORTGAGE30US, CPILFESL, GASREGW, TTLCONS, DGORDER, TCU, HOUST, TOTALSA, EXHOSLUSM495S, OVXCLS, POILDUBUSDM, DJFUELUSGULF, ID3901, PURANUSDM, DGS5, DFII10, T5YIFR, JTSJOL, ADPWNUSNERSA, RRPONTSYD, WTREGEN, WRESBAL, MMMFFAQ027S, DEXCHUS, CUSR0000SEHC, CPIMEDSL, BOGZ1FL663067003Q
❌ macro_series_collector 실패: 성공한 시리즈가 0개입니다.
RC=1
참고: 이 테스트에서 기존 코드의 실패 알림 분기가 실행되며 텔레그램 전송을 시도했으나 현재 실행 환경 DNS 제한으로 실패했다. 예외는 기존 코드대로 무시됐고, collector exit code는 1로 유지됐다.
Telegram send failed after 3 retries: <urlopen error [Errno 8] nodename nor servname provided, or not known>
5. Hermes shell-job 판단 경로 검증
실제 운영 cron을 즉시 실행하지 않고, Hermes scheduler의 shell-job 실행 함수에 임시 job을 넣어 exit code 1이 success=False로 해석되는지 확인했다.
Python 3.11.15
scheduler_success= False
scheduler_error= Exit code 1
# Script Cron Job: tmp macro collector forced fail (FAILED)
**Exit Code:** 1
output_has_failed_marker= True
output_has_summary= True
RC=0
현재 Hermes job 등록 상태:
{
"id": "macro-series-collector",
"enabled": true,
"state": "scheduled",
"kind": "shell",
"command": "/usr/bin/python3 /Users/ron/.openclaw/workspace/scripts/pipeline/macro_series_collector.py",
"cwd": "/Users/ron/.openclaw/workspace",
"last_status": "ok",
"last_error": null,
"next_run_at": "2026-04-16T06:50:00+09:00"
}
단, hermes cron status는 현재 CLI 기준으로 gateway not running을 보고했다. 반면 18789 포트는 PID 41269가 LISTEN 중이다. 이번 작업 범위는 collector 패치이므로 Gateway 상태는 수정하지 않았다.
✗ Gateway is not running — cron jobs will NOT fire
7 active job(s)
Next run: 2026-04-15T16:46:14+09:00
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
Python 41269 ron 21u IPv4 0x49eb4a93c543c1be 0t0 TCP 127.0.0.1:18789 (LISTEN)
6. diff whitespace 검증
diff_check RC=0
7. 변경 파일 확인
M scripts/pipeline/macro_series_collector.py
판단
- 원인: collector가 실패를 내부 카운터로만 기록하고 process exit code를 0으로 유지했다.
- Hermes 판단: shell-job은
proc.returncode == 0만 성공으로 본다. - 패치 효과: 전부 실패 또는 성공률 50% 미만이면 process exit code가 1이 되어 Hermes
last_status=error로 기록 가능한 상태가 됐다. - 정상 49/49 성공 경로는 exit 0 유지 확인 완료.
자체평가
- 정확성: 5/5 — 0 success silent fail 경로와 Hermes 성공 판단 경로를 코드로 확인했고, exit 1 패치가 적용됐다.
- 완성도: 4.5/5 — 실제 FRED 전체 성공 run은 현재 Codex DNS 제한 때문에 mock 검증으로 대체했지만, 정상/실패 exit path는 모두 확인했다.
- 검증: 4.5/5 — py_compile, diff_check, 49/49 성공, 49/49 실패, Hermes scheduler 판단 경로를 검증했다.
- 최소 변경: 5/5 — 대상 파일 1개, 종료 요약/exit code 처리만 추가했다.
- 종합: 4.75/5
Remaining Risks:
- 현재 hermes cron status가 gateway not running을 보고하지만 18789 LISTEN은 살아 있는 상태다. collector 패치와 별개로 Hermes health 판정 불일치가 남아 있다.
- 실패 알림 send_dm()이 level 없이 호출되는 기존 코드가 남아 있다. 이번 silent-fail 패치 범위를 넘어서 수정하지 않았다.