{"record":{"id":"b8cfe822dd67edae","repo":"koala73/worldmonitor","slug":"timeout-scenario-worker-may-be-down","errorCode":null,"errorMessage":"Timeout — scenario worker may be down","messagePattern":"Timeout — scenario worker may be down","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/components/SupplyChainPanel.ts","lineNumber":1182,"sourceCode":"      let result: ScenarioResult | null = null;\n      // 60 × 1s = 60s max (worker typically completes in <1s). 1s poll keeps\n      // the perceived latency <2s in the common case. First iteration polls\n      // immediately (no sleep) in case the worker was already running on a\n      // previous job and blocked here only because of network round-trip.\n      for (let i = 0; i < 60; i++) {\n        if (signal.aborted) { resetButton('idle'); return; }\n        if (!this.content.isConnected) return; // panel gone — nothing to update\n        if (i > 0) await new Promise(r => setTimeout(r, 1000));\n        const status = await getScenarioStatus(jobId, { signal });\n        if (status.status === 'done') {\n          const r = status.result;\n          if (!r || !Array.isArray(r.topImpactCountries)) throw new Error('done without valid result');\n          result = r;\n          break;\n        }\n        if (status.status === 'failed') throw new Error('Scenario failed');\n      }\n      if (!result) throw new Error('Timeout — scenario worker may be down');\n      if (signal.aborted) { resetButton('idle'); return; }\n      if (!this.content.isConnected) return;\n      // After this callback fires, showScenarioSummary() → render() will rebuild\n      // the scenario-trigger DOM with the button already in its \"Active\" +\n      // disabled state (driven by activeScenarioState in renderChokepoints()).\n      // Do NOT touch the captured btn reference here — it's about to be detached\n      // by render()'s setContent(), and any imperative update would no-op\n      // silently while the fresh button shows the wrong state.\n      this.scenarioRunState.delete(scenarioId);\n      this.onScenarioActivate?.(scenarioId, result);\n    } catch (err) {\n      // Abort from a new click = user-triggered retry, no error banner needed.\n      if (err instanceof Error && err.name === 'AbortError') {\n        resetButton('idle');\n        return;\n      }\n      console.error('[scenario] run failed:', err);\n      resetButton('error');","sourceCodeStart":1164,"sourceCodeEnd":1200,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/src/components/SupplyChainPanel.ts#L1164-L1200","documentation":"After the polling loop exhausts without receiving a 'done' (or 'failed') status, result is still unset and the code throws this error. It indicates the scenario worker neither completed nor explicitly failed within the polling window, so it may be down, overloaded, or the job was lost.","triggerScenarios":"Polling loop ends with result === null — every poll returned a non-terminal status (queued/running) or the loop ran out of iterations before 'done'/'failed' ever arrived.","commonSituations":"Scenario worker process down or not deployed; worker queue backed up under load making the job slower than the poll budget; network partition between client and status endpoint; very large scenario exceeding the built-in timeout.","solutions":["Check that the scenario worker is running and healthy (its health endpoint / deployment status).","Retry the scenario; transient queue congestion often resolves on a second attempt.","Reduce scenario size/complexity so it completes within the polling window.","If the job is stuck, inspect worker logs/queue for the job id and restart the worker if necessary."],"exampleFix":"// before\nif (!result) throw new Error('Timeout — scenario worker may be down');\n// after\nif (!result) {\n  const healthy = await fetch(workerHealthUrl).then(r => r.ok).catch(() => false);\n  throw new Error(healthy ? 'Scenario timed out; try a smaller scenario' : 'Scenario worker unreachable');\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  const result = await runScenario(params);\n} catch (e) {\n  if (e.message.includes('Timeout')) {\n    showBanner('Scenario worker slow or down — retrying…');\n    await retryWithBackoff(() => runScenario(params), { attempts: 2 });\n  } else throw e;\n}","preventionTips":["Health-check the scenario worker before enqueuing large scenarios.","Use smaller scenarios that fit the polling budget.","Extend the poll window for known-large scenarios instead of failing fast."],"tags":["timeout","worker","polling","availability"],"backgroundTag":"request-timeout","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}