sterilize: search-then-create + retry-on-UNIQUE-400 + don't mark errored as applied

Job 1's bulk run apply'd 184 recipes and 182 of them failed with the
same error: POST /api/foods -> 400 UNIQUE constraint failed:
ingredient_foods.name, ingredient_foods.group_id. Cause: Mealie's
name_normalized strips punctuation/whitespace/case more aggressively
than our local _build_name_index's plain .lower(), so the cache misses,
the create_food fires blindly, and Mealie's UNIQUE constraint kills
the call. Whole-recipe apply was wrapped in try/except at the bulk
runner so the recipe got marked errored — but applied_at was still
set to NOW(), making the rerun think we'd already tried. We had, but
the recipe's still unparsed.

Two fixes:

1. sterilizer._resolve_food / _resolve_unit replace the inline
   create-on-miss block. Order: local cache → Mealie search-endpoint
   tie-break → create. On any UNIQUE-flavored 400 from create, fall
   back to one more search to adopt whatever Mealie has under the
   normalized form. Mealie's search endpoint applies its own
   name_normalized internally so we don't have to mirror its rules.
   _search_for_match takes "foods" or "units" and looks for an exact
   case-insensitive match against name or pluralName, with a fallback
   to "trust Mealie's ranker" when there's exactly one hit.

2. db.mark_proposal_applied no longer sets applied_at on error. On
   success: applied_at=NOW(), apply_error=NULL. On error: applied_at
   stays NULL, apply_error gets the message. list_approved_unapplied_
   proposals keys off applied_at IS NULL, so a rerun naturally retries
   only the failed recipes.

Net effect: rerun can now successfully apply the 182 failed recipes
without re-walking them, and won't waste calls on the 2 that did go
through.
This commit is contained in:
Kayos 2026-04-30 06:05:19 -07:00
parent 9368b64a81
commit f7b30d3b65
2 changed files with 136 additions and 19 deletions

View file

@ -1069,13 +1069,26 @@ class DB:
def mark_proposal_applied(
self, job_id: int, recipe_slug: str, *, error: str | None = None
) -> None:
with self.conn() as c, c.cursor() as cur:
cur.execute(
"""UPDATE cauldron_sterilize_proposals
SET applied_at=NOW(), apply_error=%s
WHERE job_id=%s AND recipe_slug=%s""",
((error or "")[:500] or None, job_id, recipe_slug),
)
"""On success: applied_at=NOW(), apply_error=NULL. On error: leave
applied_at NULL so a rerun can retry, but record the error for
review. The list_approved_unapplied_proposals query keys off
applied_at IS NULL, so this directly drives retryability."""
if error:
with self.conn() as c, c.cursor() as cur:
cur.execute(
"""UPDATE cauldron_sterilize_proposals
SET apply_error=%s
WHERE job_id=%s AND recipe_slug=%s""",
(error[:500], job_id, recipe_slug),
)
else:
with self.conn() as c, c.cursor() as cur:
cur.execute(
"""UPDATE cauldron_sterilize_proposals
SET applied_at=NOW(), apply_error=NULL
WHERE job_id=%s AND recipe_slug=%s""",
(job_id, recipe_slug),
)
def list_approved_unapplied_proposals(self, job_id: int) -> list[dict]:
with self.conn() as c, c.cursor() as cur: