-
-
Notifications
You must be signed in to change notification settings - Fork 914
feat(engine): Batch trigger reloaded #2779
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c7bfe5c
38c4cd2
4654592
45c335f
61d6431
eef5061
d7effbd
e558d1e
e366c75
dcb03ef
3ed008d
342c9fc
ef76ff7
daa0b5b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -528,6 +528,7 @@ const EnvironmentSchema = z | |
| MAXIMUM_TRACE_SUMMARY_VIEW_COUNT: z.coerce.number().int().default(25_000), | ||
| MAXIMUM_TRACE_DETAILED_SUMMARY_VIEW_COUNT: z.coerce.number().int().default(10_000), | ||
| TASK_PAYLOAD_OFFLOAD_THRESHOLD: z.coerce.number().int().default(524_288), // 512KB | ||
| BATCH_PAYLOAD_OFFLOAD_THRESHOLD: z.coerce.number().int().optional(), // Defaults to TASK_PAYLOAD_OFFLOAD_THRESHOLD if not set | ||
| TASK_PAYLOAD_MAXIMUM_SIZE: z.coerce.number().int().default(3_145_728), // 3MB | ||
| BATCH_TASK_PAYLOAD_MAXIMUM_SIZE: z.coerce.number().int().default(1_000_000), // 1MB | ||
| TASK_RUN_METADATA_MAXIMUM_SIZE: z.coerce.number().int().default(262_144), // 256KB | ||
|
|
@@ -537,6 +538,14 @@ const EnvironmentSchema = z | |
| MAX_BATCH_V2_TRIGGER_ITEMS: z.coerce.number().int().default(500), | ||
| MAX_BATCH_AND_WAIT_V2_TRIGGER_ITEMS: z.coerce.number().int().default(500), | ||
|
|
||
| // 2-phase batch API settings | ||
| STREAMING_BATCH_MAX_ITEMS: z.coerce.number().int().default(1_000), // Max items in streaming batch | ||
| STREAMING_BATCH_ITEM_MAXIMUM_SIZE: z.coerce.number().int().default(3_145_728), | ||
| BATCH_RATE_LIMIT_REFILL_RATE: z.coerce.number().int().default(10), | ||
| BATCH_RATE_LIMIT_MAX: z.coerce.number().int().default(1200), | ||
| BATCH_RATE_LIMIT_REFILL_INTERVAL: z.string().default("10s"), | ||
| BATCH_CONCURRENCY_LIMIT_DEFAULT: z.coerce.number().int().default(10), | ||
|
Comment on lines
+541
to
+547
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apply Duration format validation to BATCH_RATE_LIMIT_REFILL_INTERVAL. Line 546 only validates Consider using a custom Zod refinement or transform that validates the duration format at startup, similar to how other duration fields may be validated elsewhere in the codebase. 🤖 Prompt for AI Agents |
||
|
|
||
| REALTIME_STREAM_VERSION: z.enum(["v1", "v2"]).default("v1"), | ||
| REALTIME_STREAM_MAX_LENGTH: z.coerce.number().int().default(1000), | ||
| REALTIME_STREAM_TTL: z.coerce | ||
|
|
@@ -931,6 +940,25 @@ const EnvironmentSchema = z | |
| .default(process.env.REDIS_TLS_DISABLED ?? "false"), | ||
| BATCH_TRIGGER_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), | ||
|
|
||
| // BatchQueue DRR settings (Run Engine v2) | ||
| BATCH_QUEUE_DRR_QUANTUM: z.coerce.number().int().default(5), | ||
| BATCH_QUEUE_MAX_DEFICIT: z.coerce.number().int().default(50), | ||
| BATCH_QUEUE_CONSUMER_COUNT: z.coerce.number().int().optional(), | ||
| BATCH_QUEUE_CONSUMER_INTERVAL_MS: z.coerce.number().int().optional(), | ||
| // Global rate limit: max items processed per second across all consumers | ||
| // If not set, no global rate limiting is applied | ||
| BATCH_QUEUE_GLOBAL_RATE_LIMIT: z.coerce.number().int().positive().optional(), | ||
|
|
||
| // Batch rate limits and concurrency by plan type | ||
| // Rate limit: max items per minute for batch creation | ||
| BATCH_RATE_LIMIT_FREE: z.coerce.number().int().default(100), // 100 items/min for free | ||
| BATCH_RATE_LIMIT_PAID: z.coerce.number().int().default(10_000), // 10k items/min for paid | ||
| BATCH_RATE_LIMIT_ENTERPRISE: z.coerce.number().int().default(100_000), // 100k items/min for enterprise | ||
| // Processing concurrency: max concurrent batch items being processed | ||
| BATCH_CONCURRENCY_FREE: z.coerce.number().int().default(1), | ||
| BATCH_CONCURRENCY_PAID: z.coerce.number().int().default(10), | ||
| BATCH_CONCURRENCY_ENTERPRISE: z.coerce.number().int().default(50), | ||
|
|
||
| ADMIN_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), | ||
| ADMIN_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), | ||
| ADMIN_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| import { type BatchTaskRunStatus } from "@trigger.dev/database"; | ||
| import { displayableEnvironment } from "~/models/runtimeEnvironment.server"; | ||
| import { engine } from "~/v3/runEngine.server"; | ||
| import { BasePresenter } from "./basePresenter.server"; | ||
|
|
||
| type BatchPresenterOptions = { | ||
| environmentId: string; | ||
| batchId: string; | ||
| userId?: string; | ||
| }; | ||
|
|
||
| export type BatchPresenterData = Awaited<ReturnType<BatchPresenter["call"]>>; | ||
|
|
||
| export class BatchPresenter extends BasePresenter { | ||
| public async call({ environmentId, batchId, userId }: BatchPresenterOptions) { | ||
| const batch = await this._replica.batchTaskRun.findFirst({ | ||
| select: { | ||
| id: true, | ||
| friendlyId: true, | ||
| status: true, | ||
| runCount: true, | ||
| batchVersion: true, | ||
| createdAt: true, | ||
| updatedAt: true, | ||
| completedAt: true, | ||
| processingStartedAt: true, | ||
| processingCompletedAt: true, | ||
| successfulRunCount: true, | ||
| failedRunCount: true, | ||
| idempotencyKey: true, | ||
| runtimeEnvironment: { | ||
| select: { | ||
| id: true, | ||
| type: true, | ||
| slug: true, | ||
| orgMember: { | ||
| select: { | ||
| user: { | ||
| select: { | ||
| id: true, | ||
| name: true, | ||
| displayName: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| errors: { | ||
| select: { | ||
| id: true, | ||
| index: true, | ||
| taskIdentifier: true, | ||
| error: true, | ||
| errorCode: true, | ||
| createdAt: true, | ||
| }, | ||
| orderBy: { | ||
| index: "asc", | ||
| }, | ||
| }, | ||
| }, | ||
| where: { | ||
| runtimeEnvironmentId: environmentId, | ||
| friendlyId: batchId, | ||
| }, | ||
| }); | ||
|
|
||
| if (!batch) { | ||
| throw new Error("Batch not found"); | ||
| } | ||
|
|
||
| const hasFinished = batch.status !== "PENDING" && batch.status !== "PROCESSING"; | ||
| const isV2 = batch.batchVersion === "runengine:v2"; | ||
|
|
||
| // For v2 batches in PROCESSING state, get live progress from Redis | ||
| // This provides real-time updates without waiting for the batch to complete | ||
| let liveSuccessCount = batch.successfulRunCount ?? 0; | ||
| let liveFailureCount = batch.failedRunCount ?? 0; | ||
|
|
||
| if (isV2 && batch.status === "PROCESSING") { | ||
| const liveProgress = await engine.getBatchQueueProgress(batch.id); | ||
| if (liveProgress) { | ||
| liveSuccessCount = liveProgress.successCount; | ||
| liveFailureCount = liveProgress.failureCount; | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| id: batch.id, | ||
| friendlyId: batch.friendlyId, | ||
| status: batch.status as BatchTaskRunStatus, | ||
| runCount: batch.runCount, | ||
| batchVersion: batch.batchVersion, | ||
| isV2, | ||
| createdAt: batch.createdAt.toISOString(), | ||
| updatedAt: batch.updatedAt.toISOString(), | ||
| completedAt: batch.completedAt?.toISOString(), | ||
| processingStartedAt: batch.processingStartedAt?.toISOString(), | ||
| processingCompletedAt: batch.processingCompletedAt?.toISOString(), | ||
| finishedAt: batch.completedAt | ||
| ? batch.completedAt.toISOString() | ||
| : hasFinished | ||
| ? batch.updatedAt.toISOString() | ||
| : undefined, | ||
| hasFinished, | ||
| successfulRunCount: liveSuccessCount, | ||
| failedRunCount: liveFailureCount, | ||
| idempotencyKey: batch.idempotencyKey, | ||
| environment: displayableEnvironment(batch.runtimeEnvironment, userId), | ||
| errors: batch.errors.map((error) => ({ | ||
| id: error.id, | ||
| index: error.index, | ||
| taskIdentifier: error.taskIdentifier, | ||
| error: error.error, | ||
| errorCode: error.errorCode, | ||
| createdAt: error.createdAt.toISOString(), | ||
| })), | ||
| }; | ||
| } | ||
| } | ||
|
|
Uh oh!
There was an error while loading. Please reload this page.