fix(MULTIVERSION-0001): fix multi-version indexing — jobs never created or triggered for secondary versions

Two bugs prevented secondary versions from ever being indexed:

1. JobQueue.enqueue() and RepositoryService.createIndexingJob() deduplication
   only checked repository_id, so a queued default-branch job blocked all
   version-specific jobs for the same repo. Fix: include version_id in the
   WHERE clause so only exact (repository_id, version_id) pairs are deduped.

2. POST /api/v1/libs/:id/versions used repoService.createIndexingJob() which
   inserts a job record but never triggers queue processing. Fix: use
   queue.enqueue() (same fallback pattern as the libs endpoint) so setImmediate
   fires processNext() after the job is inserted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Giancarmine Salucci
2026-03-28 09:32:27 +01:00
parent 781d224adc
commit 1c5b634ea4
5 changed files with 69 additions and 8 deletions

View File

@@ -36,14 +36,17 @@ export class JobQueue {
* existing job instead of creating a duplicate.
*/
enqueue(repositoryId: string, versionId?: string): IndexingJob {
// Return early if there's already an active job for this repo.
// Return early if there's already an active job for this exact (repo, version) pair.
const resolvedVersionId = versionId ?? null;
const activeRaw = this.db
.prepare<[string], IndexingJobEntity>(
.prepare<[string, string | null, string | null], IndexingJobEntity>(
`${JOB_SELECT}
WHERE repository_id = ? AND status IN ('queued', 'running')
WHERE repository_id = ?
AND (version_id = ? OR (version_id IS NULL AND ? IS NULL))
AND status IN ('queued', 'running')
ORDER BY created_at DESC LIMIT 1`
)
.get(repositoryId);
.get(repositoryId, resolvedVersionId, resolvedVersionId);
if (activeRaw) {
// Ensure the queue is draining even if enqueue was called concurrently.

View File

@@ -529,4 +529,24 @@ describe('RepositoryService.createIndexingJob()', () => {
const job = service.createIndexingJob('/facebook/react', '/facebook/react/v18.3.0');
expect(job.versionId).toBe('/facebook/react/v18.3.0');
});
it('allows separate jobs for the same repo but different versions', () => {
const defaultJob = service.createIndexingJob('/facebook/react');
const versionJob = service.createIndexingJob('/facebook/react', '/facebook/react/v18.3.0');
expect(versionJob.id).not.toBe(defaultJob.id);
expect(defaultJob.versionId).toBeNull();
expect(versionJob.versionId).toBe('/facebook/react/v18.3.0');
});
it('returns the existing job when the same (repo, version) pair is already queued', () => {
const job1 = service.createIndexingJob('/facebook/react', '/facebook/react/v18.3.0');
const job2 = service.createIndexingJob('/facebook/react', '/facebook/react/v18.3.0');
expect(job2.id).toBe(job1.id);
});
it('returns the existing default-branch job when called again without a versionId', () => {
const job1 = service.createIndexingJob('/facebook/react');
const job2 = service.createIndexingJob('/facebook/react');
expect(job2.id).toBe(job1.id);
});
});

View File

@@ -319,14 +319,17 @@ export class RepositoryService {
* If a job is already running, returns the existing job.
*/
createIndexingJob(repositoryId: string, versionId?: string): IndexingJob {
// Check for running job
// Check for an existing queued/running job for this exact (repo, version) pair.
const resolvedVersionId = versionId ?? null;
const runningJob = this.db
.prepare(
`SELECT * FROM indexing_jobs
WHERE repository_id = ? AND status IN ('queued', 'running')
WHERE repository_id = ?
AND (version_id = ? OR (version_id IS NULL AND ? IS NULL))
AND status IN ('queued', 'running')
ORDER BY created_at DESC LIMIT 1`
)
.get(repositoryId) as IndexingJobEntity | undefined;
.get(repositoryId, resolvedVersionId, resolvedVersionId) as IndexingJobEntity | undefined;
if (runningJob) return IndexingJobMapper.fromEntity(new IndexingJobEntity(runningJob));