Skip to content

Build1 publisher3 min readPublished

A five-minute lease column handles both retry backoff and crash recovery

A traveller published 178 trip reports at once. The queue in front of the model got through them on ids alone at ten jobs a minute, with one five-minute lease column covering both retries and dead workers.

The Engineer · Build desk

What happened

  • A traveller published 178 trip reports in one afternoon, imported from a Polarsteps diary of two years from Canada to Chile, onto a site that had 9 reports before that.
  • Publishing flips a status, and a trigger puts only job ids on a queue for text moderation, place extraction and one image moderation per photo, so the request returns in milliseconds.
  • A worker claims the ten oldest jobs once a minute under a five-minute lease, which expires and makes the job claimable again if the worker dies mid-call.
  • Six minutes after the publish, 55 jobs were done and 321 were pending across the three kinds, with 0 of the 18 image jobs finished.
  • The next morning about a hundred photos sat marked needs review, and 98 of them gave the reason "Image moderation service unavailable".

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • cost The bill for the rate-limited import lands on a human reviewer: 98 photos need eyes on them because the model never returned a judgement, and only two of the hundred flagged rows record an actual model decision.
  • decision A retry-count change does not recover those photos, because the trigger only enqueues rows still marked pending. Someone has to re-queue by reason string, or split transport failure out of the verdict column.
  • precedent Anyone alerting on the worker's HTTP status would page on a healthy pipeline every minute, since the cron's five-second timeout is designed; the signal that settled it was the cron's own run log.

The lease is one column with two behaviours. Claiming a job stamps it five minutes into the future [6]. A worker that dies mid-call stops writing, the stamp passes, and the next tick sees the row as claimable again [6]. There is no reaper process and no dead-letter topic to drain. After three failed attempts the job is marked failed and the item it was moderating goes to a human [7].

The same column explains a row that looks broken. Four jobs sat at one attempt and still pending; those were the batch in flight under a live lease [13].

Ten a minute is a budget number. The three job kinds together stay under the model's per-minute allowance on the author's tier, and because every kind draws from the same allowance the worker takes the oldest ten and ignores what they are [8]. That pacing transfers to another pipeline only if the model budget and the job mix match; on a tier with a larger per-minute allowance, the same code would drain the same queue faster [8].

Six minutes in, the done counts were 28 places, 27 content and 0 images, which is 55 jobs against a ceiling of 60 [9][1]. Pending was 151 plus 152 plus 18, or 321 jobs, about 32 more minutes at ten a minute [2][3]. "The worker was doing exactly what it was built to do, and it was going to take another half hour," the author wrote [10]. The two report-level kinds each show 179 rows for a burst of 178 reports, and the post does not account for the extra one [4].

The image count is where the older bug shows. Only 18 image jobs were queued out of a two-year photo set, because the trigger enqueues only photos still marked pending, and roughly a hundred of this traveller's photos already carried needs review [5].

Ninety-eight of those rows gave the same reason: Image moderation service unavailable [17]. One code path writes that string, the image moderation function, on the final of three failed attempts, so the model never returned a judgement on them [18]. Two rows carried the model's own words, "a child as the main subject" and "an unclear private space" [19].

The failures predate the queue. At import time, every photo insert called the model directly [21]. The import wrote hundreds of photos in minutes and the model rate-limited the burst; three attempts, three refusals, and each photo was parked as needs review [21]. The job rows were already gone, since finished jobs are purged after seven days [20]. Weeks later the publish trigger went looking for pending photos and skipped them [22]. "A pipeline failure had been stored in the same field as a verdict, and from then on it was treated as one," the author wrote [23].

The HTTP log showed the call to the worker timing out after five seconds, every minute, because the cron does not wait for an answer the worker takes up to sixty seconds to produce [14]. The author checked the cron's own run log instead: one run a minute, every minute, all succeeded [15].

The queue protects the model's budget and the request path. It does nothing for the next author in line. A report published during the burst waits behind it, which the author calls fine for a one-off import and not fine for a live site; the fix he names is a second worker, or a share of each tick reserved per job kind [11]. "I will build it the day the queue shows me it is needed, not before," he wrote [12].

What to watch

  • Whether the second worker or the per-kind share of each tick gets built the next time a burst puts another author behind 321 jobs.
  • Whether the 98 photos are re-queued by matching their reason string, since the trigger's pending-only selector will never pick them up.
  • Whether the seven-day purge of finished jobs is extended, given that it is why the import-time failures could not be dated from job rows.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories