{"record":{"id":"b8c77b7f0c86292d","repo":"abhigyanpatwari/GitNexus","slug":"analysis-already-in-progress-job-job-id","errorCode":null,"errorMessage":"Analysis already in progress (job ${job.id})","messagePattern":"Analysis already in progress \\(job (.+?)\\)","errorType":"http","errorClass":null,"httpStatus":409,"severity":"error","filePath":"gitnexus/src/server/analyze-job.ts","lineNumber":114,"sourceCode":"\n  /** Create a new job, or return existing active job for the same repo. */\n  createJob(params: { repoUrl?: string; repoPath?: string }): AnalyzeJob {\n    // Dedup: return existing active job for the same repo (by URL or path)\n    for (const job of this.jobs.values()) {\n      if (!this.isTerminal(job.status)) {\n        const isSameRepo =\n          (params.repoUrl && job.repoUrl === params.repoUrl) ||\n          (params.repoPath && job.repoPath === params.repoPath);\n        if (isSameRepo) {\n          return job;\n        }\n      }\n    }\n\n    // Single-slot: reject if another job is active (different repo)\n    for (const job of this.jobs.values()) {\n      if (!this.isTerminal(job.status)) {\n        throw new Error(`Analysis already in progress (job ${job.id})`);\n      }\n    }\n\n    const job: AnalyzeJob = {\n      id: randomUUID(),\n      status: 'queued',\n      repoUrl: params.repoUrl,\n      repoPath: params.repoPath,\n      progress: { phase: 'queued', percent: 0, message: 'Waiting to start...' },\n      startedAt: Date.now(),\n      retryCount: 0,\n    };\n\n    this.jobs.set(job.id, job);\n    return job;\n  }\n\n  getJob(id: string): AnalyzeJob | undefined {","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/aac7515d2a8c50a1f8f923c6fb77218b333560d6/gitnexus/src/server/analyze-job.ts#L96-L132","documentation":"JobManager.createJob enforces a single analysis slot. It first dedups: an active job for the same repoUrl or repoPath is returned instead of throwing. Only when a DIFFERENT repo has a non-terminal job (queued/cloning/analyzing) does it throw this error, guarding the one-analysis-at-a-time invariant of POST /api/analyze.","triggerScenarios":"POST /api/analyze with url/path for repo B while repo A's job is in any non-terminal status; the in-memory this.jobs map has no terminal job when the request arrives.","commonSituations":"A UI letting a user queue a second repo while the first analyzes; automation firing parallel analyze requests; a wedged worker whose job never reaches succeeded/failed, permanently holding the slot until server restart (jobs are in-memory).","solutions":["Poll the existing job (GET status endpoint) until it reaches a terminal status, then retry the request","Cancel the active job via the cancel endpoint if it is stale or unwanted","Serialize analyze calls in your orchestration so only one is in flight","If a job is wedged non-terminal with no worker alive, restart the gitnexus server to clear the in-memory map"],"exampleFix":"// before\nawait fetch('/api/analyze', { method: 'POST', body: JSON.stringify({ url: repoB }) }); // 500: already in progress\n\n// after\nawait waitForTerminalJob(activeJobId); // poll status until succeeded/failed\nawait fetch('/api/analyze', { method: 'POST', body: JSON.stringify({ url: repoB }) });","handlingStrategy":"retry","validationCode":"async function noActiveJob(getStatus) {\n  const s = await getStatus(); // e.g. GET /api/analyze/status/:id or jobs listing\n  return !s || ['succeeded', 'failed', 'cancelled'].includes(s.status);\n}","typeGuard":"function isTerminalJobStatus(status) {\n  return ['succeeded', 'failed', 'cancelled'].includes(status);\n}","tryCatchPattern":"async function createJobWithRetry(createJob, p, { pollMs = 2000, maxMs = 30 * 60 * 1000 } = {}) {\n  for (;;) {\n    try { return createJob(p); }\n    catch (e) {\n      if (!/already in progress/.test(String(e.message))) throw e;\n      const m = e.message.match(/job (\\S+\\))/); // active job id\n      await waitForTerminal(m && m[1], { pollMs, maxMs }); // poll status until terminal\n    }\n  }\n}","preventionTips":["Serialize analyze requests in orchestrators — one in flight at a time","Check job status before creating a new one; expose the active job in the UI so users see why a 409 happened","Always cancel jobs you abandon, so the single slot is released","Monitor for jobs stuck non-terminal and restart the server to clear the in-memory map"],"tags":["job-scheduler","concurrency","http-409","single-slot"],"backgroundTag":"operation-already-in-progress","analyzedSha":"aac7515d2a8c50a1f8f923c6fb77218b333560d6","analyzedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-08-22T20:17:22.307Z"}