{"record":{"id":"f184260172f079c9","repo":"abhigyanpatwari/GitNexus","slug":"analysis-already-in-progress-job-job-id-f18426","errorCode":null,"errorMessage":"Analysis already in progress (job ${job.id})","messagePattern":"Analysis already in progress \\(job (.+?)\\)","errorType":"http","errorClass":null,"httpStatus":409,"severity":"warning","filePath":"gitnexus/src/server/api.ts","lineNumber":1609,"sourceCode":"\n            if (!targetPath) {\n              throw new Error('No target path resolved');\n            }\n\n            launchAnalysisWorker(job, targetPath, { force, embeddings, dropEmbeddings });\n          } catch (err: any) {\n            if (targetPath) releaseRepoLock(getStoragePath(targetPath));\n            jobManager.updateJob(job.id, {\n              status: 'failed',\n              error: err.message || 'Analysis failed',\n            });\n          }\n        })();\n\n        res.status(202).json({ jobId: job.id, status: job.status });\n      } catch (err: any) {\n        if (err.message?.includes('already in progress')) {\n          res.status(409).json({ error: err.message });\n        } else {\n          res.status(500).json({ error: err.message || 'Failed to start analysis' });\n        }\n      }\n    },\n  );\n\n  // POST /api/analyze/upload — analyze a browser folder upload.\n  // Securely ingests the multipart upload into a sandbox, promotes it to a\n  // persistent dir, and analyzes it via the shared job/worker machinery.\n  // localhost-only (no cross-origin write reach) + conservative rate limit.\n  app.post(\n    '/api/analyze/upload',\n    createRouteLimiter({ limit: 5 }),\n    requireTrustedOrigin,\n    createAnalyzeUploadHandler({\n      createJob: (params) => jobManager.createJob(params),\n      launch: (job, targetPath, opts) => launchAnalysisWorker(job, targetPath, opts),","sourceCodeStart":1591,"sourceCodeEnd":1627,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/aac7515d2a8c50a1f8f923c6fb77218b333560d6/gitnexus/src/server/api.ts#L1591-L1627","documentation":"HTTP 409 returned by POST /api/analyze when JobManager.createJob throws `Analysis already in progress (job <uuid>)` (analyze-job.ts:114) — its single-slot rule: the manager runs one active job at a time, and a non-terminal job for a DIFFERENT repository already exists (queued/cloning/analyzing). The route maps any message containing 'already in progress' to 409. The same-repo case is not an error: createJob dedupes it and the route answers 202 with the existing jobId (plus a tokenIgnored note). /api/analyze/upload shares the same manager, so an upload analysis also occupies the slot.","triggerScenarios":"POST /api/analyze for repo B while repo A's analysis (or upload analysis) is still running; a retry double-click racing a slow clone; batch scripts iterating repos without waiting for terminal status; UIs firing a second POST before polling the first.","commonSituations":"Treating the 202 as completion and re-POSTing; parallel CI jobs against one serve instance; dashboards queuing multiple repos simultaneously; very large clones holding the single slot for a long time.","solutions":["Adopt the running job: parse its id from the 409 message (`job <uuid>`) and poll GET /api/analyze/:jobId (or its /progress SSE stream) instead of creating a new one","Serialize client-side — the manager allows exactly one active analysis, so queue repos and start the next only after the previous reaches complete/failed","Re-posting the SAME repo is safe: you get a 202 dedup with the existing id, not a 409","If the slot seems stuck, poll the active job — a hung worker eventually times out (30-minute job timeout) and frees the slot"],"exampleFix":"// before\nconst res = await fetch('/api/analyze', { method: 'POST' /* ... */ });\nif (!res.ok) throw new Error(await res.text()); // 409 crashes the flow\n\n// after\nconst res = await fetch('/api/analyze', { method: 'POST' /* ... */ });\nif (res.status === 409) {\n  const { error } = await res.json();\n  const existingId = error.match(/job ([0-9a-f-]{36})/)?.[1];\n  if (existingId) await pollUntilTerminal(`/api/analyze/${existingId}`); // wait it out\n} else if (!res.ok) throw new Error((await res.json()).error);","handlingStrategy":"fallback","validationCode":"// Single-flight: concurrent callers reuse one POST instead of colliding\nlet inflight: Promise<Response> | null = null;\nasync function postAnalyze(body: object): Promise<Response> {\n  if (inflight) return inflight;\n  const p = fetch('/api/analyze', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify(body),\n  });\n  inflight = p;\n  try { return await p; } finally { if (inflight === p) inflight = null; }\n}","typeGuard":null,"tryCatchPattern":"Catch HTTP 409 specifically, extract the active job id from `job <uuid>` in the message, and fall back to polling that job (GET /api/analyze/:jobId or the SSE stream). Do not retry the POST unchanged while the other job is active — it will keep conflicting.","preventionTips":["Treat 202 as 'started', never 'done' — poll to complete/failed before starting the next repo","Serialize multi-repo batches; the JobManager is single-slot by design","Reuse the same-repo 202 dedup instead of re-POSTing","Disable submit actions in the UI while a job is active"],"tags":["http-409","concurrency","job-scheduler","single-slot","api"],"backgroundTag":"job-already-in-progress","analyzedSha":"aac7515d2a8c50a1f8f923c6fb77218b333560d6","analyzedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-08-22T20:17:22.307Z"}