Skip to content

Commit 2ecf510

Browse files
committed
Optionally trigger batched items sequentially to preserve order
1 parent 91afa5e commit 2ecf510

File tree

10 files changed

+206
-76
lines changed

10 files changed

+206
-76
lines changed

.changeset/sweet-suits-kick.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
"@trigger.dev/core": patch
4+
---
5+
6+
Add option to trigger batched items sequentially, and default to parallel triggering which is faster

apps/webapp/app/routes/api.v1.tasks.batch.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,21 @@ import { env } from "~/env.server";
99
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
1010
import { HeadersSchema } from "./api.v1.tasks.$taskId.trigger";
1111
import { resolveIdempotencyKeyTTL } from "~/utils/idempotencyKeys.server";
12-
import { BatchTriggerV2Service } from "~/v3/services/batchTriggerV2.server";
12+
import {
13+
BatchProcessingStrategy,
14+
BatchTriggerV2Service,
15+
} from "~/v3/services/batchTriggerV2.server";
1316
import { ServiceValidationError } from "~/v3/services/baseService.server";
1417
import { OutOfEntitlementError } from "~/v3/services/triggerTask.server";
1518
import { AuthenticatedEnvironment, getOneTimeUseToken } from "~/services/apiAuth.server";
1619
import { logger } from "~/services/logger.server";
20+
import { z } from "zod";
1721

