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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user