fix(subs): retry a capped language next day
ci / web (push) Successful in 1m49s
e2e / e2e (push) Failing after 2m58s
ci / rust (push) Successful in 3m1s
ci / image (push) Successful in 3m47s

A `capped` row was excluded from the work list outright, and nothing
ever moved it back, so a language that hit a daily allowance was never
attempted again. §15 says budgets drain a backlog over days, which only
holds if the cap expires.

`pending` now returns a capped row once the UTC day it was capped in has
passed — the same boundary `subtitle_budget_spend.day` uses. The day
rule is in the query rather than the caller because `limit` is a window
over the whole list: a hundred newest files capped this morning would
otherwise hold every slot and hide the older gaps until tomorrow.

Capping no longer bumps `attempts`. That counter is the failure ladder,
and a budget that refused to spend never asked a provider.

Two reporting fixes alongside, both found chasing the same bug:

- The subtitle queue dropped `last_failure` on a capped row, so all four
  limits that write `capped` rendered identically. The sentence is the
  only thing that tells a provider budget from a 429.
- A translation engine with no API key reported itself as "not compiled
  into this binary", sending the operator to rebuild an image that was
  already correct.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Miguel Palhas
2026-09-20 18:43:08 +01:00
co-authored by Claude
parent dab3c06dd7
commit 70ba6ac376
7 changed files with 255 additions and 66 deletions
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO subtitle_attempts\n (media_file_id, language, state, attempts, last_attempt_at, last_failure)\n VALUES (?, ?, 'capped', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), ?)\n ON CONFLICT (media_file_id, language) DO UPDATE\n SET state = 'capped',\n attempts = MAX(subtitle_attempts.attempts, 1),\n last_attempt_at = excluded.last_attempt_at,\n last_failure = excluded.last_failure,\n updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "1ec1a7cbb47b92e765abc3b3524824cef5ec767b42c0976c82ef7ffdc99cdf72"
}
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "SELECT a.media_file_id AS \"media_file_id!: i64\",\n a.language AS \"language!: String\",\n a.state AS \"state!: SubtitleState\",\n a.attempts AS \"attempts!: i64\",\n a.last_attempt_at,\n a.last_failure,\n f.path AS \"media_file_path!: String\"\n FROM subtitle_attempts a\n JOIN media_files f ON f.id = a.media_file_id\n WHERE a.state IN ('wanted', 'failed')\n ORDER BY f.created_at DESC, a.last_attempt_at, a.language\n LIMIT ?",
"query": "SELECT a.media_file_id AS \"media_file_id!: i64\",\n a.language AS \"language!: String\",\n a.state AS \"state!: SubtitleState\",\n a.attempts AS \"attempts!: i64\",\n a.last_attempt_at,\n a.last_failure,\n f.path AS \"media_file_path!: String\"\n FROM subtitle_attempts a\n JOIN media_files f ON f.id = a.media_file_id\n WHERE (a.state IN ('wanted', 'failed')\n OR (a.state = 'capped'\n AND (a.last_attempt_at IS NULL\n OR a.last_attempt_at\n < strftime('%Y-%m-%dT00:00:00.000Z', 'now'))))\n ORDER BY f.created_at DESC, a.last_attempt_at, a.language\n LIMIT ?",
"describe": {
"columns": [
{
@@ -94,5 +94,5 @@
false
]
},
"hash": "47ad9162ba373dfbdf061898f5f148e65de261b494453804db912a919a78c2c5"
"hash": "ba91d1e5cb04d728f2f3d2a18333897cac5bbc66e22d678a7a008e7c0efba380"
}