1822
const { action, loader } = createActionApiRoute(
1923
{
20-
headers: HeadersSchema,
24+
headers: HeadersSchema.extend({
25+
"batch-processing-strategy": BatchProcessingStrategy.nullish(),
26+
}),
2127
body: BatchTriggerTaskV2RequestBody,
2228
allowJWT: true,
2329
maxContentLength: env.BATCH_TASK_PAYLOAD_MAXIMUM_SIZE,
@@ -52,6 +58,7 @@ const { action, loader } = createActionApiRoute(
5258
"x-trigger-span-parent-as-link": spanParentAsLink,
5359
"x-trigger-worker": isFromWorker,
5460
"x-trigger-client": triggerClient,
61+
"batch-processing-strategy": batchProcessingStrategy,
5562
traceparent,
5663
tracestate,
5764
} = headers;
@@ -67,6 +74,7 @@ const { action, loader } = createActionApiRoute(
6774
triggerClient,
6875
traceparent,
6976
tracestate,
77+
batchProcessingStrategy,
7078
});
7179

7280
const traceContext =
@@ -79,7 +87,7 @@ const { action, loader } = createActionApiRoute(
7987
resolveIdempotencyKeyTTL(idempotencyKeyTTL) ??
8088
new Date(Date.now() + 24 * 60 * 60 * 1000 * 30);
8189

82-
const service = new BatchTriggerV2Service();
90+
const service = new BatchTriggerV2Service(batchProcessingStrategy ?? undefined);
8391

8492
try {
8593
const batch = await service.call(authentication.environment, body, {

apps/webapp/app/routes/api.v3.runs.$runId.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const loader = createLoaderApiRoute(
1515
findResource: (params, auth) => {
1616
return ApiRetrieveRunPresenter.findRun(params.runId, auth.environment);
1717
},
18+
shouldRetryNotFound: true,
1819
authorization: {
1920
action: "read",
2021
resource: (run) => ({

apps/webapp/app/services/routeBuilders/apiBuilder.server.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type ApiKeyRouteBuilderOptions<
3333
params: TParamsSchema extends z.AnyZodObject ? z.infer<TParamsSchema> : undefined,
3434
authentication: ApiAuthenticationResultSuccess
3535
) => Promise<TResource | undefined>;
36+
shouldRetryNotFound?: boolean;
3637
authorization?: {
3738
action: AuthorizationAction;
3839
resource: (
@@ -81,6 +82,7 @@ export function createLoaderApiRoute<
8182
corsStrategy = "none",
8283
authorization,
8384
findResource,
85+
shouldRetryNotFound,
8486
} = options;
8587

8688
if (corsStrategy !== "none" && request.method.toUpperCase() === "OPTIONS") {
@@ -162,7 +164,10 @@ export function createLoaderApiRoute<
162164
if (!resource) {
163165
return await wrapResponse(
164166
request,
165-
json({ error: "Not found" }, { status: 404 }),
167+
json(
168+
{ error: "Not found" },
169+
{ status: 404, headers: { "x-should-retry": shouldRetryNotFound ? "true" : "false" } }
170+
),
166171
corsStrategy !== "none"
167172
);
168173
}

apps/webapp/app/services/worker.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,7 @@ function getWorkerQueue() {
733733
priority: 0,
734734
maxAttempts: 5,
735735
handler: async (payload, job) => {
736-
const service = new BatchTriggerV2Service();
736+
const service = new BatchTriggerV2Service(payload.strategy);
737737

738738
await service.processBatchTaskRun(payload);
739739
},

apps/webapp/app/v3/services/batchTriggerV2.server.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
parsePacket,
77
} from "@trigger.dev/core/v3";
88
import { BatchTaskRun, Prisma, TaskRunAttempt } from "@trigger.dev/database";
9-
import { $transaction, PrismaClientOrTransaction } from "~/db.server";
9+
import { $transaction, prisma, PrismaClientOrTransaction } from "~/db.server";
1010
import { env } from "~/env.server";
1111
import { batchTaskRunItemStatusForRunStatus } from "~/models/taskRun.server";
1212
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
@@ -26,11 +26,8 @@ import { z } from "zod";
2626
const PROCESSING_BATCH_SIZE = 50;
2727
const ASYNC_BATCH_PROCESS_SIZE_THRESHOLD = 20;
2828

29-
const BatchProcessingStrategy = z.enum(["sequential", "parallel"]);
30-
31-
type BatchProcessingStrategy = z.infer<typeof BatchProcessingStrategy>;
32-
33-
const CURRENT_STRATEGY: BatchProcessingStrategy = "parallel";
29+
export const BatchProcessingStrategy = z.enum(["sequential", "parallel"]);
30+
export type BatchProcessingStrategy = z.infer<typeof BatchProcessingStrategy>;
3431

3532
export const BatchProcessingOptions = z.object({
3633
batchId: z.string(),
@@ -52,6 +49,17 @@ export type BatchTriggerTaskServiceOptions = {
5249
};
5350

5451
export class BatchTriggerV2Service extends BaseService {
52+
private _batchProcessingStrategy: BatchProcessingStrategy;
53+
54+
constructor(
55+
batchProcessingStrategy?: BatchProcessingStrategy,
56+
protected readonly _prisma: PrismaClientOrTransaction = prisma
57+
) {
58+
super(_prisma);
59+
60+
this._batchProcessingStrategy = batchProcessingStrategy ?? "parallel";
61+
}
62+
5563
public async call(
5664
environment: AuthenticatedEnvironment,
5765
body: BatchTriggerTaskV2RequestBody,
@@ -452,14 +460,14 @@ export class BatchTriggerV2Service extends BaseService {
452460
},
453461
});
454462

455-
switch (CURRENT_STRATEGY) {
463+
switch (this._batchProcessingStrategy) {
456464
case "sequential": {
457465
await this.#enqueueBatchTaskRun({
458466
batchId: batch.id,
459467
processingId: batchId,
460468
range: { start: 0, count: PROCESSING_BATCH_SIZE },
461469
attemptCount: 0,
462-
strategy: CURRENT_STRATEGY,
470+
strategy: this._batchProcessingStrategy,
463471
});
464472

465473
break;
@@ -480,7 +488,7 @@ export class BatchTriggerV2Service extends BaseService {
480488
processingId: `${index}`,
481489
range,
482490
attemptCount: 0,
483-
strategy: CURRENT_STRATEGY,
491+
strategy: this._batchProcessingStrategy,
484492
},
485493
tx
486494
)

packages/core/src/v3/apiClient/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export type ClientTriggerOptions = {
7474
export type ClientBatchTriggerOptions = ClientTriggerOptions & {
7575
idempotencyKey?: string;
7676
idempotencyKeyTTL?: string;
77+
processingStrategy?: "parallel" | "sequential";
7778
};
7879

7980
export type TriggerRequestOptions = ZodFetchOptions & {
@@ -239,6 +240,7 @@ export class ApiClient {
239240
headers: this.#getHeaders(clientOptions?.spanParentAsLink ?? false, {
240241
"idempotency-key": clientOptions?.idempotencyKey,
241242
"idempotency-key-ttl": clientOptions?.idempotencyKeyTTL,
243+
"batch-processing-strategy": clientOptions?.processingStrategy,
242244
}),
243245
body: JSON.stringify(body),
244246
},

packages/core/src/v3/types/tasks.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,8 @@ export interface Task<TIdentifier extends string, TInput = void, TOutput = any>
592592
* ```
593593
*/
594594
batchTriggerAndWait: (
595-
items: Array<BatchTriggerAndWaitItem<TInput>>
595+
items: Array<BatchTriggerAndWaitItem<TInput>>,
596+
options?: BatchTriggerAndWaitOptions
596597
) => Promise<BatchResult<TIdentifier, TOutput>>;
597598
}
598599

@@ -781,6 +782,32 @@ export type TriggerAndWaitOptions = Omit<TriggerOptions, "idempotencyKey" | "ide
781782
export type BatchTriggerOptions = {
782783
idempotencyKey?: IdempotencyKey | string | string[];
783784
idempotencyKeyTTL?: string;
785+
786+
/**
787+
* When true, triggers tasks sequentially in batch order. This ensures ordering but may be slower,
788+
* especially for large batches.
789+
*
790+
* When false (default), triggers tasks in parallel for better performance, but order is not guaranteed.
791+
*
792+
* Note: This only affects the order of run creation, not the actual task execution.
793+
*
794+
* @default false
795+
*/
796+
triggerSequentially?: boolean;
797+
};
798+
799+
export type BatchTriggerAndWaitOptions = {
800+
/**
801+
* When true, triggers tasks sequentially in batch order. This ensures ordering but may be slower,
802+
* especially for large batches.
803+
*
804+
* When false (default), triggers tasks in parallel for better performance, but order is not guaranteed.
805+
*
806+
* Note: This only affects the order of run creation, not the actual task execution.
807+
*
808+
* @default false
809+
*/
810+
triggerSequentially?: boolean;
784811
};
785812

786813
export type TaskMetadataWithFunctions = TaskMetadata & {

packages/trigger-sdk/src/v3/shared.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import type {
7474
TriggerApiRequestOptions,
7575
TriggerOptions,
7676
AnyTaskRunResult,
77+
BatchTriggerAndWaitOptions,
7778
} from "@trigger.dev/core/v3";
7879

7980
export type {
@@ -181,7 +182,7 @@ export function createTask<
181182
});
182183
}, params.id);
183184
},
184-
batchTriggerAndWait: async (items) => {
185+
batchTriggerAndWait: async (items, options) => {
185186
const taskMetadata = taskCatalog.getTaskManifest(params.id);
186187

187188
return await batchTriggerAndWait_internal<TIdentifier, TInput, TOutput>(
@@ -191,6 +192,7 @@ export function createTask<
191192
params.id,
192193
items,
193194
undefined,
195+
options,
194196
undefined,
195197
customQueue
196198
);
@@ -326,7 +328,7 @@ export function createSchemaTask<
326328
});
327329
}, params.id);
328330
},
329-
batchTriggerAndWait: async (items) => {
331+
batchTriggerAndWait: async (items, options) => {
330332
const taskMetadata = taskCatalog.getTaskManifest(params.id);
331333

332334
return await batchTriggerAndWait_internal<TIdentifier, inferSchemaIn<TSchema>, TOutput>(
@@ -336,6 +338,7 @@ export function createSchemaTask<
336338
params.id,
337339
items,
338340
parsePayload,
341+
options,
339342
undefined,
340343
customQueue
341344
);
@@ -469,13 +472,14 @@ export function triggerAndWait<TTask extends AnyTask>(
469472
export async function batchTriggerAndWait<TTask extends AnyTask>(
470473
id: TaskIdentifier<TTask>,
471474
items: Array<BatchItem<TaskPayload<TTask>>>,
475+
options?: BatchTriggerAndWaitOptions,
472476
requestOptions?: ApiRequestOptions
473477
): Promise<BatchResult<TaskIdentifier<TTask>, TaskOutput<TTask>>> {
474478
return await batchTriggerAndWait_internal<
475479
TaskIdentifier<TTask>,
476480
TaskPayload<TTask>,
477481
TaskOutput<TTask>
478-
>("tasks.batchTriggerAndWait()", id, items, undefined, requestOptions);
482+
>("tasks.batchTriggerAndWait()", id, items, undefined, options, requestOptions);
479483
}
480484

481485
/**
@@ -618,6 +622,7 @@ export async function batchTriggerById<TTask extends AnyTask>(
618622
spanParentAsLink: true,
619623
idempotencyKey: await makeIdempotencyKey(options?.idempotencyKey),
620624
idempotencyKeyTTL: options?.idempotencyKeyTTL,
625+
processingStrategy: options?.triggerSequentially ? "sequential" : undefined,
621626
},
622627
{
623628
name: "batch.trigger()",
@@ -740,6 +745,7 @@ export async function batchTriggerById<TTask extends AnyTask>(
740745
*/
741746
export async function batchTriggerByIdAndWait<TTask extends AnyTask>(
742747
items: Array<BatchByIdAndWaitItem<InferRunTypes<TTask>>>,
748+
options?: BatchTriggerAndWaitOptions,
743749
requestOptions?: TriggerApiRequestOptions
744750
): Promise<BatchByIdResult<TTask>> {
745751
const ctx = taskContext.ctx;
@@ -786,7 +792,9 @@ export async function batchTriggerByIdAndWait<TTask extends AnyTask>(
786792
),
787793
dependentAttempt: ctx.attempt.id,
788794
},
789-
{},
795+
{
796+
processingStrategy: options?.triggerSequentially ? "sequential" : undefined,
797+
},
790798
requestOptions
791799
);
792800

@@ -948,6 +956,7 @@ export async function batchTriggerTasks<TTasks extends readonly AnyTask[]>(
948956
spanParentAsLink: true,
949957
idempotencyKey: await makeIdempotencyKey(options?.idempotencyKey),
950958
idempotencyKeyTTL: options?.idempotencyKeyTTL,
959+
processingStrategy: options?.triggerSequentially ? "sequential" : undefined,
951960
},
952961
{
953962
name: "batch.triggerByTask()",
@@ -1072,6 +1081,7 @@ export async function batchTriggerAndWaitTasks<TTasks extends readonly AnyTask[]
10721081
items: {
10731082
[K in keyof TTasks]: BatchByTaskAndWaitItem<TTasks[K]>;
10741083
},
1084+
options?: BatchTriggerAndWaitOptions,
10751085
requestOptions?: TriggerApiRequestOptions
10761086
): Promise<BatchByTaskResult<TTasks>> {
10771087
const ctx = taskContext.ctx;
@@ -1118,7 +1128,9 @@ export async function batchTriggerAndWaitTasks<TTasks extends readonly AnyTask[]
11181128
),
11191129
dependentAttempt: ctx.attempt.id,
11201130
},
1121-
{},
1131+
{
1132+
processingStrategy: options?.triggerSequentially ? "sequential" : undefined,
1133+
},
11221134
requestOptions
11231135
);
11241136

@@ -1256,6 +1268,7 @@ async function batchTrigger_internal<TRunTypes extends AnyRunTypes>(
12561268
spanParentAsLink: true,
12571269
idempotencyKey: await makeIdempotencyKey(options?.idempotencyKey),
12581270
idempotencyKeyTTL: options?.idempotencyKeyTTL,
1271+
processingStrategy: options?.triggerSequentially ? "sequential" : undefined,
12591272
},
12601273
{
12611274
name,
@@ -1377,6 +1390,7 @@ async function batchTriggerAndWait_internal<TIdentifier extends string, TPayload
13771390
id: TIdentifier,
13781391
items: Array<BatchTriggerAndWaitItem<TPayload>>,
13791392
parsePayload?: SchemaParseFn<TPayload>,
1393+
options?: BatchTriggerAndWaitOptions,
13801394
requestOptions?: ApiRequestOptions,
13811395
queue?: QueueOptions
13821396
): Promise<BatchResult<TIdentifier, TOutput>> {
@@ -1420,7 +1434,9 @@ async function batchTriggerAndWait_internal<TIdentifier extends string, TPayload
14201434
),
14211435
dependentAttempt: ctx.attempt.id,
14221436
},
1423-
{},
1437+
{
1438+
processingStrategy: options?.triggerSequentially ? "sequential" : undefined,
1439+
},
14241440
requestOptions
14251441
);
14261442

0 commit comments

Comments
 (0)