Dayward AI
Week 1 · D4About 5 hours

From Shot to Footage: Image-to-Video, Polling Async Tasks, and Retrying Failures

Video generation is the slowest, most expensive, and most failure-prone step in the whole pipeline; today turn it into a safely retryable async task, and learn to write prompts in shot language the model actually understands.

Today's goals 0/3

Sign in to tick these off and save your progress.

Today's Goals

  1. Generate a single shot from a first-frame image plus a prompt, and explain the trade-off between text-to-video and image-to-video
  2. Write a correct async task-polling loop that handles queuing, timeouts, backoff, and cancellation
  3. Classify video API failures, distinguishing which are worth retrying from which retrying won't fix

Yesterday you locked down characters and sets, so you hold a batch of stable stills. Today you make them move — but the hard part is not the picture. It is every engineering consequence of one call taking several minutes. When you finish, scroll back up and tick the three goals off.

Plain-Language Walkthrough

How long a shot runs and how it is shot: putting camera language into the prompt

Start with the thing beginners skip: what exactly are you asking the model to shoot?

On a soundstage a director never just says "shoot the lead downstairs." They say: medium shot, slow push in, six seconds, the lead crouched beside the scooter, a lit phone screen in his hand. Those four things — shot size, camera movement, duration, and the picture — are all required. Leave one out and the operator guesses, and the guess is never what you wanted.

A video API is the same, except it cannot ask a follow-up question. So the storyboard record gives each of the four its own field: shotSize is the framing (only wide, medium, close; do not invent a fourth), camera is the movement, durationSec is the length, visual is the picture. Assemble the prompt as what we shoot, then how, then the style. That order is not arbitrary: prompts have a 2000-character ceiling, and truncation always eats the tail — so the least important piece goes last, where losing it costs least.

Duration carries a counterintuitive trade-off. The API defaults to six seconds, and the first instinct is "then generate thirty seconds in one go." Don't. Longer clips fall apart in the middle — a character walks along and their face changes halfway. Failure cost scales linearly: one failed thirty-second clip burns what five six-second clips would. And a long clip cannot be partially redone — a problem at second eighteen means re-running everything, whereas six-second shots means re-running one. Short-form drama already cuts every three to eight seconds, so the engineering optimum and the editorial optimum coincide.

Now text-to-video versus image-to-video. Text-to-video skips an image call and looks cheaper, but it hands total control of the frame to the model, and the face you locked down yesterday goes unused. Image-to-video starts from a first frame and acts forward from there: one extra image buys you composition, lighting and appearance pinned to a still you already approved.

