Files
web-page-backend/docs/superpowers/specs/2026-06-12-music-pipeline-reliability-design.md
gahusb ffb96de61d docs(spec): music/YouTube 파이프라인 신뢰성·복구 설계
step 자동 재시도(publish 제외) + terminal failed의 실패 step 수동 재개(텔레그램 [재시도]). orchestrator + retry 엔드포인트 + youtube_publisher 실패 알림.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 00:08:01 +09:00

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_jobs row 생성 → 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 재시도. 소진 후 terminal failed.
    • _resolve_input 에러(입력/설정)는 재시도 안 함(재시도해도 안 고쳐짐).
    • publish step은 자동 재시도 제외 — youtube 업로드는 비멱등(중복 업로드 위험). 1회 시도 후 실패면 즉시 terminal.
    • 재시도 대상 = cover/video/thumb/meta/review.
  • 수동 재개: terminal failed 파이프라인을 실패 step부터 재실행. 이전 산출물 보존.
    • publish 재개 가드: youtube_video_id가 이미 있으면 재개 거부(원 업로드 성공 가능성 → 중복 방지).
  • 실패 알림: 영구 실패 시 텔레그램 알림 + 인라인 [🔄재시도] 버튼(현재 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_data ytpub_retry_{id}).
    • 발송 후 notified 기록.
  • service_proxy.list_active_pipelines()가 failed를 포함하는지 확인 — 미포함이면 failed도 반환하도록 보강(또는 별도 조회). (plan에서 확인.)

6.2 재시도 콜백 (telegram/webhook.py)

  • _handle_callbackcallback_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_reason prefix 폴백.
  • 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_pipelines failed 포함), tests/.
  • web-backend/CLAUDE.md music API 표 + service_music.md 메모리 갱신.