# 설계안 2: Celery Callback Link + 에러 격리 파이프라인 > **`link`/`link_error` 콜백과 단일 큐 + 우선순위 라우팅 기반 설계** --- ## 목차 1. [개요 및 핵심 차이점](#1-개요-및-핵심-차이점) 2. [아키텍처 설계](#2-아키텍처-설계) 3. [데이터 흐름 상세](#3-데이터-흐름-상세) 4. [큐 및 태스크 동작 상세](#4-큐-및-태스크-동작-상세) 5. [코드 구현](#5-코드-구현) 6. [상태 관리 및 모니터링](#6-상태-관리-및-모니터링) 7. [실패 처리 전략](#7-실패-처리-전략) 8. [설계 및 동작 설명](#8-설계-및-동작-설명) 9. [기존안과의 비교](#9-기존안과의-비교) 10. [배포 및 운영](#10-배포-및-운영) --- ## 1. 개요 및 핵심 차이점 ### 1.1 설계 철학 이 설계안은 **`link`(성공 콜백)과 `link_error`(실패 콜백)**을 활용하여 태스크 간 연결을 구현합니다. 기존안(`celery-plan.md`)은 태스크 내부에서 다음 단계를 `apply_async()`로 직접 호출하고, 설계안 1(`chain`)은 API에서 전체 순서를 선언합니다. **이 방식은 태스크 발행 시점에 콜백을 등록**하여, 성공/실패에 따른 분기를 태스크 외부에서 제어합니다. 또한 **단일 큐 + 우선순위 라우팅** 전략으로 인프라를 단순화합니다. ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ 3가지 방식 비교 │ ├─────────────────────────┬───────────────────┬───────────────────────────────┤ │ 기존안 (명시적 전달) │ 설계안 1 (chain) │ 설계안 2 (callback link) │ ├─────────────────────────┼───────────────────┼───────────────────────────────┤ │ 태스크 내부에서 │ API에서 전체 순서를│ 태스크 발행 시 성공/실패 │ │ 다음 태스크를 직접 호출 │ chain으로 선언 │ 콜백을 등록 │ │ │ │ │ │ A() 내부에서 B.delay() │ chain(A|B|C) │ A.apply_async( │ │ │ │ link=B.s(), │ │ │ │ link_error=err.s() │ │ │ │ ) │ ├─────────────────────────┼───────────────────┼───────────────────────────────┤ │ 단순하지만 강한 결합 │ 선언적이지만 │ 유연한 성공/실패 분기 │ │ │ 부분 재시작 어려움 │ + 단일 큐로 인프라 단순화 │ └─────────────────────────┴───────────────────┴───────────────────────────────┘ ``` ### 1.2 핵심 원칙 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 핵심 설계 원칙 │ ├─────────────────────────────────────────────────────────────────┤ │ 1. 콜백 기반 연결: link/link_error로 성공/실패 분기 │ │ 2. 단일 큐 + 우선순위: 3개 큐 대신 1개 큐 + priority 사용 │ │ 3. 에러 격리: 각 단계별 독립 에러 핸들러 │ │ 4. 제어 역전: 태스크가 아닌 발행자가 다음 단계를 결정 │ └─────────────────────────────────────────────────────────────────┘ ``` ### 1.3 단일 큐 + 우선순위 전략 ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ 큐 전략 비교: 3개 큐 vs 1개 큐 + 우선순위 │ └─────────────────────────────────────────────────────────────────────────────┘ [기존안: 3개 독립 큐] ┌──────────┐ ┌──────────┐ ┌──────────┐ │lyric_q │ │ song_q │ │video_q │ │(Worker1) │ │(Worker2) │ │(Worker3) │ └──────────┘ └──────────┘ └──────────┘ • 장점: 완벽한 격리 • 단점: 워커 3개 항상 가동, 유휴 리소스 낭비 [이 설계안: 단일 큐 + 우선순위] ┌─────────────────────────────────────────┐ │ pipeline_queue │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │Priority │ │Priority │ │Priority │ │ │ │ 10 │ │ 5 │ │ 1 │ │ │ │(lyric) │ │ (song) │ │(video) │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ 모든 워커가 모든 태스크 처리 가능 │ │ 우선순위로 실행 순서 자연스럽게 제어 │ └─────────────────────────────────────────┘ • 장점: 워커 풀 공유, 리소스 효율적 • 단점: 특정 유형의 태스크만 처리하는 워커 불가 ``` --- ## 2. 아키텍처 설계 ### 2.1 전체 아키텍처 ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ Callback Link + 단일 큐 아키텍처 │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────┐ │ Client │ └──────┬──────┘ │ ▼ ┌────────────────────┐ │ FastAPI │ │ │ │ lyric.apply_async( │ │ link=song.s(), │◄── 성공 시 song 실행 │ link_error= │ │ err.s() │◄── 실패 시 에러 핸들러 │ ) │ └─────────┬──────────┘ │ ▼ ┌────────────────────────────────────┐ │ pipeline_queue │ │ (단일 큐, 우선순위 지원) │ │ │ │ Priority 10: Lyric 태스크 │ │ Priority 5: Song 태스크 │ │ Priority 1: Video 태스크 │ └───────────────┬────────────────────┘ │ ┌─────────────────────┼─────────────────────┐ │ │ │ ▼ ▼ ▼ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ Worker 1 │ │ Worker 2 │ │ Worker 3 │ │ (모든 태스크) │ │ (모든 태스크) │ │ (모든 태스크) │ └───────────────┘ └───────────────┘ └───────────────┘ │ │ │ │ 각 워커는 lyric, song, video 모두 처리 가능│ │ 우선순위가 높은 태스크(lyric)를 먼저 실행 │ └─────────────────────┴─────────────────────┘ ``` ### 2.2 link/link_error 콜백 구조 ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ link/link_error 콜백 구조 │ └─────────────────────────────────────────────────────────────────────────────┘ lyric.apply_async( kwargs={...}, link=song.s(), ◄── 성공 콜백: lyric 성공 → song 실행 link_error=lyric_error.s(), ◄── 실패 콜백: lyric 실패 → 에러 처리 ) 내부 동작: ━━━━━━━━━ lyric_task 실행 │ ┌──────────┼──────────┐ │ │ [성공 시] [실패 시] │ │ ▼ ▼ link 콜백 실행 link_error 콜백 실행 = song.s() 실행 = lyric_error.s() 실행 │ │ ▼ ▼ song_task 실행 에러 로깅/알림 │ DLQ 저장 ┌────┼────┐ │ │ [성공] [실패] │ │ ▼ ▼ video song_error ``` ### 2.3 콜백 중첩 (Nested Callbacks) ```python # 전체 파이프라인을 콜백 중첩으로 표현 lyric_task.apply_async( kwargs=initial_data, priority=10, # 높은 우선순위 link=song_task.s().set( priority=5, # 중간 우선순위 link=video_task.s().set( priority=1, # 낮은 우선순위 link_error=video_error_handler.s() ), link_error=song_error_handler.s() ), link_error=lyric_error_handler.s() ) # 풀어서 설명: # lyric 성공 → song 실행 # song 성공 → video 실행 # video 성공 → 파이프라인 완료 # video 실패 → video_error_handler # song 실패 → song_error_handler # lyric 실패 → lyric_error_handler ``` --- ## 3. 데이터 흐름 상세 ### 3.1 시퀀스 다이어그램 ```mermaid sequenceDiagram participant C as Client participant API as FastAPI participant Q as pipeline_queue participant W1 as Worker (lyric) participant W2 as Worker (song) participant W3 as Worker (video) participant EH as Error Handler participant DB as MySQL C->>API: POST /pipeline/start API->>Q: lyric_task + link(song) + link_error(err) API-->>C: {"task_id": "xxx"} Q->>W1: lyric_task (priority=10) W1->>DB: Lyric 생성 + ChatGPT alt 성공 W1-->>Q: SUCCESS → link 콜백 발동 Note over Q: Celery가 자동으로 song_task를 큐에 발행 Q->>W2: song_task (priority=5) W2->>DB: Song 생성 + Suno alt 성공 W2-->>Q: SUCCESS → link 콜백 발동 Q->>W3: video_task (priority=1) W3->>DB: Video 생성 + Creatomate W3-->>Q: SUCCESS Note over Q: 파이프라인 완료 else 실패 W2-->>EH: link_error → song_error_handler EH->>DB: 실패 상태 기록 end else 실패 W1-->>EH: link_error → lyric_error_handler EH->>DB: 실패 상태 기록 end ``` ### 3.2 우선순위 기반 태스크 처리 ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ 우선순위 기반 태스크 처리 │ └─────────────────────────────────────────────────────────────────────────────┘ 시간축 → ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ T+0 pipeline_queue: ┌────────────────────────────────────────────────────────────┐ │ [P=10] lyric_A │ [P=10] lyric_B │ [P=5] song_X │ │ └────────────────────────────────────────────────────────────┘ Worker1 → lyric_A 처리 (P=10, 가장 높은 우선순위) Worker2 → lyric_B 처리 T+5 lyric_A 완료 → link 콜백으로 song_A가 큐에 추가 (P=5) ┌────────────────────────────────────────────────────────────┐ │ [P=5] song_X │ [P=5] song_A │ │ └────────────────────────────────────────────────────────────┘ Worker1 → song_X 처리 (같은 우선순위면 FIFO) T+60 song_X 완료 → video_X 추가 (P=1) ┌────────────────────────────────────────────────────────────┐ │ [P=5] song_A │ [P=1] video_X │ │ └────────────────────────────────────────────────────────────┘ Worker1 → song_A 처리 (P=5 > P=1) ※ video_X는 모든 song 태스크보다 낮은 우선순위 효과: • 새로운 파이프라인 요청(lyric)이 가장 먼저 처리됨 • 이미 진행 중인 파이프라인의 후반 단계(video)는 자연스럽게 후순위 • 전체 처리량(throughput)보다 응답성(latency)을 우선시 ``` ### 3.3 데이터 전달 방식 ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ Callback에서의 데이터 전달 │ └─────────────────────────────────────────────────────────────────────────────┘ link 콜백에서 이전 태스크의 결과는 다음 태스크의 첫 번째 인자로 전달됩니다. 이는 chain과 동일한 동작입니다. lyric_task 반환값: { "task_id": "xxx", "lyric_id": 42, "status": "completed" } │ │ link 콜백 발동 ▼ song_task(prev_result={...}) ◄── 자동으로 첫 번째 인자 song_task 반환값: { "task_id": "xxx", "song_id": 15, "song_result_url": "...", "status": "completed" } │ │ link 콜백 발동 ▼ video_task(prev_result={...}) ◄── 자동으로 첫 번째 인자 ``` --- ## 4. 큐 및 태스크 동작 상세 ### 4.1 단일 큐 설정 ```python # Redis 큐 우선순위 설정 # Redis는 기본적으로 우선순위 큐를 지원하지 않지만, # Celery가 priority_steps로 가상 우선순위를 구현합니다. celery_app.conf.update( # 브로커 전송 옵션 broker_transport_options={ 'priority_steps': list(range(11)), # 0~10 우선순위 레벨 'sep': ':', 'queue_order_strategy': 'priority', # 우선순위 기반 처리 }, ) ``` ### 4.2 Redis에서의 우선순위 구현 ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ Redis 우선순위 큐 내부 구조 │ └─────────────────────────────────────────────────────────────────────────────┘ Celery가 Redis에서 우선순위를 구현하는 방법: priority_steps=[0,1,2,3,4,5,6,7,8,9,10] 설정 시 Redis에 다음과 같은 키가 생성됩니다: pipeline_queue (priority 0 - 가장 높음) pipeline_queue\x06\x16\x01 (priority 1) pipeline_queue\x06\x16\x02 (priority 2) ... pipeline_queue\x06\x16\x0a (priority 10 - 가장 낮음) 워커가 BRPOP으로 여러 키를 동시에 감시하되, 낮은 번호(높은 우선순위) 키를 먼저 확인합니다. 이 프로젝트에서의 우선순위 할당: lyric_task → priority=1 (가장 먼저 처리) song_task → priority=5 (중간) video_task → priority=9 (가장 나중에) ``` ### 4.3 워커 동작 ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ 단일 큐 워커 동작 │ └─────────────────────────────────────────────────────────────────────────────┘ # 모든 워커가 하나의 큐를 구독 celery -A app.celery_app worker -Q pipeline_queue -c 4 동작 방식: 1. Worker가 pipeline_queue를 BRPOP으로 감시 2. 우선순위 높은 메시지부터 가져옴 3. 메시지의 task 타입에 따라 해당 함수 실행 4. 완료 시 link 콜백이 자동으로 다음 태스크 발행 모든 워커가 모든 태스크를 처리할 수 있으므로: • 유휴 워커가 없음 (한 유형의 태스크가 없어도 다른 유형 처리) • 특정 유형에 병목이 생기면 자연스럽게 모든 워커가 분담 • 스케일링이 단순: 워커 수만 조절하면 됨 ``` --- ## 5. 코드 구현 ### 5.1 Celery 앱 설정 ```python # app/celery_app.py """ Callback Link + 단일 큐 Celery 설정 핵심 차이점: - 큐가 1개뿐 (pipeline_queue) - 우선순위로 태스크 유형별 처리 순서 제어 - 모든 워커가 모든 태스크 처리 가능 """ from celery import Celery from kombu import Queue, Exchange import os celery_app = Celery( 'o2o_castad', broker=os.getenv('CELERY_BROKER_URL', 'redis://localhost:6379/0'), backend=os.getenv('CELERY_RESULT_BACKEND', 'redis://localhost:6379/1'), include=[ 'app.tasks.lyric_tasks', 'app.tasks.song_tasks', 'app.tasks.video_tasks', 'app.tasks.error_handlers', # 에러 핸들러 모듈 ] ) # ============================================================================ # 단일 큐 정의 (우선순위 지원) # ============================================================================ pipeline_exchange = Exchange('pipeline', type='direct') celery_app.conf.task_queues = ( Queue( 'pipeline_queue', pipeline_exchange, routing_key='pipeline', queue_arguments={ 'x-max-priority': 10, # 우선순위 범위: 0~10 } ), ) # 기본 큐 설정 - 모든 태스크가 pipeline_queue로 라우팅 celery_app.conf.task_default_queue = 'pipeline_queue' celery_app.conf.task_default_routing_key = 'pipeline' # 우선순위 설정 celery_app.conf.broker_transport_options = { 'priority_steps': list(range(11)), 'sep': ':', 'queue_order_strategy': 'priority', } celery_app.conf.update( task_serializer='json', accept_content=['json'], result_serializer='json', timezone='Asia/Seoul', enable_utc=True, task_acks_late=True, task_reject_on_worker_lost=True, worker_prefetch_multiplier=1, result_expires=86400, result_extended=True, ) ``` ### 5.2 에러 핸들러 모듈 ```python # app/tasks/error_handlers.py """ 단계별 에러 핸들러 link_error 콜백으로 사용되는 에러 처리 태스크입니다. 각 단계별로 독립된 에러 핸들러가 있어, 실패 원인에 맞는 대응이 가능합니다. """ import logging import redis import json from datetime import datetime from app.celery_app import celery_app logger = logging.getLogger(__name__) redis_client = redis.Redis.from_url( celery_app.conf.result_backend, decode_responses=True ) # ============================================================================ # 공통 에러 핸들러 로직 # ============================================================================ def _handle_error(stage: str, request, exc, traceback): """ 공통 에러 처리 로직 1. 로깅 2. Redis 상태 업데이트 3. DLQ 저장 4. 알림 발송 (선택) """ task_id = 'unknown' # link_error 콜백에서는 request 객체로 원본 태스크 정보 접근 if hasattr(request, 'kwargs') and request.kwargs: # 원본 태스크의 kwargs에서 task_id 추출 if isinstance(request.kwargs, dict): task_id = request.kwargs.get('task_id', 'unknown') # prev_result에서 추출 (chain/link에서 전달된 경우) elif hasattr(request, 'args') and request.args: first_arg = request.args[0] if isinstance(first_arg, dict): task_id = first_arg.get('task_id', 'unknown') logger.error( f"[{stage.upper()} ERROR] task_id={task_id}, error={exc}", exc_info=True ) # Redis 상태 업데이트 pipeline_key = f"pipeline:{task_id}:status" stage_key = f"pipeline:{task_id}:{stage}" redis_client.hset(pipeline_key, mapping={ 'current_stage': stage, 'status': 'failed', }) redis_client.hset(stage_key, mapping={ 'status': 'failed', 'error': str(exc), 'failed_at': datetime.utcnow().isoformat(), }) redis_client.expire(pipeline_key, 86400) redis_client.expire(stage_key, 86400) # DLQ에 실패 정보 저장 redis_client.lpush('failed_tasks', json.dumps({ 'stage': stage, 'task_id': task_id, 'error': str(exc), 'traceback': str(traceback), 'failed_at': datetime.utcnow().isoformat(), })) # ============================================================================ # 단계별 에러 핸들러 # ============================================================================ @celery_app.task(name='app.tasks.error_handlers.lyric_error_handler') def lyric_error_handler(request, exc, traceback): """ 가사 생성 실패 핸들러 ChatGPT API 실패 원인: - API 키 만료 - Rate limit 초과 - 네트워크 타임아웃 - 잘못된 프롬프트 """ _handle_error('lyric', request, exc, traceback) # 가사 실패 시 추가 처리: # - API 키 상태 확인 알림 발송 # - Rate limit인 경우 자동 재시도 예약 (선택) @celery_app.task(name='app.tasks.error_handlers.song_error_handler') def song_error_handler(request, exc, traceback): """ 노래 생성 실패 핸들러 Suno API 실패 원인: - 생성 타임아웃 - Rate limit - 오디오 다운로드 실패 - Azure 업로드 실패 """ _handle_error('song', request, exc, traceback) # 노래 실패 시 추가 처리: # - 가사는 이미 완료됨 → 재시도 시 가사부터 안해도 됨 # - Suno 크레딧 확인 알림 @celery_app.task(name='app.tasks.error_handlers.video_error_handler') def video_error_handler(request, exc, traceback): """ 비디오 생성 실패 핸들러 Creatomate 실패 원인: - 렌더링 타임아웃 - 템플릿 오류 - 이미지 로드 실패 - Azure 업로드 실패 """ _handle_error('video', request, exc, traceback) # 비디오 실패 시 추가 처리: # - 가사 + 노래 모두 완료 상태 → 비디오만 재시도 가능 # - Creatomate 크레딧 확인 알림 ``` ### 5.3 가사 생성 태스크 ```python # app/tasks/lyric_tasks.py """ 가사 생성 태스크 (Callback Link 방식) chain 방식과 유사하게, 이 태스크는 다음 단계를 모릅니다. 결과를 반환하면 link 콜백이 자동으로 다음 태스크를 실행합니다. 핵심 차이점: 우선순위 설정 (priority=1, 가장 높음) """ from sqlalchemy import select import asyncio import logging from app.celery_app import celery_app from app.tasks.base import BaseTaskWithDB from app.home.models import Project from app.lyric.models import Lyric from app.utils.chatgpt_prompt import ChatgptService from app.utils.prompts.prompts import Prompt logger = logging.getLogger(__name__) @celery_app.task( base=BaseTaskWithDB, bind=True, name='app.tasks.lyric_tasks.generate_lyric', max_retries=3, default_retry_delay=30, acks_late=True, # ──────────────────────────────────────────── # 단일 큐에서는 큐 이름 대신 우선순위로 구분 # priority=1 → 가장 먼저 처리 # ──────────────────────────────────────────── ) def generate_lyric(self, data: dict) -> dict: """ 가사 생성 태스크 반환값이 link 콜백(song_task)의 첫 번째 인자가 됩니다. Args: data: 초기 파이프라인 데이터 Returns: dict: 다음 태스크에 전달될 결과 """ task_id = data['task_id'] self.update_pipeline_status( task_id=task_id, stage='lyric', status='processing', message='가사 생성 시작' ) async def _generate(): async with self.get_db_session() as session: project = await session.scalar( select(Project).where(Project.task_id == task_id) ) if not project: project = Project( task_id=task_id, customer_name=data['customer_name'], region=data['region'], ) session.add(project) await session.flush() prompt = Prompt( customer_name=data['customer_name'], region=data['region'], detail_region_info=data['detail_region_info'], language=data.get('language', 'Korean'), ) lyric_prompt = prompt.get_full_prompt() lyric = Lyric( project_id=project.id, task_id=task_id, status='processing', lyric_prompt=lyric_prompt, language=data.get('language', 'Korean'), ) session.add(lyric) await session.commit() lyric_id = lyric.id # ChatGPT 호출 (DB 세션 외부) try: chatgpt = ChatgptService() lyric_result = await chatgpt.generate_lyric(lyric_prompt) if not lyric_result or len(lyric_result.strip()) < 50: raise ValueError("가사가 너무 짧음") except Exception as e: async with self.get_db_session() as session: lyric = await session.get(Lyric, lyric_id) lyric.status = 'failed' lyric.lyric_result = f"Error: {str(e)}" await session.commit() raise # 결과 저장 async with self.get_db_session() as session: lyric = await session.get(Lyric, lyric_id) lyric.status = 'completed' lyric.lyric_result = lyric_result await session.commit() return { 'task_id': task_id, 'lyric_id': lyric_id, 'lyric_result': lyric_result, 'language': data.get('language', 'Korean'), 'status': 'completed', } result = self.run_async(_generate()) self.update_pipeline_status( task_id=task_id, stage='lyric', status='completed', message='가사 생성 완료' ) # ──────────────────────────────────────────────────────────── # 다음 태스크를 직접 호출하지 않음! # link 콜백이 자동으로 이 결과를 song_task에 전달합니다. # ──────────────────────────────────────────────────────────── return result ``` ### 5.4 노래/비디오 태스크 노래 및 비디오 태스크는 설계안 1(chain)과 구조적으로 동일합니다. `prev_result`를 첫 번째 인자로 받아 처리하고, 결과를 반환합니다. > 설계안 1의 `song_tasks.py`, `video_tasks.py`를 그대로 사용합니다. > 유일한 차이: `queue` 지정이 없음 (단일 큐이므로) ### 5.5 파이프라인 API (핵심 차이점) ```python # app/api/routers/v1/pipeline.py """ Callback Link 기반 파이프라인 API 핵심 차이점: 1. chain() 대신 link/link_error 콜백 사용 2. 단일 큐에 우선순위로 발행 3. 각 단계별 독립 에러 핸들러 등록 """ from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from app.dependencies.auth import get_current_user from app.tasks.lyric_tasks import generate_lyric from app.tasks.song_tasks import generate_song from app.tasks.video_tasks import generate_video from app.tasks.error_handlers import ( lyric_error_handler, song_error_handler, video_error_handler, ) router = APIRouter(prefix="/pipeline", tags=["Pipeline"]) class StartPipelineRequest(BaseModel): task_id: str customer_name: str region: str detail_region_info: str language: str = "Korean" orientation: str = "vertical" genre: str = "pop, ambient" @router.post("/start") async def start_pipeline( request: StartPipelineRequest, current_user=Depends(get_current_user) ): """ 파이프라인 시작 (Callback Link 방식) link/link_error를 중첩하여 전체 파이프라인을 구성합니다. 각 단계별로 독립된 에러 핸들러가 등록됩니다. """ initial_data = { "task_id": request.task_id, "customer_name": request.customer_name, "region": request.region, "detail_region_info": request.detail_region_info, "language": request.language, } # ──────────────────────────────────────────────────────────────── # 핵심: 콜백 중첩으로 파이프라인 구성 # ──────────────────────────────────────────────────────────────── # # 읽는 순서: 안쪽부터 바깥으로 # video_task ← song 성공 시 실행 (priority=9) # song_task ← lyric 성공 시 실행 (priority=5) # lyric_task ← 즉시 실행 (priority=1) # # 에러 핸들러: 각 단계별 독립 # lyric 실패 → lyric_error_handler # song 실패 → song_error_handler # video 실패 → video_error_handler result = generate_lyric.apply_async( kwargs={'data': initial_data}, priority=1, # 가장 높은 우선순위 # lyric 성공 시 → song 실행 link=generate_song.s( genre=request.genre, ).set( priority=5, # 중간 우선순위 # song 성공 시 → video 실행 link=generate_video.s( orientation=request.orientation, ).set( priority=9, # 가장 낮은 우선순위 link_error=video_error_handler.s(), # video 실패 시 ), link_error=song_error_handler.s(), # song 실패 시 ), link_error=lyric_error_handler.s(), # lyric 실패 시 ) return { 'success': True, 'task_id': request.task_id, 'celery_task_id': result.id, 'message': '파이프라인이 시작되었습니다.', } @router.post("/retry/{task_id}/{stage}") async def retry_stage( task_id: str, stage: str, current_user=Depends(get_current_user) ): """ 특정 단계 재시도 실패한 단계부터 콜백을 다시 구성하여 재시작합니다. """ if stage == 'song': # DB에서 lyric 결과 조회 후 song부터 재시작 lyric_result = await _get_lyric_result(task_id) result = generate_song.apply_async( args=(lyric_result,), priority=5, link=generate_video.s().set( priority=9, link_error=video_error_handler.s(), ), link_error=song_error_handler.s(), ) elif stage == 'video': song_result = await _get_song_result(task_id) result = generate_video.apply_async( args=(song_result,), priority=9, link_error=video_error_handler.s(), ) return {'resumed_from': stage, 'task_id': result.id} ``` --- ## 6. 상태 관리 및 모니터링 ### 6.1 상태 추적 기존안의 Redis 커스텀 상태(`pipeline:{task_id}:*`)를 그대로 사용합니다. 추가로 에러 핸들러에서도 상태를 업데이트합니다. ### 6.2 Flower 모니터링 ```bash # 단일 큐이므로 더 단순한 모니터링 celery -A app.celery_app flower --port=5555 # Flower에서 볼 수 있는 정보: # - pipeline_queue의 대기 태스크 수 (전체) # - 태스크 유형별 필터링 (lyric/song/video) # - 우선순위별 분포 ``` --- ## 7. 실패 처리 전략 ### 7.1 단계별 독립 에러 핸들링 ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ 에러 격리 전략 │ └─────────────────────────────────────────────────────────────────────────────┘ 이 방식의 가장 큰 장점: 각 단계별로 독립된 에러 핸들러 [기존안/chain] ───────────── 모든 실패 → 동일한 에러 처리 또는 chain 자체 실패 [이 방식] ───────── lyric 실패 → lyric_error_handler ├─ ChatGPT API 키 확인 ├─ Rate limit 대기 후 재시도 예약 └─ 관리자에게 API 상태 알림 song 실패 → song_error_handler ├─ Suno 크레딧 확인 ├─ 이미 완료된 lyric 보존 └─ song만 재시도 가능 video 실패 → video_error_handler ├─ Creatomate 렌더링 오류 분석 ├─ 이미 완료된 lyric + song 보존 └─ video만 재시도 가능 각 에러 핸들러는 해당 단계의 실패 원인에 맞는 구체적인 대응이 가능합니다. 이것이 "에러 격리"의 핵심입니다. ``` --- ## 8. 설계 및 동작 설명 ### 8.1 link vs chain vs 명시적 전달 상세 비교 ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ 태스크가 다음 단계를 "아는가"에 따른 분류 │ └─────────────────────────────────────────────────────────────────────────────┘ 태스크가 다음 단계를 안다? │ ┌──────────────┼──────────────┐ │ │ │ ▼ ▼ ▼ [안다 (Yes)] [모른다 (No)] [모른다 (No)] │ │ │ ▼ ▼ ▼ ┌─────────────┐ ┌───────────┐ ┌──────────────┐ │ 기존안 │ │ 설계안 1 │ │ 설계안 2 │ │ (명시적 전달)│ │ (chain) │ │ (link 콜백) │ └─────────────┘ └───────────┘ └──────────────┘ │ │ │ ▼ ▼ ▼ 다음 단계 지식이 API에서 chain API에서 link 태스크 코드에 선언 콜백으로 연결 하드코딩 (파이프라인만) (에러 핸들러도 단계별 등록) ``` ### 8.2 단일 큐의 장단점 ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ 단일 큐 장단점 분석 │ └─────────────────────────────────────────────────────────────────────────────┘ 장점: ───── ✓ 인프라 단순화: Redis 큐 1개, 워커 타입 1개 ✓ 리소스 효율: 유휴 워커 없음, 모든 워커가 모든 작업 처리 ✓ 스케일링 단순: 워커 수만 조절 ✓ 운영 부담 감소: 워커 3종류 → 1종류 관리 단점: ───── ✗ 격리 불가: video 작업이 lyric 워커 리소스 사용 가능 ✗ 리소스 불균형: ChatGPT(가벼움)와 Creatomate(무거움) 같은 워커 ✗ 우선순위 한계: Redis 우선순위가 RabbitMQ만큼 정확하지 않음 ✗ 특정 워커 튜닝 불가: 메모리, CPU 한도를 태스크 유형별로 설정 불가 이 프로젝트에서의 판단: ────────────────────── • 트래픽이 적은 초기 단계에서는 단일 큐가 효율적 • 트래픽 증가 시 3개 큐로 전환 가능 (호환됨) • 우선순위로 "새 요청 우선 처리"가 가능하여 UX 향상 ``` --- ## 9. 기존안과의 비교 ``` ┌──────────────────────┬──────────────┬──────────────┬──────────────┐ │ 기준 │ 기존안 │ 설계안 1 │ 설계안 2 │ │ │ (명시적 전달)│ (chain) │ (link+단일큐)│ ├──────────────────────┼──────────────┼──────────────┼──────────────┤ │ 큐 구성 │ 3개 독립 큐 │ 3개 독립 큐 │ 1개 우선순위 │ │ 워커 구성 │ 3종류 │ 3종류 │ 1종류 │ │ 태스크 독립성 │ 중간 │ 높음 │ 높음 │ │ 에러 핸들링 │ 태스크 내 │ link_error │ 단계별 독립 │ │ 부분 재시작 │ 쉬움 │ 약간 번거로움│ 쉬움 │ │ 인프라 복잡도 │ 중간 │ 중간 │ 낮음 │ │ 리소스 효율 │ 중간 │ 중간 │ 높음 │ │ 적합한 규모 │ 중~대 │ 중 │ 소~중 │ │ Celery 숙련도 │ 초급 │ 중급 │ 중급 │ └──────────────────────┴──────────────┴──────────────┴──────────────┘ ``` --- ## 10. 배포 및 운영 ### 10.1 실행 명령어 ```bash # 워커 실행 - 모든 워커가 동일한 큐 구독 # 워커 수만 조절하면 됨 celery -A app.celery_app worker -Q pipeline_queue -c 4 -n worker1@%h celery -A app.celery_app worker -Q pipeline_queue -c 4 -n worker2@%h celery -A app.celery_app worker -Q pipeline_queue -c 4 -n worker3@%h # 또는 단일 워커로 시작 (개발환경) celery -A app.celery_app worker -Q pipeline_queue -c 4 -n dev@%h ``` ### 10.2 Docker Compose ```yaml # docker-compose.yml (단순화됨) version: '3.8' services: redis: image: redis:7-alpine ports: ["6379:6379"] api: build: . ports: ["8000:8000"] command: uvicorn main:app --host 0.0.0.0 --port 8000 # 워커가 1종류뿐이므로 스케일링이 단순 worker: build: . command: celery -A app.celery_app worker -Q pipeline_queue -c 4 deploy: replicas: 3 # 워커 3개 (모든 태스크 처리) ``` --- ## 문서 버전 | 버전 | 날짜 | 변경 내용 | |------|------|-----------| | 1.0 | 2024-XX-XX | 초안 작성 |