[object Object]

← back to Nationalrealestate

discover: fail-soft run recording (try/catch closeRun) so search-anti-bot errors don't leave zombie 'running' rows that hog the consumer slot

03215463059678213689c4eab65b2b3d83566753 · 2026-07-30 19:03:52 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit 03215463059678213689c4eab65b2b3d83566753
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Thu Jul 30 19:03:52 2026 -0700

    discover: fail-soft run recording (try/catch closeRun) so search-anti-bot errors don't leave zombie 'running' rows that hog the consumer slot
---
 src/enrich/firm_website_discovery.ts | 19 ++++++++++++++++---
 1 file changed, 16 insertions(+), 3 deletions(-)

diff --git a/src/enrich/firm_website_discovery.ts b/src/enrich/firm_website_discovery.ts
index 26e2b2c..c71e25c 100644
--- a/src/enrich/firm_website_discovery.ts
+++ b/src/enrich/firm_website_discovery.ts
@@ -157,6 +157,8 @@ async function main() {
   console.log(`[discover] run #${runId} · ${queue.length} firms · jittered 2.5-4s/query`);
 
   let ok = 0, miss = 0, err = 0, consecThrottle = 0;
+  let fatal: any = null;
+  try {
   for (let i = 0; i < queue.length; i++) {
     const f = queue[i];
     const q = `${f.name} ${f.hq_city || ''} ${f.license_state} real estate`.replace(/\s+/g, ' ').trim();
@@ -209,12 +211,23 @@ async function main() {
     await new Promise(res => setTimeout(res, 2500 + Math.random() * 1500));
   }
 
+  } catch (e: any) {
+    // Fail-soft: an uncaught search/transport error must NOT skip closeRun (which
+    // would leave a zombie 'running' row that never refreshes freshness and makes
+    // this consumer job perpetually stalest → hogs its reserved loop slot).
+    fatal = e;
+    console.error(`[discover] loop aborted, recording partial: ${String(e?.message || e).slice(0, 120)}`);
+  }
+
+  // Always close the run. 'ok' if it processed anything (partial anti-bot failures
+  // are normal for HTML search scraping); 'failed' only on a total pre-loop wipeout.
+  const status = (ok || miss || err) ? 'ok' : 'failed';
   await query(
-    `UPDATE ingest_runs SET finished_at = NOW(), status = 'ok', rows_upserted = $2, rows_skipped = $3,
+    `UPDATE ingest_runs SET finished_at = NOW(), status = $5, rows_upserted = $2, rows_skipped = $3,
             notes = notes || $4 WHERE id = $1`,
-    [runId, ok, miss, ` · ${ok} found, ${miss} no-result, ${err} search-errors`],
+    [runId, ok, miss, ` · ${ok} found, ${miss} no-result, ${err} search-errors${fatal ? ` · partial(${String(fatal?.message || fatal).slice(0, 40)})` : ''}`, status],
   );
-  console.log(`[discover] done · ${ok} found · ${miss} no-result · ${err} errors`);
+  console.log(`[discover] done · ${ok} found · ${miss} no-result · ${err} errors${fatal ? ' (partial)' : ''}`);
   await pool.end();
 }
 

← 908d603 Loop: reserve >=1 slot/tick for fast-consumer streams (homes  ·  back to Nationalrealestate  ·  discover: mark run 'failed' when every firm errored (ok iff b569855 →