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:
@@ -153,6 +153,83 @@ export async function createTaskRunRecord(input: CreateTaskInput) {
|
||||
});
|
||||
}
|
||||
|
||||
export type AtomicCreateResult =
|
||||
| { task: Task; created: true }
|
||||
| { task: null; created: false };
|
||||
|
||||
const SQLITE_CONSTRAINT_UNIQUE = 2067;
|
||||
|
||||
async function attemptAtomicInsert(
|
||||
tx: Parameters<Parameters<typeof db.transaction>[0]>[0],
|
||||
input: CreateTaskInput,
|
||||
now: string
|
||||
): Promise<{ task: Task; created: true } | null> {
|
||||
try {
|
||||
const [row] = await tx
|
||||
.insert(taskRun)
|
||||
.values({
|
||||
id: input.id,
|
||||
user_id: input.user_id,
|
||||
task_type: input.task_type,
|
||||
status: 'queued',
|
||||
stage: 'queued',
|
||||
stage_detail: null,
|
||||
stage_context: null,
|
||||
resource_key: input.resource_key ?? null,
|
||||
notification_read_at: null,
|
||||
notification_silenced_at: null,
|
||||
priority: input.priority,
|
||||
payload: input.payload,
|
||||
result: null,
|
||||
error: null,
|
||||
attempts: 0,
|
||||
max_attempts: input.max_attempts,
|
||||
workflow_run_id: null,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
finished_at: null
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await insertTaskStageEvent(tx, {
|
||||
task_id: row.id,
|
||||
user_id: row.user_id,
|
||||
stage: row.stage as TaskStage,
|
||||
stage_detail: row.stage_detail,
|
||||
stage_context: row.stage_context ?? null,
|
||||
status: row.status,
|
||||
created_at: now
|
||||
});
|
||||
|
||||
return { task: toTask(row), created: true };
|
||||
} catch (error) {
|
||||
const sqliteError = error as { code?: number; message?: string };
|
||||
if (
|
||||
sqliteError.code === SQLITE_CONSTRAINT_UNIQUE ||
|
||||
sqliteError.message?.includes('UNIQUE constraint failed')
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createTaskRunRecordAtomic(input: CreateTaskInput): Promise<AtomicCreateResult> {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
return await db.transaction(async (tx) => {
|
||||
const result = await attemptAtomicInsert(tx, input, now);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
return { task: null, created: false };
|
||||
});
|
||||
}
|
||||
|
||||
export async function setTaskWorkflowRunId(taskId: string, workflowRunId: string) {
|
||||
await db
|
||||
.update(taskRun)
|
||||
|
||||
Reference in New Issue
Block a user