Add atomic task deduplication with partial unique index

- Add partial unique index for active resource-scoped tasks
- Implement createTaskRunRecordAtomic for race-free task creation
- Update findOrEnqueueTask to use atomic insert first
- Add tests for concurrent task creation deduplication
This commit is contained in:
2026-03-15 14:40:38 -04:00
parent a7f7be50b4
commit ed4420b8db
5 changed files with 225 additions and 6 deletions

View File

@@ -7,6 +7,7 @@ import { describeTaskFailure } from '@/lib/server/task-errors';
import {
countTasksByStatus,
createTaskRunRecord,
createTaskRunRecordAtomic,
findInFlightTaskByResourceKey,
getTaskByIdForUser,
listTaskStageEventsForTask,
@@ -134,6 +135,38 @@ export async function findOrEnqueueTask(input: EnqueueTaskInput) {
return await enqueueTask(input);
}
const taskId = randomUUID();
const result = await createTaskRunRecordAtomic({
id: taskId,
user_id: input.userId,
task_type: input.taskType,
payload: input.payload ?? {},
priority: input.priority ?? 50,
max_attempts: input.maxAttempts ?? 3,
resource_key: input.resourceKey
});
if (result.created) {
try {
const run = await start(runTaskWorkflow, [result.task.id]);
await setTaskWorkflowRunId(result.task.id, run.runId);
return {
...result.task,
workflow_run_id: run.runId
} satisfies Task;
} catch (error) {
const failure = describeTaskFailure(result.task, 'Failed to start workflow');
await markTaskFailure(result.task.id, failure.detail, 'failed', {
detail: failure.summary
});
const wrapped = new Error(failure.detail);
(wrapped as Error & { cause?: unknown }).cause = error;
throw wrapped;
}
}
const existingTask = await findInFlightTaskByResourceKey(
input.userId,
input.taskType,
@@ -141,13 +174,10 @@ export async function findOrEnqueueTask(input: EnqueueTaskInput) {
);
if (existingTask) {
const reconciledTask = await reconcileTaskWithWorkflow(existingTask);
if (reconciledTask.status === 'queued' || reconciledTask.status === 'running') {
return reconciledTask;
}
return await reconcileTaskWithWorkflow(existingTask);
}
return await enqueueTask(input);
throw new Error('Task deduplication conflict detected but no in-flight task found');
}
export async function findInFlightTask(userId: string, taskType: TaskType, resourceKey: string) {