Temporal 워크플로 시그널: AI 영상 웹훅에서 보내기

영상 작업은 Temporal 액티비티에서 시작하고, 웹훅 수신기가 워크플로 ID로 보내는 시그널을 기다리세요. 시그널과 별개로 실행을 읽는 타이머도 두세요.

읽는 시간 6분Sume
전체 글

Temporal 워크플로 시그널은 클라이언트가 워크플로 ID를 지정해, 실행 중인 워크플로에 보내는 비동기 메시지입니다. 워크플로는 시그널 핸들러에서 이를 처리하는데, 핸들러는 워크플로의 상태를 바꿀 수 있지만 값을 반환할 수는 없고, 보낸 쪽은 워크플로가 처리할 때까지 기다리지 않습니다. 그래서 AI 영상 렌더링 같은 외부 작업이 웹훅을 보낼 때 워크플로를 깨우는 방법으로는 시그널이 자연스럽습니다.

여기서 외부 작업은 Sume Format 실행입니다. 액티비티가 실행을 시작하고, Sume가 결과를 POST하면 웹훅 수신기가 워크플로에 시그널을 보내며, POST가 오지 않는 실행은 타이머가 처리합니다. Temporal 관련 내용은 Temporal의 메시지 전달, Python 메시지 전달, 워크플로 정의, 액티비티, 재시도 정책 페이지에서, Sume 관련 내용은 Format 호출하기 (영문), 실행과 결과 (영문), Run 웹훅 (영문)에서 가져왔습니다. 모두 2026-09-27에 확인했습니다. Sume에는 Temporal 전용 연동이 없으며, 액티비티가 Sume의 HTTPS API를 직접 호출합니다.

API 호출은 왜 워크플로가 아니라 액티비티에 두나요?

Temporal은 워크플로 코드를 재생(replay)할 수 있도록 결정적이어야 한다고 요구하며, API 호출은 액티비티에 두라고 안내합니다. 액티비티는 재생 경로 밖에서 실행되고 자동으로 재시도됩니다. 기본 재시도 정책은 1초 간격에서 시작해 백오프 계수 2.0, 시도 횟수 무제한이며, 그래서 Temporal은 액티비티를 멱등하게 만들라고 권장합니다.

유료 API라면 이 점이 중요합니다. 재시도된 액티비티가 영상을 한 번 더 사서는 안 됩니다. 워크플로 ID와 같은 비즈니스 ID로 만든 Idempotency-Key를 Sume에 보내세요. 같은 키에 같은 본문이면 200과 원래 실행이 반환되고, 두 번째 실행도 두 번째 청구도 없습니다. 같은 키에 다른 본문이면 409 idempotency_conflict이므로, 본문 전체도 그 ID로 만드세요.

  • start_run(order_id)는 /v1/formats/acme/product-promo/runs로 POST합니다. 워크플로 ID로도 쓰는 문자열인 Idempotency-Key: video-<order_id>와 https://hooks.example.com/sume?workflow_id=video-<order_id> 같은 communication.webhook_url을 함께 보내고, data.id를 반환합니다.
  • read_run(run_id)는 /v1/format-runs/{run_id}를 GET하고, 영수증에서 status, primary_output_url, error만 반환합니다.

워크플로는 시그널을 어떻게 기다리나요?

워크플로는 실행을 시작한 뒤, 시그널 핸들러가 그 실행의 ID를 기록할 때까지 workflow.wait_condition에서 대기합니다. timeout을 주면 시간이 다 됐을 때 wait_condition이 asyncio.TimeoutError를 던지며, 워크플로는 어느 쪽이든 실행을 한 번 읽습니다.

import asyncio
from datetime import timedelta
from temporalio import workflow

@workflow.defn
class VideoWorkflow:
    def __init__(self) -> None:
        self.ended_run_id: str | None = None

    @workflow.signal
    def run_ended(self, run_id: str) -> None:
        self.ended_run_id = run_id  # a repeated delivery sets the same value

    @workflow.run
    async def run(self, order_id: str) -> dict:
        t = timedelta(seconds=30)
        run_id = await workflow.execute_activity(start_run, order_id, start_to_close_timeout=t)
        try:
            await workflow.wait_condition(
                lambda: self.ended_run_id == run_id, timeout=timedelta(minutes=100))
        except asyncio.TimeoutError:
            pass  # no webhook came: canceled, or every delivery refused
        return await workflow.execute_activity(read_run, run_id, start_to_close_timeout=t)