Whether that trade is worth it depends on the gap in magnitude. Images are listed at CNY 0.025 apiece. For video, the official pay-as-you-go page lists CNY 0.50 per second at 768P, CNY 0.80 at 2K, and CNY 0.33 at 480P. So one six-second shot costs a hundred-odd times what one still costs — spending two cents to lower its failure rate is a guaranteed win. (Those per-second tiers are the pay-as-you-go path; today's model is billed against a package credit balance instead. The two ledgers are not interchangeable, and D12 unpacks that.)

Which gives today's first ruling: use the approved still you picked on D3 as the video's first frame. How to pass it, which field it goes in, and why the real trouble only starts after that step — because this API does not hand you the video on the spot.

The first frame: actually using yesterday's output

The image-to-video field is first_frame_image. It takes a public URL or a Base64 data URL, accepts JPG, PNG and WebP, and must be under 20MB. Any approved still in yesterday's asset library goes straight in, as long as it can become an address the API can fetch.

The certainty it buys is larger than it sounds. Once the first frame is given, the clip's opening frame is that image — composition, lighting, wardrobe and face all settled, and the model only pushes six seconds forward from there. Yesterday's reference image governs "do these images look like the same person"; today's first frame governs "where does this shot start acting from." They are a relay, not a repetition; character consistency was covered yesterday and is not reopened here.

One practice matters when writing alongside a first frame: describe what happens next, not what the frame already shows. The rainy night, the scooter and the blue work jacket are already there; repeating them just spends characters. Write "he slowly straightens up and looks toward the lit window upstairs" instead.

And one switch must go off up front: prompt_optimizer defaults to true in the video API (the image API defaults it to false — exactly opposite, so don't mix them up). Left on, the server rewrites your prompt invisibly and the same input can produce completely different results twice. For reproducibility, set it to false.

submit.js
const SHOT_SIZE_TEXT = { wide: 'wide shot', medium: 'medium shot', close: 'close-up' }
 
// Order: picture, framing, camera, style. Truncation eats the tail, so style goes last
function shotPrompt(shot) {
  return `${shot.visual}. ${SHOT_SIZE_TEXT[shot.shotSize]}, ${shot.camera}. ${STYLE}`
}
 
async function submit(shot, firstFrameUrl) {
  const res = await fetch(`${process.env.MINIMAX_BASE_URL}/v1/video_generation`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.MINIMAX_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'MiniMax-Hailuo-2.3',
      prompt: shotPrompt(shot),
      first_frame_image: firstFrameUrl, // the approved still picked yesterday
      duration: Math.round(shot.durationSec),
      resolution: '768P',
      prompt_optimizer: false, // defaults to true here and quietly rewrites your prompt
    }),
  })
  const json = await res.json()
  if (json.base_resp?.status_code) throw new Error(`submit failed ${json.base_resp.status_code}`)
  return json.task_id // only an identifier comes back; not one frame exists yet
}

The shape of an async task: four steps, every one of which can fail

Look at the last line of that code: what comes back is a task_id, not a video.

This is the real subject of the chapter. The image API is synchronous — send, wait a few seconds, take the result. Video cannot be: it runs for minutes, and no HTTP connection wants to hang that long. So the shape becomes four steps:

  1. Submit: POST /v1/video_generation, receive a task_id.
  2. Poll: GET /v1/query/video_generation?task_id=, checking status repeatedly. Note task_id is a query parameter, not a path parameter. The status values are capitalized: Preparing, Queueing, Processing, Success, Fail.
  3. Collect: on success, take the file_id out of the query result and call GET /v1/files/retrieve?file_id= to exchange it for a download address.
  4. Download: put the file on disk immediately.

In crew terms: submitting hands the call sheet to the production office and the task_id is that sheet's number; polling is walking over to ask whether your shot is done; collecting is taking the number to the vault for the tape. Each step has its own way of going wrong — the sheet gets lost, the take is unusable, the vault has no such tape, the network drops on the way back.

One trap to name early: the same vendor may run two video APIs with different shapes at once. Besides the v1 line used here, MiniMax has a v2 line. The difference is shape, not capability — query is a path parameter, the status values are lowercase, and success returns the result address directly, with no collect step at all. Parse one line's response against the other's docs and you get a pile of nulls plus a suspicion that your network is broken. This course uses v1 only; in your own project, pin which line you use in both code and docs.

The VideoProvider.generate() from D1 wraps those four steps into a single await, which is pleasant to call. Today you take the wrapper apart, because what breaks is always the layer inside it: in production you see "this shot never came out," and fixing it means knowing which step it is stuck on. A bonus: each step becomes injectable, so every failure can be rehearsed offline — which is what today's lab does.

The manners and the cost of polling

Now write that loop. It looks simple, but three parts will bite you in production.

First, the interval cannot be fixed. Polling a five-minute queue at one second is three hundred useless requests. The query endpoint has its own rate limit, so you easily rate-limit yourself, then read "video generation failed: rate limited" and blame the generation endpoint. Use exponential backoff: start at five seconds and multiply by a factor (say 1.5) each round.

Second, the backoff needs a ceiling. Uncapped, by round ten you are checking every few minutes — the task finished long ago and you are still asleep. Around 20 seconds is reasonable.

Third, the timeout check goes before the sleep. The common version sleeps first and then checks the budget, which always oversleeps by a full round — 20 wasted seconds at a 20-second interval. The correct test is: would sleeping take me past the deadline? If so, give up now.

And one ordering issue: check the terminal states before the timeout. If a task returns success on the very last query and you discard it as a timeout, you have paid for it and lost the output.

poll.js
export async function pollUntilDone(taskId, opts) {
  const startedAt = Date.now()
  const deadline = startedAt + opts.timeoutMs
  let waitMs = opts.initialWaitMs // 5000
 
  for (let attempt = 1; ; attempt += 1) {
    const { status, fileId } = await query(taskId)
 
    // Terminal states first: a task that succeeds on the last query must not be killed
    if (status === 'Success') {
      if (!fileId) throw new Error('task succeeded but returned no file_id')
      return fileId
    }
    if (status === 'Fail') throw new TaskFailedError(taskId)
 
    // The test is "would the sleep cross the deadline", not "did we overrun after sleeping"
    if (Date.now() + waitMs > deadline) {
      throw new TaskTimeoutError(taskId, Date.now() - startedAt)
    }
 
    await sleep(waitMs)
    waitMs = Math.min(Math.round(waitMs * opts.factor), opts.maxWaitMs) // backoff needs a cap
  }
}

That code handles one task. A real episode has forty shots, and pushing forty at once hits the vendor's concurrency and quota limits, so you need a gate on how many run at a time. That gate and quota accounting are D9's material; today, get one task right.

The result file is temporary

Once the task succeeds you hold a file_id, and what that buys is not a video either, but a temporary download address.

The docs do not say how long it lives, so the only safe assumption is that it can expire at any moment. There is exactly one correct behavior: download to disk the instant you have it, and store the local path in your database and indexes — never the URL.

The violation is subtle. Everything works for months, because the download always happens seconds after the address arrives. Then you add an optimization that "collects all the addresses first, then downloads them together," and the incident arrives. Put the download inside the same function as the collect step, so the two are structurally inseparable; that beats a reminder comment.

After it lands, record two more things: which task_id this shot came from, and what prompt was used. Video is the most expensive artifact on this line, and being unable to answer "how was this generated" means paying again.

The failure classification table

The last section is about failure, the part of this chapter interviews ask about most. The criterion is not the leading digit of the status code but three questions: will waiting help? will changing the input help? or does a human have to step in?

CodeMeaningClassHandling
1002Rate limitedWaiting helpsBack off and retry; the program handles it
Server errorVendor faultWaiting helpsBack off and retry; the program handles it
2013Invalid parameterChanging input helpsFix the request body; ten thousand retries give the same error
1026, 1027Content moderationChanging input helpsChange the prompt or the first frame, not the retry count
1004Authentication failedA human must step inAlert immediately; the program cannot fix it
1008Insufficient balanceA human must step inAlert immediately; the program cannot fix it

The table's main value is isolating the class that burns money for nothing. Invalid parameters and moderation repeat the same error on every retry while consuming the rate limit that genuinely retryable calls need. Classifying those two correctly saves more than any amount of clever retry logic. Why moderation triggers and how to rewrite a prompt is D11's compliance topic; today, just get the class right.

Write the table as a function and the retry logic collapses to a single check:

retry.js
// Back off only for errors classified as retry; everything else throws at once, wasting no budget
export async function withRetry(fn, { attempts, initialWaitMs, maxWaitMs }) {
  let waitMs = initialWaitMs
  for (let attempt = 1; ; attempt += 1) {
    try {
      return await fn()
    } catch (err) {
      const verdict = classify(err)
      if (verdict.action !== 'retry' || attempt >= attempts) throw err
      console.log(`attempt ${attempt} failed (${verdict.kind}), retrying in ${waitMs}ms`)
      await sleep(waitMs)
      waitMs = Math.min(waitMs * 2, maxWaitMs)
    }
  }
}

