From ffb96de61dd30e25eb3ba95b14a4222168d0d309 Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 12 Jun 2026 00:08:01 +0900 Subject: [PATCH] =?UTF-8?q?docs(spec):=20music/YouTube=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EC=8B=A0=EB=A2=B0?= =?UTF-8?q?=EC=84=B1=C2=B7=EB=B3=B5=EA=B5=AC=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit step 자동 재시도(publish 제외) + terminal failed의 실패 step 수동 재개(텔레그램 [재시도]). orchestrator + retry 엔드포인트 + youtube_publisher 실패 알림. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...06-12-music-pipeline-reliability-design.md | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-12-music-pipeline-reliability-design.md diff --git a/docs/superpowers/specs/2026-06-12-music-pipeline-reliability-design.md b/docs/superpowers/specs/2026-06-12-music-pipeline-reliability-design.md new file mode 100644 index 0000000..b340bb0 --- /dev/null +++ b/docs/superpowers/specs/2026-06-12-music-pipeline-reliability-design.md @@ -0,0 +1,105 @@ +# 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_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_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` 메모리 갱신.