fix(heartbeat): atomic find-or-create + checkout to eliminate duplicate task race
Replace the racy three-step heartbeat path (listAgentTasks -> findReusableHeartbeatTask -> saveTask -> checkoutTask) with a single transactional acquireHeartbeatTask method. The prior flow allowed two concurrent scheduler ticks to both observe "no reusable heartbeat" and both INSERT a fresh [Heartbeat] task before either CAS-checked out, producing duplicates that cleanupDuplicateHeartbeatTasks had to mop up. acquireHeartbeatTask runs inside BEGIN IMMEDIATE so concurrent ticks serialize on the writer lock. Inside the transaction it first attempts to claim any existing claimable [Heartbeat] task (todo/backlog/done/ in_review with checkout_run_id IS NULL), reviving done/in_review by resetting status. If nothing is claimable but an in-flight heartbeat exists (status NOT IN done/cancelled), it returns null instead of creating a duplicate. Only when no heartbeat task exists at all does it INSERT a new one, already in 'in_progress' with checkout_run_id set. cleanupDuplicateHeartbeatTasks remains as deletion-side defense.
This commit is contained in:
1 parent
659a2026de
commit
8f7054630b
3 files changed
+143
-45
No files matched your search
@@ -243,6 +243,7 @@ export class AppDatabaseBase {
|
||||
renewTaskLease(taskId: string, runId: string) { return this.tasks.renewTaskLease(taskId, runId); }
|
||||
listAgentTasks(agentId: string, companyId: string, statuses: string[]) { return this.tasks.listAgentTasks(agentId, companyId, statuses); }
|
||||
findReusableHeartbeatTask(agentId: string, companyId: string) { return this.tasks.findReusableHeartbeatTask(agentId, companyId); }
|
||||
acquireHeartbeatTask(input: { agentId: string; companyId: string; runId: string; workspaceId: string | null; title: string; description: string; priority: string }) { return this.tasks.acquireHeartbeatTask(input); }
|
||||
cleanupDuplicateHeartbeatTasks(companyId: string) { return this.tasks.cleanupDuplicateHeartbeatTasks(companyId); }
|
||||
listReviewTasksForManager(managerId: string, companyId: string) { return this.tasks.listReviewTasksForManager(managerId, companyId); }
|
||||
countActiveTasksForCompany(companyId: string) { return this.tasks.countActiveTasksForCompany(companyId); }
|
||||
|
||||
Reference in new issue
Block a user