One class is missing from the table and needs more care than any row in it: timeouts. A timeout is not a failure; it is "we do not know whether it succeeded." The task may still be queued, or may already have finished. Resubmit straight away and you may pay twice for the same shot. The correct order is: check the idempotency key for a finished artifact first, resubmit only if there is none. Idempotency keys and resuming from checkpoints are D8's workflow engine; today, write that sentence into the timeout log line so your future self knows the trap is there.

Finally, one batch-level ruling, consistent with yesterday: a single failed shot must not abort the batch. If shot seven of forty is blocked, record it, keep running, and report the failure list at the end. Aborting throws away the money spent on the first six — and since one shot costs a hundred-odd times one image, that arithmetic is harsher than yesterday's.

Source Reading

Hands-On Lab

🧪 D4 lab: an async task client that turns one storyboard record into a playable shot video

Code location: labs/ai-drama-pipeline/day-04-shot-video

Acceptance criteria:

  1. In step 2 of MOCK=1 pnpm start, the four polling lines show a next-wait that grows each time, the last line is a success terminal state, and the video file lands on disk.
  2. Step 3 prints a timeout give-up, and the seconds actually waited match the budget — both leaving early and leaving late fail, and the program prints the difference.
  3. In step 4's classification table, 1002 is retry, 1004 and 1008 are alert, 1026, 1027 and 2013 are fix-input, and no column is uniformly one handling.
  4. Use the injection parameters to force a moderation failure; the program exits immediately, says the prompt itself must change, and the log contains no retry lines.
  5. pnpm typecheck passes with no any.

Fault injection uses the course-wide INJECT variable, with five values today: timeout keeps the task queued forever, ratelimit makes the first submit hit the rate limit, blocked hits moderation, badrequest hits an invalid parameter, and taskfail drives the task to a failed terminal state. Injection applies only at the network egress layer, so the polling state machine and the classifier still run for real — you test your own code, not the stub.

starter/ leaves four exercises: two in the polling loop (timeout budget, backoff), one in the classification table, one in prompt assembly. Everything runs offline under MOCK=1 — the offline queue really walks the chain from queued to success and can be switched to "queued forever," so no line of your polling code is bypassed. Run it once unmodified first: you will see a constant interval, a timeout that leaves one second early, and a table that is retry all the way down. Those three symptoms are your to-do list.

  1. Write submit, poll, collect and download as four separate functions, run the happy path once, and confirm the log shows the status moving from queued to success.
  2. Run it again using a D3 approved still as the first frame, and compare the composition against the run without one.
  3. Change polling to exponential backoff with a ceiling, inject a permanently queued task, and confirm the program gives up at the moment the budget runs out rather than early or never.
  4. Write the classifier so the table's six codes land in retry, fix-input and alert respectively, and inject a rate limit to verify the backoff retry really happens.
  5. Inject a moderation failure and confirm the program does not retry and gives a readable recommendation instead.

Interview Questions

Today's 3 questions are in the question bank below, focused on writing an async task client correctly, polling backoff and timeout cancellation, and error classification for generative APIs. Read the analysis before the key points — question 1 is close to mandatory for any role touching generative APIs, and listing the failure cases completely matters more than answering elegantly. The cn / global labels let you pick by target market.

Checklist and Tomorrow

  • I can generate a single shot from a first-frame image plus a prompt, and explain the trade-off between text-to-video and image-to-video
  • I can write a correct async task-polling loop that handles queuing, timeouts, backoff, and cancellation
  • I can classify video API failures, distinguishing which are worth retrying from which retrying won't fix
  • I can explain why a timeout is not a failure, and what must happen before any retry
  • I can explain why the download has to live in the same step as the collect
  • All 5 lab acceptance criteria pass
  • I can answer at least 2 of the 3 interview questions without looking at the key points

