From 626cd9cee948eab706ea53c1e9446abff6f645e7 Mon Sep 17 00:00:00 2001 From: Deploy Bot Date: Fri, 30 Jan 2026 17:02:28 +0530 Subject: [PATCH 1/4] fix: batchTriggerAndWait with duplicate idempotencyKeys (#2965) --- .changeset/fix-batch-duplicate-idempotency.md | 7 ++ .../app/v3/services/batchTriggerV3.server.ts | 90 ++++++++++++++++--- 2 files changed, 83 insertions(+), 14 deletions(-) create mode 100644 .changeset/fix-batch-duplicate-idempotency.md diff --git a/.changeset/fix-batch-duplicate-idempotency.md b/.changeset/fix-batch-duplicate-idempotency.md new file mode 100644 index 0000000000..b68f2d1e98 --- /dev/null +++ b/.changeset/fix-batch-duplicate-idempotency.md @@ -0,0 +1,7 @@ +--- +"@trigger.dev/webapp": patch +--- + +Fix batchTriggerAndWait running forever when duplicate idempotencyKey is provided in the same batch + +When using batchTriggerAndWait with duplicate idempotencyKeys in the same batch, the batch would never complete because the completedCount and expectedCount would be mismatched. This fix ensures that cached runs (duplicate idempotencyKeys) are properly tracked in the batch, with their completedCount incremented immediately if the cached run is already in a final status. diff --git a/apps/webapp/app/v3/services/batchTriggerV3.server.ts b/apps/webapp/app/v3/services/batchTriggerV3.server.ts index e4bc583b7c..ef99aeaa83 100644 --- a/apps/webapp/app/v3/services/batchTriggerV3.server.ts +++ b/apps/webapp/app/v3/services/batchTriggerV3.server.ts @@ -124,11 +124,11 @@ export class BatchTriggerV3Service extends BaseService { const existingBatch = options.idempotencyKey ? await this._prisma.batchTaskRun.findFirst({ - where: { - runtimeEnvironmentId: environment.id, - idempotencyKey: options.idempotencyKey, - }, - }) + where: { + runtimeEnvironmentId: environment.id, + idempotencyKey: options.idempotencyKey, + }, + }) : undefined; if (existingBatch) { @@ -167,16 +167,16 @@ export class BatchTriggerV3Service extends BaseService { const dependentAttempt = body?.dependentAttempt ? await this._prisma.taskRunAttempt.findFirst({ - where: { friendlyId: body.dependentAttempt }, - include: { - taskRun: { - select: { - id: true, - status: true, - }, + where: { friendlyId: body.dependentAttempt }, + include: { + taskRun: { + select: { + id: true, + status: true, }, }, - }) + }, + }) : undefined; if ( @@ -890,7 +890,69 @@ export class BatchTriggerV3Service extends BaseService { } } - return false; + // FIX for Issue #2965: When a run is cached (duplicate idempotencyKey), + // we need to create a BatchTaskRunItem and immediately mark it as completed. + // This ensures the batch completion check (completedCount === expectedCount) works correctly. + const isAlreadyComplete = isFinalRunStatus(result.run.status); + + logger.debug( + "[BatchTriggerV2][processBatchTaskRunItem] Cached run detected, creating batch item", + { + batchId: batch.friendlyId, + runId: task.runId, + cachedRunId: result.run.id, + cachedRunStatus: result.run.status, + isAlreadyComplete, + currentIndex, + } + ); + + try { + // Create a BatchTaskRunItem for the cached run + await this._prisma.batchTaskRunItem.create({ + data: { + batchTaskRunId: batch.id, + taskRunId: result.run.id, + // Mark as COMPLETED if the cached run is already finished + status: isAlreadyComplete ? "COMPLETED" : batchTaskRunItemStatusForRunStatus(result.run.status), + }, + }); + + // If the cached run is already complete, we need to increment completedCount + // since this item won't go through the normal completeBatchTaskRunItemV3 flow + if (isAlreadyComplete) { + await this._prisma.batchTaskRun.update({ + where: { id: batch.id }, + data: { + completedCount: { + increment: 1, + }, + }, + }); + } + + // Return true so expectedCount is incremented + return true; + } catch (error) { + if (isUniqueConstraintError(error, ["batchTaskRunId", "taskRunId"])) { + // BatchTaskRunItem already exists for this batch and cached run + // This can happen if the same idempotencyKey is used multiple times in the same batch + logger.debug( + "[BatchTriggerV2][processBatchTaskRunItem] BatchTaskRunItem already exists for cached run", + { + batchId: batch.friendlyId, + runId: task.runId, + cachedRunId: result.run.id, + currentIndex, + } + ); + + // Don't increment expectedCount since this item is already tracked + return false; + } + + throw error; + } } async #enqueueBatchTaskRun(options: BatchProcessingOptions) { From b72d09cfc06a271a975b750c04943ab76e614c96 Mon Sep 17 00:00:00 2001 From: Deploy Bot Date: Fri, 30 Jan 2026 17:25:33 +0530 Subject: [PATCH 2/4] fix: skip BatchTaskRunItem for in-progress cached runs (Devin review) --- .../app/v3/services/batchTriggerV3.server.ts | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/apps/webapp/app/v3/services/batchTriggerV3.server.ts b/apps/webapp/app/v3/services/batchTriggerV3.server.ts index ef99aeaa83..f331a40fbd 100644 --- a/apps/webapp/app/v3/services/batchTriggerV3.server.ts +++ b/apps/webapp/app/v3/services/batchTriggerV3.server.ts @@ -891,12 +891,11 @@ export class BatchTriggerV3Service extends BaseService { } // FIX for Issue #2965: When a run is cached (duplicate idempotencyKey), - // we need to create a BatchTaskRunItem and immediately mark it as completed. - // This ensures the batch completion check (completedCount === expectedCount) works correctly. + // we need to handle it based on whether the cached run is already complete. const isAlreadyComplete = isFinalRunStatus(result.run.status); logger.debug( - "[BatchTriggerV2][processBatchTaskRunItem] Cached run detected, creating batch item", + "[BatchTriggerV2][processBatchTaskRunItem] Cached run detected", { batchId: batch.friendlyId, runId: task.runId, @@ -907,31 +906,45 @@ export class BatchTriggerV3Service extends BaseService { } ); + // If the cached run is NOT in a final status (still PENDING or EXECUTING), + // we should NOT create a BatchTaskRunItem because: + // 1. The original item will complete when the run finishes + // 2. Creating a duplicate item would cause the batch to hang forever + // (as noted in Devin AI review) + if (!isAlreadyComplete) { + logger.debug( + "[BatchTriggerV2][processBatchTaskRunItem] Cached run still in progress, skipping batch item creation", + { + batchId: batch.friendlyId, + cachedRunId: result.run.id, + cachedRunStatus: result.run.status, + } + ); + // Return false to NOT increment expectedCount + return false; + } + + // The cached run is already complete, create a BatchTaskRunItem and increment completedCount try { - // Create a BatchTaskRunItem for the cached run await this._prisma.batchTaskRunItem.create({ data: { batchTaskRunId: batch.id, taskRunId: result.run.id, - // Mark as COMPLETED if the cached run is already finished - status: isAlreadyComplete ? "COMPLETED" : batchTaskRunItemStatusForRunStatus(result.run.status), + status: "COMPLETED", }, }); - // If the cached run is already complete, we need to increment completedCount - // since this item won't go through the normal completeBatchTaskRunItemV3 flow - if (isAlreadyComplete) { - await this._prisma.batchTaskRun.update({ - where: { id: batch.id }, - data: { - completedCount: { - increment: 1, - }, + // Increment completedCount since the cached run is already finished + await this._prisma.batchTaskRun.update({ + where: { id: batch.id }, + data: { + completedCount: { + increment: 1, }, - }); - } + }, + }); - // Return true so expectedCount is incremented + // Return true so expectedCount is incremented (matches completedCount increment above) return true; } catch (error) { if (isUniqueConstraintError(error, ["batchTaskRunId", "taskRunId"])) { From c1c51024a63dfc312c1b0c7270802517e621870a Mon Sep 17 00:00:00 2001 From: Deploy Bot Date: Fri, 30 Jan 2026 17:53:14 +0530 Subject: [PATCH 3/4] fix: always create BatchTaskRunItem for cached runs (CodeRabbit review) --- .../app/v3/services/batchTriggerV3.server.ts | 51 ++++++++----------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/apps/webapp/app/v3/services/batchTriggerV3.server.ts b/apps/webapp/app/v3/services/batchTriggerV3.server.ts index f331a40fbd..aa7914bafd 100644 --- a/apps/webapp/app/v3/services/batchTriggerV3.server.ts +++ b/apps/webapp/app/v3/services/batchTriggerV3.server.ts @@ -891,11 +891,13 @@ export class BatchTriggerV3Service extends BaseService { } // FIX for Issue #2965: When a run is cached (duplicate idempotencyKey), - // we need to handle it based on whether the cached run is already complete. + // we need to ALWAYS create a BatchTaskRunItem to properly track it. + // This handles cases where cached run may originate from another batch. + // Use unique constraint (batchTaskRunId, taskRunId) to prevent duplicates. const isAlreadyComplete = isFinalRunStatus(result.run.status); logger.debug( - "[BatchTriggerV2][processBatchTaskRunItem] Cached run detected", + "[BatchTriggerV2][processBatchTaskRunItem] Cached run detected, creating batch item", { batchId: batch.friendlyId, runId: task.runId, @@ -906,45 +908,32 @@ export class BatchTriggerV3Service extends BaseService { } ); - // If the cached run is NOT in a final status (still PENDING or EXECUTING), - // we should NOT create a BatchTaskRunItem because: - // 1. The original item will complete when the run finishes - // 2. Creating a duplicate item would cause the batch to hang forever - // (as noted in Devin AI review) - if (!isAlreadyComplete) { - logger.debug( - "[BatchTriggerV2][processBatchTaskRunItem] Cached run still in progress, skipping batch item creation", - { - batchId: batch.friendlyId, - cachedRunId: result.run.id, - cachedRunStatus: result.run.status, - } - ); - // Return false to NOT increment expectedCount - return false; - } - - // The cached run is already complete, create a BatchTaskRunItem and increment completedCount + // Always create BatchTaskRunItem for cached runs + // This ensures proper tracking even for cross-batch scenarios try { await this._prisma.batchTaskRunItem.create({ data: { batchTaskRunId: batch.id, taskRunId: result.run.id, - status: "COMPLETED", + // Use appropriate status based on the cached run's current status + status: isAlreadyComplete ? "COMPLETED" : batchTaskRunItemStatusForRunStatus(result.run.status), }, }); - // Increment completedCount since the cached run is already finished - await this._prisma.batchTaskRun.update({ - where: { id: batch.id }, - data: { - completedCount: { - increment: 1, + // Only increment completedCount if the cached run is already finished + // For in-progress runs, completedCount will be incremented when the run completes + if (isAlreadyComplete) { + await this._prisma.batchTaskRun.update({ + where: { id: batch.id }, + data: { + completedCount: { + increment: 1, + }, }, - }, - }); + }); + } - // Return true so expectedCount is incremented (matches completedCount increment above) + // Return true so expectedCount is incremented return true; } catch (error) { if (isUniqueConstraintError(error, ["batchTaskRunId", "taskRunId"])) { From cd36575753b5382789941123cb0845e6f8f168e0 Mon Sep 17 00:00:00 2001 From: Deploy Bot Date: Fri, 30 Jan 2026 19:29:00 +0530 Subject: [PATCH 4/4] fix: use batchTaskRunItemStatusForRunStatus for all cached runs (Devin review) --- apps/webapp/app/v3/services/batchTriggerV3.server.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/v3/services/batchTriggerV3.server.ts b/apps/webapp/app/v3/services/batchTriggerV3.server.ts index aa7914bafd..f6dcfd6278 100644 --- a/apps/webapp/app/v3/services/batchTriggerV3.server.ts +++ b/apps/webapp/app/v3/services/batchTriggerV3.server.ts @@ -915,8 +915,9 @@ export class BatchTriggerV3Service extends BaseService { data: { batchTaskRunId: batch.id, taskRunId: result.run.id, - // Use appropriate status based on the cached run's current status - status: isAlreadyComplete ? "COMPLETED" : batchTaskRunItemStatusForRunStatus(result.run.status), + // Use batchTaskRunItemStatusForRunStatus() for all cases + // This correctly maps both successful (COMPLETED) and failed (FAILED) statuses + status: batchTaskRunItemStatusForRunStatus(result.run.status), }, });