웹훅 수신기는 시그널을 어떻게 보내나요?

먼저 원본 본문에 대한 Sume의 서명을 검증합니다. 빈 서명 시크릿은 거부하고, <timestamp>.<raw_body>에 대한 HMAC-SHA256을 계산해 sume-v1= 항목 중 하나라도 상수 시간 비교로 일치하는지, 타임스탬프가 오 분 범위 안에 있는지 확인하세요. 이 코드는 FastAPI·Django에서 Python 웹훅 HMAC 검증하기에 있습니다. 그런 다음 워크플로 ID로 시그널을 보냅니다. 수신기는 보통 워크플로 클래스를 import하지 않으므로, Temporal의 타입 없는 get_workflow_handle을 쓰고 시그널 이름을 넘깁니다.

  • signal은 Temporal 서버가 시그널을 받아들이면 반환하며, 워크플로에 전달될 때까지 기다리지 않습니다. 그래서 수신기는 Sume가 시도마다 주는 10초 안에 2xx로 응답할 수 있습니다.
  • 시그널은 아직 닫히지 않은 워크플로 실행에만 전달되며, 시그널이 실패하면 RPCError가 발생하고 워크플로가 없을 때는 상태가 NOT_FOUND입니다. 이미 사라진 워크플로라면 2xx로 응답하세요. 그러지 않으면 Sume가 재시도하며, 시도는 최대 10회입니다. 다른 오류라면 2xx가 아닌 응답을 보내 재시도를 받으세요.
  • 각 시그널은 워크플로의 Event History에 기록되고 핸들러는 값을 저장하기만 하므로, wait_condition이 실행되기 전에 도착한 웹훅도 사라지지 않습니다.
  • 실행 ID만 보내세요. Sume 영수증은 1 MiB에 이를 수 있지만, Temporal의 셀프 호스팅 기본값은 페이로드가 256 KB면 경고합니다. 영수증은 대신 read_run이 가져옵니다.
from temporalio.client import Client

async def on_verified_delivery(client: Client, workflow_id: str, event: dict) -> None:
    handle = client.get_workflow_handle(workflow_id)  # from ?workflow_id=
    await handle.signal("run_ended", event["run_id"])

수신기에서 Signal-With-Start를 써야 하나요?

아니요, 쓰지 마세요. Signal-With-Start는 시그널을 보내면서, 워크플로가 아직 실행 중이 아니면 워크플로 실행을 시작합니다. Python에서는 start_workflow에 start_signal을 넘깁니다. 웹훅 수신기에서 이를 쓰면 늦게 오거나 반복된 전달이, 이미 있는 영상을 위한 새 워크플로가 되어 버립니다. Signal-With-Start는 작업을 만들어야 하는 메시지에만 쓰고, 결과는 일반 시그널로 보내세요.

워크플로는 얼마나 기다려야 하나요?

Sume 자체의 기한을 넘길 만큼 기다리세요. 아직 진행 중인 실행은 created_at으로부터 90분 뒤에 failed로 강제 종료되므로, 타임아웃을 100분으로 두면 워크플로가 읽을 때쯤에는 실행이 끝나 있습니다. 타임아웃은 Temporal 타이머를 만들고 타이머는 영속적으로 저장되므로, 워커가 재시작돼도 대기는 이어집니다. 이 타이머는 시그널을 보내지 않는 실행도 잡아냅니다.

Sume의 실행과 결과 (영문), Run 웹훅 (영문) 페이지 기준, 2026-09-27 확인.
실행에 일어난 일웹훅워크플로가 보는 것
완료 또는 실패서명된 POST 한 번, 최대 10회 시도까지 재시도시그널
취소보내지 않음타임아웃. read_run에 canceled가 보임
엔드포인트가 10회 시도를 모두 거부전달 실패. 실행은 그대로타임아웃. read_run에 결과가 있음

출처

관련 글

연동 카테고리의 다른 글

연동 글 전체 보기

작성자 Sume