Tomorrow (D5) we give these shots a voice. Each character gets its own timbre, the lines are synthesized in bulk, and then we do something whose ordering matters: use the measured real speech duration to correct the shot durations in reverse. Today's shots were generated at planned lengths, but how many seconds a line actually takes is only knowable after synthesis — and that "picture first or sound first" loop decides the ordering of the whole pipeline. D5 also produces the subtitle file from that timeline, the sole input to the editing bay the day after.

Interview questions

  • You are asked to implement a client for an asynchronous generation task. Which failure cases would you cover?让你实现一个异步生成任务的客户端,你会考虑哪些失败情况?
    Common in ChinaCommon overseasIntermediate#async-task#error-handling

    How to reason about it · think before answering

    1. The differentiator here is coverage, not code. Answering 'wrap it in try/catch and retry' usually means you have never run this kind of API in production.
    2. Describe the shape first so the failures have somewhere to hang: submit and get an id, poll for status, retrieve a URL, download to disk — four steps, four families of failure.
    3. Then enumerate: at submit, rate limiting, auth failure, invalid parameters, content moderation; at poll, the query endpoint rate limiting you, a status that never advances, or a terminal failure; at retrieve, a valid id that yields no URL; at download, an expired link, a stream cut halfway, a disk write error.
    4. Then the two that span the whole flow: timeout and process restart. A timeout is not a failure, it is 'I don't know' — you must look up the idempotency key before resubmitting. A restart means in-memory task ids are gone, so the id has to be persisted before or immediately after the request, or you will have paid-for tasks you can never reclaim.
    5. Close with a line that shows judgment: of the four steps, only the download is safely retryable on its own; a retry at any other step can create a new billable job.
    6. Expect the follow-up: if the vendor offers callbacks, do you still poll? Yes. Callbacks get lost to restarts, network blips and unreachable endpoints, so the standard is callback-first with a low-frequency sweep for tasks stuck without a terminal state.

    分析过程 · 先想清楚再作答

    1. 这题的区分度不在代码,在你能列出多少种失败。只答「加个 try catch 和重试」的人,通常没在生产上跑过这类接口。
    2. 先把任务的形状说清楚,失败点才有地方挂:提交拿标识、轮询查状态、取件换地址、下载落盘,四步是四类不同的失败。
    3. 然后逐步列:提交阶段有限流、鉴权、参数无效、内容审核;轮询阶段有查询接口自己限流、状态一直不前进、任务返回失败终态;取件阶段有标识存在但取不到地址;下载阶段有地址过期、下到一半断流、写盘失败。
    4. 接着说横跨全程的两类:超时与进程重启。超时的关键在于它不是失败而是「不知道成没成」,必须先按幂等键查一遍再决定要不要重提;进程重启意味着内存里的任务标识没了,所以标识必须先落盘再发请求,否则你会有一批花了钱却找不回来的任务。
    5. 最后给一句能体现工程判断的话:这四步里只有下载是可以无脑重试的,其余每一步的重试都可能产生一次新的计费。
    6. 可以预期的追问:厂商提供回调了还需要轮询吗?需要。回调会因为服务重启、网络抖动、地址不可达而丢失,生产上的标准做法是回调为主、低频轮询兜底扫描长时间没有终态的任务。

    Key points

    • Break failures down by the four steps: submit (rate limit, auth, invalid params, moderation), poll (query rate limit, stalled status, terminal failure), retrieve (no URL), download (expired link, cut stream, disk error)
    • A timeout means unknown, not failed: look up the idempotency key for an existing artifact before resubmitting, or you pay twice
    • Persist the task id promptly so in-flight tasks survive a process restart
    • Only the download is safely retryable on its own; retries at the other steps can create new billable jobs
    • Keep a low-frequency polling sweep even when callbacks exist, because callbacks get lost

    答题要点

    • 按四步拆失败:提交(限流、鉴权、参数无效、内容审核)、轮询(查询限流、状态停滞、终态失败)、取件(拿不到地址)、下载(地址过期、断流、写盘失败)
    • 超时不是失败而是状态未知,重试前必须先按幂等键查一遍已有产物,否则会为同一个任务付两次钱
    • 任务标识要及时落盘,进程重启后才能把在途任务认回来
    • 四步里只有下载可以无脑重试,其余每一步的重试都可能产生新的计费
    • 有回调也要保留低频兜底轮询,回调会丢
  • How do you choose a polling interval, and why is a fixed interval a bad default?轮询间隔怎么定?为什么不能一直用固定间隔死等?
    Common in ChinaCommon overseasBasic#async-task#backoff

    How to reason about it · think before answering

    1. This looks like a giveaway, but it has three layers and only the first one is obvious. They want to know whether you have actually written this loop.
    2. Layer one is cost: a task queued for five minutes polled every second is three hundred wasted requests. The query endpoint has its own rate limit, so you can throttle yourself and then misread 'rate limited' in the logs as a generation problem.
    3. Layer two is capping the backoff: multiply without a cap and you end up polling every few minutes, sleeping long after the task finished. Pick the cap from what extra wait a user tolerates — usually in the ten-to-twenty-second range.
    4. Layer three is where people actually get it wrong: the timeout check belongs before the sleep, and the test is whether sleeping would cross the deadline. Sleeping first means overshooting the budget by a full interval, which at a twenty-second backoff is twenty wasted seconds.
    5. Mention ordering too: check terminal states before the timeout. Discarding a task that just succeeded on the final poll means paying for an artifact you then throw away.
    6. Expect the follow-up: how do you pick the initial interval? From the typical duration of this class of task, a bit above a tenth of it; and the very first poll can be delayed slightly, since a just-submitted task is almost never done.

    分析过程 · 先想清楚再作答

    1. 这是一道送分题,但它有三个层次,只答出第一层拿不到高分。面试官想看的是你有没有真的写过这个循环。
    2. 第一层是成本:一个排队五分钟的任务,用一秒的固定间隔就是三百次无效请求。查询接口自己也有速率限制,你很可能自己把自己打到限流,然后在日志里看到「生成失败:限流」,还以为是生成接口的问题。
    3. 第二层是退避要封顶:只乘不封顶的话,退到后面已经是几分钟查一次,任务早就好了你还在睡。上限的选法是「用户能忍受的额外等待」,一般十几到二十秒。
    4. 第三层最容易写错,也是这题真正的区分点:超时判断必须放在睡觉之前,判据是「睡下去会不会越过截止时间」。先睡再判会让你在预算之外多睡整整一轮,退避到二十秒时就是白等二十秒。
    5. 另外提一条顺序:先判终态再判超时。任务恰好在最后一次查询里成功却被当成超时扔掉,等于付了钱还丢了产物。
    6. 可以预期的追问:起步间隔怎么定?按这类任务的典型耗时定,比典型耗时的十分之一略大即可;再往细说就是首次查询可以稍微延后一点,因为刚提交的任务几乎不可能立刻完成。

    Key points

    • A fixed interval is either too tight, wasting requests and throttling yourself, or too loose, adding dead time after completion
    • Use exponential backoff starting near a tenth of the task's typical duration
    • Cap the backoff, choosing the cap from the extra wait a user will tolerate
    • Check the timeout before sleeping, testing whether the sleep would cross the deadline, or you overshoot the budget by a full interval
    • Check terminal states before the timeout so a task that just succeeded is not discarded

    答题要点

    • 固定间隔要么太密造成大量无效请求并把自己打到限流,要么太疏让完成后的等待过长
    • 用指数退避:从接近典型耗时十分之一的间隔起步,每轮乘一个系数
    • 退避必须封顶,上限按用户能忍受的额外等待来定
    • 超时判断放在 sleep 之前,判据是「睡完会不会越过截止时间」,否则会在预算之外多睡一轮
    • 先判终态再判超时,避免把最后一次查询里刚成功的任务误杀
  • When a generation API returns a failure, how do you decide whether to retry, and what happens after the retries run out?生成类接口返回失败,你怎么判断该不该重试?重试几次之后该做什么?
    Common in ChinaCommon overseasDeep dive#error-handling#retry#cost

    How to reason about it · think before answering

    1. The hinge is 'decide'. Bucketing by the leading digit of the HTTP status is the classic wrong answer, because generation APIs often return HTTP 200 with a business error code in the body.
    2. Give a reusable test instead of reciting a code table: ask three questions — will waiting help, will changing the input help, or does a human have to step in? They map onto three dispositions: back off and retry, fix the request, alert immediately.
    3. Concretely: rate limits and server errors are the first bucket and the program handles them; invalid parameters and content moderation are the second, where retrying repeats the same error and burns rate-limit budget that genuinely retryable tasks needed; auth failure and insufficient balance are the third, where retrying only delays the alert.
    4. Handle timeout separately — this is the line that signals experience. A timeout is unknown, not failed: the job may still be running, or may have finished. So never resubmit blindly; look up the idempotency key for an existing artifact first.
    5. When retries are exhausted, do three things: mark the item failed with the last error code and the exact request parameters, keep processing the rest of the batch instead of aborting it, and aggregate the failures into one readable alert rather than one per item.
    6. Expect the follow-up: how many retries? Scale it by unit price. The more expensive the call, the fewer automatic retries, and expensive failures should go to a human for review before being redone.

    分析过程 · 先想清楚再作答

    1. 这题的题眼是「判断」。按状态码首位数字一刀切是最常见的错误答案,因为生成类接口的业务错误码往往和 HTTP 状态码不在一个层面上——很多厂商的失败是 HTTP 200 加一个响应体里的业务码。
    2. 给一条可复用的判据,比背错误码表有用:问三个问题——等一等会不会好、改输入会不会好、还是必须叫人来。三个问题对应三种处置:退避重试、修请求、立刻告警。
    3. 落到具体:限流和服务端故障属于第一类,程序自己扛;参数无效与内容审核属于第二类,重试一万次都是同一个错,而且会挤占限流额度让真正该重试的排不上号;鉴权失败与余额不足属于第三类,重试只会延迟告警。
    4. 然后单独处理超时,这是最能体现经验的一条:超时不是失败,是状态未知,对方队列里那个任务可能还在跑甚至已经成了。所以超时之后不能直接重提,要先按幂等键查一遍已有产物。
    5. 重试用尽之后要做三件事,缺一不可:把这一条标成失败并记下最后一次的错误码与请求参数、继续跑批次里剩下的任务不要中断、把失败清单汇总成一次可读的告警而不是每条发一次。
    6. 可以预期的追问:重试次数怎么定?按单价定。单价越高,允许的重试次数越少,而且高单价的失败更应该先送人复核再决定要不要重做。

    Key points

    • Do not bucket by the leading HTTP digit; generation APIs often hide the business error code inside an HTTP 200 body
    • Use three questions — will waiting help, will changing the input help, or is a human required — mapping to back off, fix the request, alert
    • Rate limits and server errors are retryable; invalid parameters and moderation blocks are not and waste rate-limit budget; auth and balance failures need an alert
    • A timeout is unknown rather than failed: check the idempotency key for an existing artifact before resubmitting, or you pay twice
    • When retries run out, mark the item failed with its error code and request parameters, keep the batch running, and aggregate failures into one alert; scale retry counts by unit price

    答题要点

    • 不要按状态码首位一刀切,生成类接口的业务错误码常常藏在 HTTP 200 的响应体里
    • 判据是三个问题:等一等会不会好、改输入会不会好、还是必须叫人来,分别对应退避重试、修请求、立刻告警
    • 限流与服务端故障可重试;参数无效与内容审核重试无用且会挤占限流额度;鉴权失败与余额不足必须告警
    • 超时是状态未知不是失败,重试前先按幂等键查一遍已有产物,否则会重复计费
    • 重试用尽后:标记失败并留下错误码与请求参数、不中断整批、把失败汇总成一次可读告警;重试次数按单价定

Comments