[object Object]

← back to Dw Five Field Step0

fivefield drain: clean 429 back-off on Shopify daily variant limit

8339519f49dcabd0ddb079d6ac07ec8d466d07c6 · 2026-06-23 10:07:17 -0700 · Steve Studio

On the 'Daily variant creation limit' 429, raise a BaseException sentinel that the
main loop catches → stop the run immediately + refund the unused backlog grant,
instead of grinding the whole granted slice into thousands of failed 429s (11,754
in one day, created ~0, monopolized the shared cap). Cap resets in ~24h; resumable.

Files touched

Diff

commit 8339519f49dcabd0ddb079d6ac07ec8d466d07c6
Author: Steve Studio <steve@designerwallcoverings.com>
Date:   Tue Jun 23 10:07:17 2026 -0700

    fivefield drain: clean 429 back-off on Shopify daily variant limit
    
    On the 'Daily variant creation limit' 429, raise a BaseException sentinel that the
    main loop catches → stop the run immediately + refund the unused backlog grant,
    instead of grinding the whole granted slice into thousands of failed 429s (11,754
    in one day, created ~0, monopolized the shared cap). Cap resets in ~24h; resumable.
---
 bulk-fivefield-exec.py | 29 +++++++++++++++++++++++++++--
 1 file changed, 27 insertions(+), 2 deletions(-)

diff --git a/bulk-fivefield-exec.py b/bulk-fivefield-exec.py
index a52f574..2fb47f3 100644
--- a/bulk-fivefield-exec.py
+++ b/bulk-fivefield-exec.py
@@ -33,6 +33,13 @@ DOES NOT mutate the worklist table. Audit JSON is the resume ledger.
 """
 import json, urllib.request, urllib.error, subprocess, time, os, sys, argparse
 
+
+# Sentinel for Shopify's DAILY variant-creation 429. Inherits BaseException (NOT Exception) so the
+# per-SKU `except Exception` inside process() can't swallow it — it propagates straight to main()'s
+# handler, which stops the run cleanly + refunds. (2026-06-23 DTD-C 429-back-off.)
+class DailyVariantLimit(BaseException):
+    pass
+
 DOMAIN = "designer-laboratory-sandbox.myshopify.com"
 API = "2024-10"
 OUTDIR = "/Users/stevestudio2/Projects/dw-five-field-step0/out"
@@ -231,6 +238,16 @@ def process(item, results, dry_run=False):
         # --- create the missing variant ---
         st2, r2 = shop("POST", f"products/{pid}/variants.json", vbody)
         if st2 not in (200, 201):
+            # 2026-06-23 (DTD-C / Steve): on Shopify's DAILY variant-creation 429, STOP the whole
+            # run cleanly instead of grinding the rest of the granted slice into thousands of failed
+            # 429s (11,754 in one day → it monopolized the cap creating ~0). The cap resets in ~24h;
+            # the unused grant is refunded by main()'s refund-rule path. Raise a sentinel the loop
+            # catches → records this SKU as errored:daily_limit, refunds, exits.
+            body_txt = json.dumps(r2)
+            if st2 == 429 and "Daily variant creation limit" in body_txt:
+                rec["status"] = "errored"; rec["reason"] = "daily_variant_limit_429_backoff"; rec["api"] = r2
+                results.append(rec); print(f"BACKOFF {dw}: Daily variant creation limit 429 — stopping run cleanly (resets ~24h)")
+                raise DailyVariantLimit(dw)
             rec["status"] = "errored"; rec["reason"] = f"create_{st2}"; rec["api"] = r2
             results.append(rec); print(f"ERR {dw}: create {st2} {r2}"); return 0
         nv = r2.get("variant", {})
@@ -316,7 +333,15 @@ def main():
             break
         if args.dry_run and processed >= granted:
             break
-        made = process(w, results, dry_run=args.dry_run)
+        try:
+            made = process(w, results, dry_run=args.dry_run)
+        except DailyVariantLimit:
+            # Shopify's daily variant cap is hit — stop NOW (no point grinding the rest of the
+            # grant into 429s). The refund path below returns the whole unused grant to budget.cjs.
+            json.dump(results, open(RESULT, "w"), indent=1)
+            print(f"\n[backoff] Daily variant creation limit reached — stopping run cleanly after "
+                  f"{created} create(s) this run. Resumable next budget window (~24h reset).")
+            break
         created += made
         processed += 1
         if not args.dry_run:
@@ -333,7 +358,7 @@ def main():
         unused = granted - created
         if unused > 0:
             got = budget_refund(unused)
-            print(f"[budget] refund: granted={granted} used={created} -> refunded {got} upload variants.")
+            print(f"[budget] refund: granted={granted} used={created} -> refunded {got} {BUDGET_CAT} variants.")
 
     # --- summary ---
     fixed = sum(1 for r in results if r["status"] == "fixed")

← 750a3a7 auto-save: 2026-06-23T09:20:41 (1 files) — out/bulk-fivefiel  ·  back to Dw Five Field Step0  ·  auto-save: 2026-06-23T10:21:01 (2 files) — out/bulk-fivefiel 35f153f →