[object Object]

← back to Designer Wallcoverings

enrich-full.py: treat mid-run Henry unmount as transient (VolumeUnavailable -> retry/defer) instead of burning SKUs as permanent image_not_found stubs (a memory-pressure I/O stall burned 1992 this way)

dcd19e579e2c11435278f44ec2142a83cfc344b4 · 2026-07-01 14:14:47 -0700 · Steve

Files touched

Diff

commit dcd19e579e2c11435278f44ec2142a83cfc344b4
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 1 14:14:47 2026 -0700

    enrich-full.py: treat mid-run Henry unmount as transient (VolumeUnavailable -> retry/defer) instead of burning SKUs as permanent image_not_found stubs (a memory-pressure I/O stall burned 1992 this way)
---
 onboarding/sangetsu-lilycolor/scripts/enrich-full.py | 19 ++++++++++++++++---
 1 file changed, 16 insertions(+), 3 deletions(-)

diff --git a/onboarding/sangetsu-lilycolor/scripts/enrich-full.py b/onboarding/sangetsu-lilycolor/scripts/enrich-full.py
index 785b5886..bf20bc6c 100644
--- a/onboarding/sangetsu-lilycolor/scripts/enrich-full.py
+++ b/onboarding/sangetsu-lilycolor/scripts/enrich-full.py
@@ -106,8 +106,19 @@ def clean(arr, cap=4):
     return out
 
 
+class VolumeUnavailable(Exception):
+    """The image's source volume (e.g. Henry) is not mounted — an INFRASTRUCTURE
+    outage, not a missing file. Routed through the transient retry/defer path so a
+    mid-run drive drop never burns a SKU as a permanent image_not_found stub."""
+
+
 def enrich_one(sku, img):
     if not os.path.exists(img):
+        # If the containing directory is gone, the whole volume dropped (Henry
+        # unmounted) → transient, defer. If the dir is present but the file is
+        # absent, it's a genuinely missing image → permanent stub.
+        if not os.path.isdir(os.path.dirname(img)):
+            raise VolumeUnavailable(f"volume for {img} unavailable")
         return {"sku": sku, "error": "image_not_found"}
     t0 = time.time()
     samp = palette(img, 6)
@@ -258,8 +269,10 @@ def main():
                     fout.write(json.dumps(row, ensure_ascii=False) + "\n")
                     fout.flush()
                     break
-                except (urllib.error.URLError, TimeoutError, ConnectionError) as e:
-                    # TRANSIENT ollama outage (Errno 61 Connection refused / timeout).
+                except (urllib.error.URLError, TimeoutError, ConnectionError,
+                        VolumeUnavailable) as e:
+                    # TRANSIENT outage — ollama down (Errno 61 / timeout) OR the
+                    # image volume (Henry) unmounted mid-run.
                     # NEVER write a stub: a stub row carries a "sku" key, which load_done
                     # would treat as done and skip forever. Wait for ollama to return and
                     # retry the SAME SKU with capped backoff; if it stays down past the
@@ -271,7 +284,7 @@ def main():
                               f"(ollama still down) — left for a later sweep", flush=True)
                         break
                     wait = min(60, 5 * attempt)
-                    print(f"[{i}/{len(todo)}] {sku} ollama down ({str(e)[:60]}) — "
+                    print(f"[{i}/{len(todo)}] {sku} transient outage ({str(e)[:60]}) — "
                           f"retry {attempt}/{MAX_TRANSIENT_RETRIES} in {wait}s", flush=True)
                     time.sleep(wait)
                     continue

← db33be40 enrich-full.py: retry-with-backoff on transient ollama outag  ·  back to Designer Wallcoverings  ·  recover: enrich Lily swatches from local copy (Henry USB unm 324313fd →