본문 바로가기
C.W.K.
Lesson 03 of 04 · published

시도 하나에 키 하나

~14 min · 멱등성, 재시도, 장애 복구, 유료 api

Level 0스케치
0 XP0/43 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

돌아오지 않은 답

워커가 클립을 Ember에 보냈는데 연결이 끊겼어. Ember가 받았을까? 워커 쪽에선 알 길이 없어. 요청이 가는 길에 사라졌을 수도 있고, 도착해서 유료 렌더까지 시작했는데 답만 돌아오다 사라졌을 수도 있어. 두 장군 문제의 축소판이야. 아무리 기다려도 풀리지 않아.

이미지라면 Avatars는 조심스럽고 느리게 답해. 작업을 uncertain으로 두고, 누가 다시 시도하기 전에 Ember 기록에서 작업 id를 찾아 대조해. 그건 트랙 4에서 따라가 봤지. 클립은 더 나은 답을 받아. Ember의 Pro Video는 호출하는 쪽이 정한 멱등 키를 받거든. Ember의 기록 장부는 request_id를 유니크 제약이 걸린 칸에 저장해. Ember의 create()는 계획도 지출도 하기 전에 그 키부터 찾아봐. 같은 키가 오면 이미 만든 클립을 돌려줘.

시도에 이름 붙이기

clip_request_id()의 논리는 한 줄이야. 키는 avatars-, 작업 id, 시도 번호 순서야. 시도 번호는 작업의 시도 기록 길이에 1을 더한 값이고. 무작위인 건 하나도 없고, 작업 밖에 따로 저장하는 것도 없어. 작업 레코드만 있으면 누구든 키를 다시 계산할 수 있어. 방금 다시 켜진 워커한테 딱 필요한 성질이지.

키는 시도 기록이 늘어날 때만 바뀌어. 기록이 느는 곳은 두 군데야. Ember가 제출을 받아주면 워커가 받아준 클립 id를 덧붙여. 아빠가 작업을 다시 시도하면 retry()가 방금 끝난 시도를 상태와 오류까지 덧붙여. 그래서 Ember가 거절한 첫 제출은 avatars-<task>-1로 나가고, 그 뒤 재시도는 -2로 나가. Ember가 받아준 뒤 벤더가 실패한 클립은 둘 다 기록돼 있어서 재시도가 -3으로 나가. 번호가 시도 횟수를 정확히 세는 건 아니야. 사람이 다시 돈을 내기로 한 순간엔 새 키가 되고, 나머지 모든 순간엔 그대로면 그걸로 충분해.

입력 먼저, 요청은 하나

뭘 만들기도 전에 run()은 요청한 소울이 이 작업의 피사체를 그려도 되는지 피파에게 다시 물어. 검토와 제출 사이에 어떤 피사체가 보호 대상이 될 수도 있으니까. 레퍼런스는 하나씩 Ember의 비디오 입력 저장소(POST /api-video/inputs)로 올라가. 돌아올 땐 역할이 붙은 입력 id가 돼. 본문엔 키, 확정된 생성기, 프롬프트, 확정된 설정, 그 입력들이 들어가. 검토 때 이미 받은 동의라서 confirm: true도 붙어. source_metadata엔 배치, 작업, 시도 번호, 등장하는 소울들이 적혀. 워커는 시각과 함께 submitting을 먼저 기록하고, 그 뒤에야 보내.

답은 셋, 그중 하나는 다시야

돌아온 것이미지 작업클립 작업
4xx 거절failed, Submission refusedfailed, Submission refused, 청구는 없어
전송 오류나 5xxuncertain, 다시 하기 전에 대조queued로 돌아가. 단계 문구는 Ember가 안 닿아 다시 시도한다는 뜻의 Ember unreachable; will retry야. 10초 쉬고, 키는 그대로
202잡 id와 함께 running클립 id와 함께 running, 시도도 기록
제출 중에 워커가 죽었을 때재시작하면 uncertain재시작하면 queued

핵심은 가운데 두 줄이야. 키가 있으면 모르는 결과가 더는 위험하지 않아. 같은 키로 다시 보내면 도착 못 한 클립을 만들거나 이미 만든 클립을 돌려줘. 어느 쪽이든 값을 치르는 클립은 딱 하나야. 다음 차례에 레퍼런스를 또 올리긴 해. 그건 Ember 디스크를 조금 쓸 뿐 크레딧은 안 써.

4xx는 일부러 다르게 다뤄. 거절은 청구 전에 Ember나 벤더가 안 된다고 말한 거야. 예를 들면 레퍼런스 이미지를 주면 8초 길이만 받는 Veo 모델이 있어. 거절된 요청을 다시 보내 봤자 또 거절이야. 그래서 작업은 벤더 자신의 말을 단 채 failed로 멈추고, 새 시도는 사람이 정해.

재시작 때 하는 정리

워커는 켜지자마자 파일 잠금부터 잡아. 데이터베이스 주인을 워커 하나로 못 박는 거야. 그러고 나서 지난 워커가 죽을 때 한창 진행 중이던 작업을 거둬들여. preparing에 걸린 작업은 큐로 돌아가. 대개는 아직 아무것도 안 보낸 작업이야. 클립엔 예외가 있어. 앞서 낸 제출을 Ember가 받아줬는데 답이 사라진 클립은 같은 키를 단 채 queued로 돌아갔다가, 다음 차례에 다시 preparing으로 들어와. 그래도 큐로 돌리는 건 안전해. 시도 기록이 안 늘었으니 다음에 보낼 때도 키가 같고, Ember는 이미 만든 클립을 돌려주거든. submitting에 걸린 작업은 종류에 따라 갈라. 이미지는 uncertain으로 가. 지난 제출이 Ember에 닿았을 수도 있는데 물어볼 키가 없거든. 클립은 바로 queued로 돌아가. 옆 주석이 이유를 대. 요청 id가 Ember에서 제출을 멱등하게 만든다고.

그걸 증명하는 테스트

test_a_lost_answer_or_a_restart_never_pays_twice는 위험한 길을 차례로 걸어.

  1. Ember가 503으로 답해. 작업은 큐로 돌아가고, 시도 기록은 비어 있고, Ember는 아무것도 안 만들었어.
  2. 테스트가 작업을 억지로 submitting에 두고 새 워커를 켜. 클립은 큐로 돌아갔다가 나가서 끝나. Ember가 받은 요청 id는 avatars-<task>-1 딱 하나고, Ember가 가진 클립도 하나야.
  3. 두 번째 요청은 8초 길이 얘기를 단 422로 거절돼. 작업은 그 말을 단 채 failed야.
  4. 명시적으로 다시 시도하면, Ember가 보는 요청은 -2로 끝나.
원칙: 키는 돈을 내겠다는 결정에 붙어. 선이 몇 번을 끊기든, 결정이 하나면 키도 하나야.

Code

cwkAvatars, backend/worker.py: 시도 하나에 멱등 키 하나 (원문 그대로, 16-23행)·python
def is_clip(task):
    return task.get("media") == "clip"


def clip_request_id(task):
    """One idempotency key per attempt: re-posting it after a crash returns
    the clip Ember already made instead of paying for another."""
    return f"avatars-{task['id']}-{len(task['attempts']) + 1}"
cwkAvatars, backend/worker.py: 제출, 그리고 안 닿으면 왜 큐로 돌아가나 (원문 그대로, 158-174행)·python
            settings = task["clip"]
            body = {"request_id": clip_request_id(task), "generator": task["provider"], "prompt": task["prompt"], "aspect_ratio": settings["aspect_ratio"], "resolution": settings["resolution"], "duration_secs": settings["duration_secs"], "audio": settings["audio"], "negative_prompt": settings.get("negative_prompt"), "inputs": inputs,
                    # Consent was settled when the batch was accepted (studio.generate).
                    "confirm": True,
                    "source_metadata": {**batch["request"].get("source_metadata", {}), "source_app": "cwkAvatars", "batch_id": batch["id"], "task_id": id, "attempt": len(task["attempts"]) + 1, "soul_ids": task["subjects"], "ordinal": task["ordinal"]}}
            task = await io("update", id, expected=task["version"], state="submitting", phase="Submitting to Ember", submitted_at=now())
            try:
                accepted = await upstream.video_create(body)
            except StudioError as exc:
                if 400 <= exc.status < 500:
                    await io("update", id, state="failed", error=str(exc), phase="Submission refused")
                else:
                    # The same request id is safe to send again: leave it for the next pass.
                    await io("update", id, state="queued", phase="Ember unreachable; will retry", error=str(exc))
                    await asyncio.sleep(10)
                return
            task = await io("update", id, state="running", phase=accepted.get("phase") or "Generating", job_id=accepted["id"], error=None, attempts=[*task["attempts"], {"job_id": accepted["id"], "submitted_at": task["submitted_at"]}])
cwkAvatars, backend/worker.py: 재시작하면 이미지는 불확실, 클립은 큐로 (원문 그대로, 43-50행)·python
        for task in await self.studio.io("list", "task", states=("preparing",)):
            await self.studio.io("update", task["id"], state="queued", phase="Queued")
        for task in await self.studio.io("list", "task", states=("submitting",)):
            if is_clip(task):
                # The clip's request id makes the submit idempotent in Ember.
                await self.studio.io("update", task["id"], state="queued", phase="Queued")
                continue
            await self.studio.io("update", task["id"], state="uncertain", phase="Reconcile before retry", error="The previous submit may have reached Ember. Check history before submitting again.")

External links

Exercise

네 소프트웨어에서 다른 서비스에 유료로 뭔가를 만들고, 오류가 나면 다시 시도하는 호출을 하나 찾아봐. 멱등 키는 보낼 때 만든 무작위 값 말고 네가 저장한 레코드에서 뽑아. 작업 id에 시도 번호를 붙이는 식으로. 보낼 때마다 그 키를 실어. 전송 오류나 5xx면 같은 키로 큐에 남겨 두고, 4xx면 실패로 끝내. 시도 번호는 사람이나 규칙이 명시적으로 다시 하기로 했을 때만 올려. 마지막으로 submitting을 기록한 뒤 답을 받기 전에 워커를 죽였다가 다시 켜 봐. 서비스가 키 하나를 보고 하나만 만들었는지 확인해.
Hint
서비스에 멱등 키가 없으면 이 방법은 못 써. 그땐 불확실 상태로 돌아가서, 요청 메타데이터에 직접 넣은 id로 대조해. 키를 딕셔너리에 저장하고 같은 키가 오면 저장한 결과를 돌려주는 가짜 서비스 하나면, 돈 한 푼 안 쓰고 흐름 전체를 시험할 수 있어.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.