step 자동 재시도(publish 제외) + terminal failed의 실패 step 수동 재개(텔레그램 [재시도]). orchestrator + retry 엔드포인트 + youtube_publisher 실패 알림. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.7 KiB
7.7 KiB
music/YouTube 파이프라인 신뢰성·복구 — 설계
작성 2026-06-12. YouTube 자동화 파이프라인의 step 실패를 자동 재시도(일시적)하고, 영구 실패는 실패 step부터 수동 재개(텔레그램 [🔄재시도])할 수 있게 한다. "music/YouTube 파이프라인 고도화" 중 신뢰성/복구 슬라이스.
1. 목표
파이프라인 step(cover→video→thumb→meta→review→publish) 실패가 ① 일시적이면 자동 재시도로 흡수하고, ② 영구적이면 terminal failed로 둔 뒤 이전 산출물을 보존한 채 실패 step부터 재개할 수 있게 한다. 현재는 step 한 번 실패하면 전체 파이프라인이 terminal failed가 되고 복구 경로가 없어 처음부터 다시 만들어야 한다.
2. 배경 (현재 동작)
orchestrator.run_step(pipeline_id, step, feedback):pipeline_jobsrow 생성 → step 실행 → 성공 시update_pipeline_state(next_state), 예외 시pipeline_jobs.status='failed'+ 파이프라인state='failed'+failed_reason="{step}: {e}". 재시도/재개 없음.- 항상
bg.add_task(orchestrator.run_step, pid, step, ...)로 BackgroundTask 호출(start_pipeline→cover, feedback→next_step, publish_pipeline→publish). - 이전 step 산출물(
cover_url/video_url/thumbnail_url/metadata_json/review_json)은 파이프라인 row에 보존됨 → 실패 step만 재실행하면 이어갈 수 있는 구조. state_machine: STEPS,_APPROVE_NEXT, TERMINAL_STATES={published, cancelled, failed, awaiting_manual}.agent-office youtube_publisher.poll_state_changes:*_pending신규 진입만 텔레그램 알림.failed는 무알림(silent) — 사용자가 실패를 모름.
3. 요구사항 (확정)
- 자동 재시도: step 실행 실패 시
STEP_MAX_RETRIES(기본 2 → 총 3회)까지 backoff 재시도. 소진 후 terminalfailed._resolve_input에러(입력/설정)는 재시도 안 함(재시도해도 안 고쳐짐).publishstep은 자동 재시도 제외 — youtube 업로드는 비멱등(중복 업로드 위험). 1회 시도 후 실패면 즉시 terminal.- 재시도 대상 =
cover/video/thumb/meta/review.
- 수동 재개: terminal
failed파이프라인을 실패 step부터 재실행. 이전 산출물 보존.- publish 재개 가드:
youtube_video_id가 이미 있으면 재개 거부(원 업로드 성공 가능성 → 중복 방지).
- publish 재개 가드:
- 실패 알림: 영구 실패 시 텔레그램 알림 + 인라인
[🔄재시도]버튼(현재 silent 갭 해소). - 범위 밖(YAGNI): stuck 감지(*_running hang / *_pending 방치). 수동 재시도로 복구 가능하므로 이번 슬라이스 제외.
4. 아키텍처
3 컴포넌트:
[music-lab orchestrator] run_step: step 실행을 재시도 루프로 (publish 제외) → 소진 시 failed
[music-lab API] POST /api/music/pipeline/{id}/retry → 실패 step부터 run_step 재트리거
[agent-office] youtube_publisher: failed 감지 → 텔레그램 알림+[🔄재시도]
webhook: ytpub_retry_{pid} → service_proxy.pipeline_retry → music-lab retry
5. music-lab 상세
5.1 자동 재시도 (pipeline/orchestrator.py)
- 상수:
STEP_MAX_RETRIES = 2,STEP_RETRY_BACKOFF_SEC = [5, 15](시도 간 대기),NON_RETRY_STEPS = {"publish"}. run_step의 step 실행부(현재 try lines 31-47)를 루프로:attempts = 1 if step in NON_RETRY_STEPS else (STEP_MAX_RETRIES + 1) for i in range(attempts): try: result = await _dispatch_step(step, p, ctx, feedback) update_pipeline_job(job_id, status="succeeded") update_pipeline_state(pipeline_id, result["next_state"], **fields) return except Exception as e: last = e if i < attempts - 1: add_log/pipeline_job note "retry {i+1}" await asyncio.sleep(STEP_RETRY_BACKOFF_SEC[min(i, len-1)]) # 소진 update_pipeline_job(job_id, status="failed", error=str(last)) update_pipeline_state(pipeline_id, "failed", failed_reason=f"{step}: {last}")_resolve_input실패는 루프 진입 전 early-return(현행 유지, 재시도 X).- 재시도 시도 가시화:
pipeline_jobs에 attempt별 기록(또는 error 메시지에 "attempt n/N").
5.2 resume 엔드포인트 (main.py)
POST /api/music/pipeline/{id}/retry:- 파이프라인 조회 없으면 404.
state != "failed"→ 409 "재개 불가 (state=...)".- 실패 step 판별:
db.get_last_failed_step(pipeline_id)(pipeline_jobs에서 status='failed' 최신 step). 없으면failed_reason.split(":")[0].strip()폴백. - 실패 step이
publish이고youtube_video_id가 이미 있으면 → 409 "이미 업로드됨 (중복 방지)". bg.add_task(orchestrator.run_step, pid, failed_step)재트리거. 반환{ok: true, retrying_step}.
db.get_last_failed_step(pipeline_id) -> str | None헬퍼 신규.
6. agent-office 상세
6.1 실패 알림 (agents/youtube_publisher.py)
poll_state_changes:_STEP_TITLES(*_pending) 처리 후,state == "failed"인 파이프라인도 검사.- 신규 failed(중복 방지:
self._notified_failed: set[int], 또는 기존 dict에 ('failed', reason_hash))면 텔레그램 발송:⚠️ [{track_title}] 파이프라인 #{id} '{step}' 실패\n사유: {failed_reason}+ 인라인[🔄 재시도](callback_dataytpub_retry_{id}). - 발송 후 notified 기록.
- 신규 failed(중복 방지:
service_proxy.list_active_pipelines()가 failed를 포함하는지 확인 — 미포함이면 failed도 반환하도록 보강(또는 별도 조회). (plan에서 확인.)
6.2 재시도 콜백 (telegram/webhook.py)
_handle_callback에callback_id.startswith("ytpub_retry_")분기 →_handle_ytpub_retry._handle_ytpub_retry:pid = int(callback_id.removeprefix("ytpub_retry_"))→service_proxy.pipeline_retry(pid)→ 결과 텔레그램 회신("재개: {step}" / 거부 사유).service_proxy.pipeline_retry(pid)신규:POST {MUSIC_LAB_URL}/api/music/pipeline/{pid}/retry.
7. 에러 처리 / 엣지
- 재시도 backoff 중 컨테이너 재시작 → 해당 step 작업 유실, 파이프라인 비-terminal stuck. 범위 밖이나 수동 [🔄재시도]로 복구 가능(안전망).
- resume 시 state≠failed → 409(중복 재개·동시성 방지). 텔레그램 [🔄재시도] 중복 탭도 멱등 거부.
- pipeline_jobs에 failed row 없고 state만 failed →
failed_reasonprefix 폴백. - publish 재개 +
youtube_video_id존재 → 409(중복 업로드 방지). - 알림 중복: notified 기록으로 같은 failed 1회만 발송.
8. 테스트
- orchestrator (재시도): step 2회 실패 후 성공 → next_state 도달(3시도). 끝까지 실패 → failed. publish는 1시도 후 즉시 failed(재시도 X).
_resolve_input실패 → 재시도 없이 failed. - API retry: failed→run_step 재트리거(mock 확인) + retrying_step 반환. 비-failed→409. publish+youtube_video_id→409.
- db:
get_last_failed_step— 최신 failed job step 반환, 없으면 None. - agent-office: poll 신규 failed→텔레그램 발송(중복 방지).
_handle_ytpub_retry→service_proxy.pipeline_retry 호출 + pid 파싱.
9. 영향받는 파일
- music-lab:
app/pipeline/orchestrator.py(재시도 루프 +_dispatch_step추출),app/main.py(retry 엔드포인트),app/db.py(get_last_failed_step),tests/. - agent-office:
app/agents/youtube_publisher.py(failed 알림),app/telegram/webhook.py(ytpub_retry 디스패치),app/service_proxy.py(pipeline_retry, 필요 시list_active_pipelinesfailed 포함),tests/. - web-backend/CLAUDE.md music API 표 +
service_music.md메모리 갱신.