{"record":{"id":"944ed3c61e961212","repo":"abhigyanpatwari/GitNexus","slug":"analysis-already-in-progress-job-job-id-944ed3","errorCode":null,"errorMessage":"Analysis already in progress (job ${job.id})","messagePattern":"Analysis already in progress \\(job (.+?)\\)","errorType":"http","errorClass":"BadRequestError","httpStatus":409,"severity":"error","filePath":"gitnexus/src/server/analyze-upload.ts","lineNumber":111,"sourceCode":"        innerIsDir = false;\n      }\n      if (!innerIsDir) {\n        throw new BadRequestError('Upload must be a folder');\n      }\n\n      const finalName = await pickAvailableName(baseName);\n      const finalDir = getUploadDir(finalName);\n\n      // createJob occupies the single analysis slot (throws 'already in\n      // progress' → 409). From here on, ANY error before launch MUST release\n      // the slot via failJob in the catch, or the server wedges all analyses.\n      let job: UploadJobRef;\n      try {\n        job = deps.createJob({ repoPath: finalDir });\n      } catch (err) {\n        const msg = err instanceof Error ? err.message : '';\n        if (msg.includes('already in progress')) {\n          throw new BadRequestError(msg, 409);\n        }\n        throw err;\n      }\n      createdJobId = job.id;\n\n      // Promote staging → persistent upload dir. Both live under UPLOAD_ROOT's\n      // filesystem, so this rename stays atomic (no EXDEV).\n      await fsp.mkdir(UPLOAD_ROOT, { recursive: true });\n      await fsp.rename(innerRoot, finalDir);\n      promotedDir = finalDir;\n      const oldStage = stageRoot;\n      stageRoot = undefined;\n      await fsp.rm(oldStage, { recursive: true, force: true }).catch(() => {});\n\n      // Drop any crafted index the upload may have carried (a `.gitnexus`\n      // segment passes containment); the worker will build a fresh one.\n      await fsp\n        .rm(path.join(finalDir, '.gitnexus'), { recursive: true, force: true })","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/aac7515d2a8c50a1f8f923c6fb77218b333560d6/gitnexus/src/server/analyze-upload.ts#L93-L129","documentation":"Inside POST /api/analyze/upload, deps.createJob (the same single-slot JobManager behind error 244) threw 'Analysis already in progress (job ...)' because a non-terminal job for a different target holds the slot. The upload handler catches it and rethrows BadRequestError(msg, 409) so HTTP callers see a correct conflict status rather than a generic 500.","triggerScenarios":"POST /api/analyze/upload while any other analysis — git-URL clone, local-path analyze, or a previous upload — is in a non-terminal status.","commonSituations":"Two browser tabs uploading folders concurrently; an upload fired while a long clone is running; UI not disabling the upload button during an active job.","solutions":["Poll the active job's status until terminal, then re-upload","Cancel the in-flight job if it is no longer wanted","Disable/queue the upload action in the UI while a job is active"],"exampleFix":"// before\nconst res = await fetch('/api/analyze/upload', { method: 'POST', body: formData });\nif (res.status === 409) throw new Error('upload failed');\n\n// after\nif (res.status === 409) {\n  await waitForTerminalJob(); // poll job status\n  return fetch('/api/analyze/upload', { method: 'POST', body: formData });\n}\nreturn res;","handlingStrategy":"retry","validationCode":"async function slotFree(getActiveJob, isTerminal) {\n  const j = await getActiveJob();\n  return !j || isTerminal(j.status);\n}","typeGuard":"function isConflictResponse(res) { return res.status === 409; }","tryCatchPattern":"async function uploadWithRetry(formData, { intervalMs = 5000, maxMs = 30 * 60 * 1000 } = {}) {\n  const deadline = Date.now() + maxMs;\n  for (;;) {\n    const res = await fetch('/api/analyze/upload', { method: 'POST', body: formData });\n    if (res.status !== 409) return res;\n    if (Date.now() > deadline) throw new Error('analysis slot still busy');\n    await sleep(intervalMs);\n  }\n}","preventionTips":["Poll the active job to a terminal state before re-uploading","Offer a cancel action in the UI when a 409 lands","Queue uploads client-side instead of firing them concurrently"],"tags":["upload","concurrency","http-409","job-scheduler"],"backgroundTag":"operation-already-in-progress","analyzedSha":"aac7515d2a8c50a1f8f923c6fb77218b333560d6","analyzedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-08-22T20:17:22.307Z"}