From 3eea949f7bb6cb832a31c0d1c823b7c483cd9f0e Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 2 Oct 2023 12:15:10 +0000 Subject: [PATCH 01/27] Upgrade graphile-worker --- apps/webapp/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 2e90e4b22e..4d4f5ce165 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -74,7 +74,7 @@ "emails": "workspace:*", "express": "^4.18.1", "framer-motion": "^10.12.11", - "graphile-worker": "^0.13.0", + "graphile-worker": "0.14.0-rc.0", "highlight.run": "^7.3.4", "humanize-duration": "^3.27.3", "intl-parse-accept-language": "^1.0.0", From e4630f6f203c018e17697154591291fb174ae706 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 2 Oct 2023 12:16:17 +0000 Subject: [PATCH 02/27] CronItem.pattern to match --- apps/webapp/app/platform/zodWorker.server.ts | 4 ++-- apps/webapp/app/services/worker.server.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/platform/zodWorker.server.ts b/apps/webapp/app/platform/zodWorker.server.ts index ea3cafe4b9..9bc034345b 100644 --- a/apps/webapp/app/platform/zodWorker.server.ts +++ b/apps/webapp/app/platform/zodWorker.server.ts @@ -67,7 +67,7 @@ type RecurringTaskPayload = { export type ZodRecurringTasks = { [key: string]: { - pattern: string; + match: string; options?: CronItemOptions; handler: (payload: RecurringTaskPayload, job: GraphileJob) => Promise; }; @@ -349,7 +349,7 @@ export class ZodWorker { for (const [key, task] of Object.entries(this.#recurringTasks)) { const cronItem: CronItem = { - pattern: task.pattern, + match: task.match, identifier: key, task: key, options: task.options, diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index 59c5e8b52c..8192698683 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -136,7 +136,7 @@ function getWorkerQueue() { recurringTasks: { // Run this every 5 minutes autoIndexProductionEndpoints: { - pattern: "*/5 * * * *", + match: "*/5 * * * *", handler: async (payload, job) => { const service = new RecurringEndpointIndexService(); @@ -145,7 +145,7 @@ function getWorkerQueue() { }, // Run this every hour purgeOldIndexings: { - pattern: "0 * * * *", + match: "0 * * * *", handler: async (payload, job) => { // Delete indexings that are older than 7 days await prisma.endpointIndex.deleteMany({ From 8e887afb160888d822afa5acb8944b37dbdb6c6d Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 2 Oct 2023 12:24:53 +0000 Subject: [PATCH 03/27] Update graphile job schema --- apps/webapp/app/platform/zodWorker.server.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/platform/zodWorker.server.ts b/apps/webapp/app/platform/zodWorker.server.ts index 9bc034345b..3d8a01bbd6 100644 --- a/apps/webapp/app/platform/zodWorker.server.ts +++ b/apps/webapp/app/platform/zodWorker.server.ts @@ -1,7 +1,7 @@ import type { CronItem, CronItemOptions, - Job as GraphileJob, + DbJob as GraphileJob, Runner as GraphileRunner, JobHelpers, RunnerOptions, @@ -29,8 +29,8 @@ const RawCronPayloadSchema = z.object({ const GraphileJobSchema = z.object({ id: z.coerce.string(), - queue_name: z.string().nullable(), - task_identifier: z.string(), + job_queue_id: z.number().nullable(), + task_id: z.number(), payload: z.unknown(), priority: z.number(), run_at: z.coerce.date(), From 0400e02ec50b9112aa765a0b6ee0c1862d294221 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 2 Oct 2023 15:50:27 +0000 Subject: [PATCH 04/27] Add FAIL_LOCKED_JOBS_ON_STARTUP --- apps/webapp/app/env.server.ts | 1 + apps/webapp/app/services/worker.server.ts | 55 +++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 46f3f9e819..f7794d06e8 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -40,6 +40,7 @@ const EnvironmentSchema = z.object({ EXECUTION_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), WORKER_ENABLED: z.string().default("true"), EXECUTION_WORKER_ENABLED: z.string().default("true"), + FAIL_LOCKED_JOBS_ON_STARTUP: z.string().default("false"), }); export type Environment = z.infer; diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index 8192698683..4732027093 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -111,6 +111,10 @@ if (env.NODE_ENV === "production") { } export async function init() { + if (env.FAIL_LOCKED_JOBS_ON_STARTUP === "true") { + return failLockedTasks(); + } + if (env.WORKER_ENABLED === "true") { await workerQueue.initialize(); } @@ -120,6 +124,57 @@ export async function init() { } } +async function failLockedTasks() { + console.log("⚠️ Failing all locked jobs as requested"); + console.log("⚠️ Will NOT start any workers"); + + const graphileWorkerSchema = env.WORKER_SCHEMA; + + const message = "Failing all locked jobs"; + + const migrationQueryResult = await prisma.$queryRawUnsafe(` + SELECT id FROM graphile_worker.migrations + ORDER BY id DESC LIMIT 1`); + + const MigrationQueryResultSchema = z.array(z.object({ id: z.number() })); + + const migrationResults = MigrationQueryResultSchema.parse(migrationQueryResult); + if (!migrationResults.length) { + throw new Error("No migrations applied yet."); + } + + const latestMigration = migrationResults[0].id; + const separateJobQueuesTable = latestMigration > 10; + + const unlockJobQueue = `, queues AS ( + UPDATE ${graphileWorkerSchema}.job_queues + SET + locked_by = null, + locked_at = null + FROM j + WHERE job_queues.id = j.job_queue_id + )`; + + const failJobsResult = await prisma.$queryRawUnsafe>( + `WITH j AS ( + UPDATE ${graphileWorkerSchema}.jobs + SET + last_error = $1::text, + run_at = greatest(now(), run_at) + (exp(least(attempts, 10)) * interval '1 second'), + locked_by = null, + locked_at = null + WHERE locked_at is not null + AND locked_at > now() - interval '4 hours' + RETURNING * + )${separateJobQueuesTable ? unlockJobQueue : ""} + SELECT * FROM j;`, + message + ); + + console.log(`⚠️ Failed ${failJobsResult.length} locked jobs`); + console.log("⚠️ Unset FAIL_LOCKED_JOBS_ON_STARTUP to enable workers"); +} + function getWorkerQueue() { return new ZodWorker({ name: "workerQueue", From 6440e30e14336b25eae0c2448a54ce5558d856ec Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:06:37 +0000 Subject: [PATCH 05/27] Add to .env.example --- .env.example | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.env.example b/.env.example index 48170e8f36..5bac3ac60d 100644 --- a/.env.example +++ b/.env.example @@ -24,6 +24,10 @@ NODE_ENV=development # FROM_EMAIL= # REPLY_TO_EMAIL= +# Required for upgrading graphile-worker to v0.14 when graceful shutdown is impossible or impractical. +# Start the app once with this set, then unset and restart. The migration should succeed. +# FAIL_LOCKED_JOBS_ON_STARTUP=true + # CLOUD VARIABLES POSTHOG_PROJECT_KEY= PLAIN_API_KEY= From e3c95591a6b782a9cde8b582908af1a9a827b278 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 2 Oct 2023 21:52:13 +0000 Subject: [PATCH 06/27] Detect migration errors --- apps/webapp/app/entry.server.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/webapp/app/entry.server.tsx b/apps/webapp/app/entry.server.tsx index c270926697..9e3f2c3fb1 100644 --- a/apps/webapp/app/entry.server.tsx +++ b/apps/webapp/app/entry.server.tsx @@ -164,5 +164,11 @@ function logError(error: unknown, request?: Request) { ); } } + console.error(error); + + if (error instanceof Error && error.message === "division by zero") { + console.log("⚠️ Possible graphile-worker migration issue detected") + console.log("⚠️ Set FAIL_LOCKED_JOBS_FOR_MIGRATION=true if this persists") + } } From 25d64786c6139ddb017b875301e84d19af022199 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 2 Oct 2023 21:53:48 +0000 Subject: [PATCH 07/27] Rename migration env var --- .env.example | 2 +- apps/webapp/app/env.server.ts | 2 +- apps/webapp/app/services/worker.server.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 5bac3ac60d..a168668d20 100644 --- a/.env.example +++ b/.env.example @@ -26,7 +26,7 @@ NODE_ENV=development # Required for upgrading graphile-worker to v0.14 when graceful shutdown is impossible or impractical. # Start the app once with this set, then unset and restart. The migration should succeed. -# FAIL_LOCKED_JOBS_ON_STARTUP=true +# FAIL_LOCKED_JOBS_FOR_MIGRATION=true # CLOUD VARIABLES POSTHOG_PROJECT_KEY= diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index f7794d06e8..c8577e5bee 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -40,7 +40,7 @@ const EnvironmentSchema = z.object({ EXECUTION_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), WORKER_ENABLED: z.string().default("true"), EXECUTION_WORKER_ENABLED: z.string().default("true"), - FAIL_LOCKED_JOBS_ON_STARTUP: z.string().default("false"), + FAIL_LOCKED_JOBS_FOR_MIGRATION: z.string().default("false"), }); export type Environment = z.infer; diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index 4732027093..a841b4ddfb 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -111,7 +111,7 @@ if (env.NODE_ENV === "production") { } export async function init() { - if (env.FAIL_LOCKED_JOBS_ON_STARTUP === "true") { + if (env.FAIL_LOCKED_JOBS_FOR_MIGRATION === "true") { return failLockedTasks(); } From 00eade23cfb0adee563900125d8fcbf38c2d67e1 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 2 Oct 2023 21:59:37 +0000 Subject: [PATCH 08/27] Improve migration helper, don't require restart --- apps/webapp/app/services/worker.server.ts | 47 +++++++++++++---------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index a841b4ddfb..3c2c1eb073 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -112,7 +112,7 @@ if (env.NODE_ENV === "production") { export async function init() { if (env.FAIL_LOCKED_JOBS_FOR_MIGRATION === "true") { - return failLockedTasks(); + await failLockedJobsForMigration(); } if (env.WORKER_ENABLED === "true") { @@ -124,14 +124,12 @@ export async function init() { } } -async function failLockedTasks() { - console.log("⚠️ Failing all locked jobs as requested"); - console.log("⚠️ Will NOT start any workers"); +/** Helper for graphile-worker v0.14.0 migration */ +async function failLockedJobsForMigration() { + console.log("⚠️ failing locked jobs for migration"); const graphileWorkerSchema = env.WORKER_SCHEMA; - const message = "Failing all locked jobs"; - const migrationQueryResult = await prisma.$queryRawUnsafe(` SELECT id FROM graphile_worker.migrations ORDER BY id DESC LIMIT 1`); @@ -139,21 +137,23 @@ async function failLockedTasks() { const MigrationQueryResultSchema = z.array(z.object({ id: z.number() })); const migrationResults = MigrationQueryResultSchema.parse(migrationQueryResult); + if (!migrationResults.length) { - throw new Error("No migrations applied yet."); + console.log("⚠️ nothing to do, no migrations applied yet"); + console.log("⚠️ unset FAIL_LOCKED_JOBS_FOR_MIGRATION to hide these messages"); + return; } const latestMigration = migrationResults[0].id; - const separateJobQueuesTable = latestMigration > 10; - const unlockJobQueue = `, queues AS ( - UPDATE ${graphileWorkerSchema}.job_queues - SET - locked_by = null, - locked_at = null - FROM j - WHERE job_queues.id = j.job_queue_id - )`; + // the first v0.14.0 migration has ID 11 + if (latestMigration > 10) { + console.log("⚠️ nothing to do, graphile-worker already upgraded"); + console.log("⚠️ unset FAIL_LOCKED_JOBS_FOR_MIGRATION to hide these messages"); + return; + } + + const errorMessage = "Failing locked jobs for migration"; const failJobsResult = await prisma.$queryRawUnsafe>( `WITH j AS ( @@ -166,13 +166,20 @@ async function failLockedTasks() { WHERE locked_at is not null AND locked_at > now() - interval '4 hours' RETURNING * - )${separateJobQueuesTable ? unlockJobQueue : ""} + ), queues AS ( + UPDATE ${graphileWorkerSchema}.job_queues + SET + locked_by = null, + locked_at = null + FROM j + WHERE job_queues.queue_name = j.queue_name + ) SELECT * FROM j;`, - message + errorMessage ); - console.log(`⚠️ Failed ${failJobsResult.length} locked jobs`); - console.log("⚠️ Unset FAIL_LOCKED_JOBS_ON_STARTUP to enable workers"); + console.log(`⚠️ failed ${failJobsResult.length} locked jobs`); + console.log("⚠️ You can now unset FAIL_LOCKED_JOBS_FOR_MIGRATION"); } function getWorkerQueue() { From 8efb0a52a8b69a55160ba948b380dbf2df7330eb Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 3 Oct 2023 08:21:57 +0000 Subject: [PATCH 09/27] Remove restart comment for now --- .env.example | 1 - 1 file changed, 1 deletion(-) diff --git a/.env.example b/.env.example index a168668d20..e6e12aef31 100644 --- a/.env.example +++ b/.env.example @@ -25,7 +25,6 @@ NODE_ENV=development # REPLY_TO_EMAIL= # Required for upgrading graphile-worker to v0.14 when graceful shutdown is impossible or impractical. -# Start the app once with this set, then unset and restart. The migration should succeed. # FAIL_LOCKED_JOBS_FOR_MIGRATION=true # CLOUD VARIABLES From d20a49e047d6d7676e831250d5e4d624e9710073 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 3 Oct 2023 09:39:35 +0000 Subject: [PATCH 10/27] Add migration docs --- .../self-hosting/graphile-migration.mdx | 35 ++++++++++++++++++ docs/images/graphile-migration-error.png | Bin 0 -> 83026 bytes docs/mint.json | 1 + 3 files changed, 36 insertions(+) create mode 100644 docs/documentation/guides/self-hosting/graphile-migration.mdx create mode 100644 docs/images/graphile-migration-error.png diff --git a/docs/documentation/guides/self-hosting/graphile-migration.mdx b/docs/documentation/guides/self-hosting/graphile-migration.mdx new file mode 100644 index 0000000000..87ddffba33 --- /dev/null +++ b/docs/documentation/guides/self-hosting/graphile-migration.mdx @@ -0,0 +1,35 @@ +--- +title: "Graphile migration" +description: "The `graphile-worker` migration may require some manual input" +--- + +## See any warnings? + + + Graphile Migration Error + + +A warning like this may have brought you here. If it hasn't, you probably **don't** need this! + +## Semi-automatic migration steps + +This SHOULD be a safe process, but it's recommended to always have backups of your database. + +1. Set `FAIL_LOCKED_JOBS_FOR_MIGRATION=true` as you do with other env vars +2. Restart your server + +The environment variable won't have any effect if the migration was already successful. To remove additional log output, you may want to unset it again afterwards. + +## Alternative: Graceful shutdown + +The above issue only arises if there are any locked jobs when (automatic) migration is attempted. + +To prevent this, please shut your server down _gracefully_. How you do this will depend on your platform. This may involve: + +- Sending SIGTERM via the `kill` command +- Using `docker stop` or `docker-compose down` +- Equivalent methods on your managed platform + +## Problems + +If you run into any other issues, you may have to temporarily scale down to using only _a single instance_. All other instructions above still apply. \ No newline at end of file diff --git a/docs/images/graphile-migration-error.png b/docs/images/graphile-migration-error.png new file mode 100644 index 0000000000000000000000000000000000000000..6e9c6c1c2d34d0007e95d8c6d171201e00b15333 GIT binary patch literal 83026 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

Og6h`}gmkifKk2P^P?{ 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 W9F^27q&LLPBlL9+Xjf?3M*J@oae0jZ literal 0 HcmV?d00001 diff --git a/docs/mint.json b/docs/mint.json index 7d7c3bd4b5..66f51cf94a 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -197,6 +197,7 @@ "documentation/guides/self-hosting/flyio", "documentation/guides/self-hosting/render", "documentation/guides/self-hosting/supabase", + "documentation/guides/self-hosting/graphile-migration", "documentation/guides/tunneling-platform" ] }, From aab2c8b857e03bd4c136f995272f65cc1964e91e Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 3 Oct 2023 09:40:20 +0000 Subject: [PATCH 11/27] Link to migration docs --- apps/webapp/app/entry.server.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/entry.server.tsx b/apps/webapp/app/entry.server.tsx index 9e3f2c3fb1..6ed9e2a3fa 100644 --- a/apps/webapp/app/entry.server.tsx +++ b/apps/webapp/app/entry.server.tsx @@ -168,7 +168,8 @@ function logError(error: unknown, request?: Request) { console.error(error); if (error instanceof Error && error.message === "division by zero") { - console.log("⚠️ Possible graphile-worker migration issue detected") - console.log("⚠️ Set FAIL_LOCKED_JOBS_FOR_MIGRATION=true if this persists") + console.log("⚠️ possible graphile-worker migration issue detected"); + console.log("⚠️ set FAIL_LOCKED_JOBS_FOR_MIGRATION=true if this persists"); + console.log("⚠️ see: https://trigger.dev/docs/documentation/guides/self-hosting/graphile-migration"); } } From b45f9fb3821deded4c778c9558fe4c54aa56f715 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 3 Oct 2023 09:42:25 +0000 Subject: [PATCH 12/27] Lowercase --- apps/webapp/app/services/worker.server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index 3c2c1eb073..90b710b3ad 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -179,7 +179,7 @@ async function failLockedJobsForMigration() { ); console.log(`⚠️ failed ${failJobsResult.length} locked jobs`); - console.log("⚠️ You can now unset FAIL_LOCKED_JOBS_FOR_MIGRATION"); + console.log("⚠️ you can now unset FAIL_LOCKED_JOBS_FOR_MIGRATION"); } function getWorkerQueue() { From 86d6343d344927714e060a941b87ca7cc38c22b7 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 16 Oct 2023 11:19:07 +0000 Subject: [PATCH 13/27] Fix merge --- apps/webapp/app/platform/zodWorker.server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/platform/zodWorker.server.ts b/apps/webapp/app/platform/zodWorker.server.ts index a151aefe3a..0c19a90e24 100644 --- a/apps/webapp/app/platform/zodWorker.server.ts +++ b/apps/webapp/app/platform/zodWorker.server.ts @@ -379,7 +379,7 @@ export class ZodWorker { if (this.#cleanup) { cronItems.push({ - pattern: this.#cleanup.frequencyExpression, + match: this.#cleanup.frequencyExpression, identifier: CLEANUP_TASK_NAME, task: CLEANUP_TASK_NAME, options: this.#cleanup.taskOptions, @@ -388,7 +388,7 @@ export class ZodWorker { if (this.#reporter) { cronItems.push({ - pattern: "50 * * * *", // Every hour at 50 minutes past the hour + match: "50 * * * *", // Every hour at 50 minutes past the hour identifier: REPORTER_TASK_NAME, task: REPORTER_TASK_NAME, }); From 70af76a6e8068fb06c3903f946e5a32c484a2636 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 5 Oct 2023 09:57:17 +0000 Subject: [PATCH 14/27] Send batched events --- .../events/deliverBatchedEvent.server.ts | 131 ++++++++++++++++++ .../services/events/ingestSendEvent.server.ts | 60 ++++++-- apps/webapp/app/services/worker.server.ts | 11 ++ packages/core/src/schemas/api.ts | 2 + references/job-catalog/src/events.ts | 43 ++++++ 5 files changed, 236 insertions(+), 11 deletions(-) create mode 100644 apps/webapp/app/services/events/deliverBatchedEvent.server.ts diff --git a/apps/webapp/app/services/events/deliverBatchedEvent.server.ts b/apps/webapp/app/services/events/deliverBatchedEvent.server.ts new file mode 100644 index 0000000000..e0dd972f64 --- /dev/null +++ b/apps/webapp/app/services/events/deliverBatchedEvent.server.ts @@ -0,0 +1,131 @@ +import type { EventDispatcher, EventRecord } from "@trigger.dev/database"; +import type { EventFilter } from "@trigger.dev/core"; +import { EventFilterSchema, eventFilterMatches } from "@trigger.dev/core"; +import { $transaction, PrismaClientOrTransaction, prisma } from "~/db.server"; +import { logger } from "~/services/logger.server"; +import { workerQueue } from "../worker.server"; + +export class DeliverBatchedEventService { + #prismaClient: PrismaClientOrTransaction; + + constructor(prismaClient: PrismaClientOrTransaction = prisma) { + this.#prismaClient = prismaClient; + } + + public async call(ids: string[]) { + await $transaction( + this.#prismaClient, + async (tx) => { + const eventRecords = await tx.eventRecord.findMany({ + where: { + id: { in: ids }, + }, + include: { + environment: { + include: { + organization: true, + project: true, + }, + }, + }, + }); + + for (const eventRecord of eventRecords) { + const possibleEventDispatchers = await tx.eventDispatcher.findMany({ + where: { + environmentId: eventRecord.environmentId, + event: { + has: eventRecord.name, + }, + source: eventRecord.source, + enabled: true, + manual: false, + }, + }); + + logger.debug("Found possible event dispatchers", { + possibleEventDispatchers, + eventRecord: eventRecord.id, + }); + + const matchingEventDispatchers = possibleEventDispatchers.filter((eventDispatcher) => + this.#evaluateEventRule(eventDispatcher, eventRecord) + ); + + if (matchingEventDispatchers.length === 0) { + logger.debug("No matching event dispatchers", { + eventRecord: eventRecord.id, + }); + + return; + } + + logger.debug("Found matching event dispatchers", { + matchingEventDispatchers, + eventRecord: eventRecord.id, + }); + + await Promise.all( + matchingEventDispatchers.map((eventDispatcher) => + workerQueue.enqueue( + "events.invokeDispatcher", + { + id: eventDispatcher.id, + eventRecordId: eventRecord.id, + }, + { tx } + ) + ) + ); + + await tx.eventRecord.update({ + where: { + id: eventRecord.id, + }, + data: { + deliveredAt: new Date(), + }, + }); + } + }, + { timeout: 10000 } + ); + } + + #evaluateEventRule(dispatcher: EventDispatcher, eventRecord: EventRecord): boolean { + if (!dispatcher.payloadFilter && !dispatcher.contextFilter) { + return true; + } + + const payloadFilter = EventFilterSchema.safeParse(dispatcher.payloadFilter ?? {}); + + const contextFilter = EventFilterSchema.safeParse(dispatcher.contextFilter ?? {}); + + if (!payloadFilter.success || !contextFilter.success) { + logger.error("Invalid event filter", { + payloadFilter, + contextFilter, + }); + return false; + } + + const eventMatcher = new EventMatcher(eventRecord); + + return eventMatcher.matches({ + payload: payloadFilter.data, + context: contextFilter.data, + }); + } +} + +export class EventMatcher { + event: EventRecord; + + constructor(event: EventRecord) { + this.event = event; + } + + public matches(filter: EventFilter) { + return eventFilterMatches(this.event, filter); + } +} diff --git a/apps/webapp/app/services/events/ingestSendEvent.server.ts b/apps/webapp/app/services/events/ingestSendEvent.server.ts index 9e4a254bba..fb2a411b07 100644 --- a/apps/webapp/app/services/events/ingestSendEvent.server.ts +++ b/apps/webapp/app/services/events/ingestSendEvent.server.ts @@ -10,6 +10,7 @@ type UpdateEventInput = { existingEventLog: EventRecord; reqEvent: RawEvent; deliverAt?: Date; + batchKey?: string; }; type CreateEventInput = { @@ -19,6 +20,7 @@ type CreateEventInput = { deliverAt?: Date; sourceContext?: { id: string; metadata?: any }; externalAccount?: ExternalAccount; + batchKey?: string; }; const EVENT_UPDATE_THRESHOLD_WINDOW_IN_MSECS = 5 * 1000; // 5 seconds @@ -81,7 +83,13 @@ export class IngestSendEvent { }); const eventLog = await (existingEventLog - ? this.updateEvent({ tx, existingEventLog, reqEvent: event, deliverAt }) + ? this.updateEvent({ + tx, + existingEventLog, + reqEvent: event, + deliverAt, + batchKey: options?.batchKey, + }) : this.createEvent({ tx, event, @@ -89,6 +97,7 @@ export class IngestSendEvent { deliverAt, sourceContext, externalAccount, + batchKey: options?.batchKey, })); return eventLog; @@ -116,6 +125,7 @@ export class IngestSendEvent { deliverAt, sourceContext, externalAccount, + batchKey, }: CreateEventInput) { const eventLog = await tx.eventRecord.create({ data: { @@ -139,7 +149,13 @@ export class IngestSendEvent { return eventLog; } - private async updateEvent({ tx, existingEventLog, reqEvent, deliverAt }: UpdateEventInput) { + private async updateEvent({ + tx, + existingEventLog, + reqEvent, + deliverAt, + batchKey, + }: UpdateEventInput) { if (!this.shouldUpdateEvent(existingEventLog)) { logger.debug(`not updating event for event id: ${existingEventLog.eventId}`); return existingEventLog; @@ -159,7 +175,7 @@ export class IngestSendEvent { }, }); - await this.enqueueWorkerEvent(tx, updatedEventLog); + await this.enqueueWorkerEvent(tx, updatedEventLog, batchKey); return updatedEventLog; } @@ -170,16 +186,38 @@ export class IngestSendEvent { return eventLog.deliverAt >= thresholdTime; } - private async enqueueWorkerEvent(tx: PrismaClientOrTransaction, eventLog: EventRecord) { + private async enqueueWorkerEvent( + tx: PrismaClientOrTransaction, + eventLog: EventRecord, + batchKey?: string + ) { if (this.deliverEvents) { // Produce a message to the event bus - await workerQueue.enqueue( - "deliverEvent", - { - id: eventLog.id, - }, - { runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` } - ); + if (batchKey) { + await workerQueue.enqueue( + "deliverBatchedEvent", + // Batch jobs require array payloads. + [ + { + id: eventLog.id, + }, + ], + { + runAt: eventLog.deliverAt, + tx, + jobKey: `event:${eventLog.environmentId}:${batchKey}`, + jobKeyMode: "preserve_run_at", + } + ); + } else { + await workerQueue.enqueue( + "deliverEvent", + { + id: eventLog.id, + }, + { runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` } + ); + } } } } diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index ecdfd8a97f..2f14cb2090 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -9,6 +9,7 @@ import { IndexEndpointService } from "./endpoints/indexEndpoint.server"; import { PerformEndpointIndexService } from "./endpoints/performEndpointIndexService"; import { RecurringEndpointIndexService } from "./endpoints/recurringEndpointIndex.server"; import { DeliverEventService } from "./events/deliverEvent.server"; +import { DeliverBatchedEventService } from "./events/deliverBatchedEvent.server"; import { InvokeDispatcherService } from "./events/invokeDispatcher.server"; import { integrationAuthRepository } from "./externalApis/integrationAuthRepository.server"; import { IntegrationConnectionCreatedService } from "./externalApis/integrationConnectionCreated.server"; @@ -62,6 +63,7 @@ const workerCatalog = { ]) ), deliverEvent: z.object({ id: z.string() }), + deliverBatchedEvent: z.array(z.object({ id: z.string() })), "events.invokeDispatcher": z.object({ id: z.string(), eventRecordId: z.string(), @@ -366,6 +368,15 @@ function getWorkerQueue() { await service.call(payload.id); }, }, + deliverBatchedEvent: { + priority: 0, // smaller number = higher priority + maxAttempts: 5, + handler: async (payload, job) => { + const service = new DeliverBatchedEventService(); + + await service.call(payload.map((p) => p.id)); + }, + }, refreshOAuthToken: { priority: 8, // smaller number = higher priority maxAttempts: 7, diff --git a/packages/core/src/schemas/api.ts b/packages/core/src/schemas/api.ts index 4e16cbfc55..7d32a2e343 100644 --- a/packages/core/src/schemas/api.ts +++ b/packages/core/src/schemas/api.ts @@ -408,6 +408,8 @@ export type ApiEventLog = z.infer; /** Options to control the delivery of the event */ export const SendEventOptionsSchema = z.object({ + /** An optional string to enable event batching. Events with a shared `batchKey` will be delivered together as an array of payloads. */ + batchKey: z.string().optional(), /** An optional Date when you want the event to trigger Jobs. The event will be sent to the platform immediately but won't be acted upon until the specified time. */ diff --git a/references/job-catalog/src/events.ts b/references/job-catalog/src/events.ts index 7bd9d9a088..3973bddfa0 100644 --- a/references/job-catalog/src/events.ts +++ b/references/job-catalog/src/events.ts @@ -73,4 +73,47 @@ client.defineJob({ }, }); +client.defineJob({ + id: "send-batched-event", + name: "Send Batched Event", + version: "1.0.0", + trigger: eventTrigger({ + name: "batch.send", + }), + run: async (payload, io, ctx) => { + // unbatched + await io.sendEvent("unbatched", + { name: "new.message", payload: { message: "Not batching this!" } }, + { deliverAfter: 5 } + ); + + // batched + await io.sendEvent("batched-1", + { name: "new.message", payload: { message: "Hey there!" } }, + { batchKey: "user123", deliverAfter: 10 } + ); + await io.sendEvent("batched-2", + { name: "new.message", payload: { message: "It's me!" } }, + { batchKey: "user123" } // deliverAfter: 10 - should be preserved + ); + await io.sendEvent("batched-3", + { name: "new.message", payload: { message: "Where are you?" } }, + { batchKey: "user123" } // deliverAfter: 10 - should be preserved + ); + }, +}); + +client.defineJob({ + id: "receive-batched-event", + name: "Receive Batched Event", + version: "1.0.0", + trigger: eventTrigger({ + name: "new.message", + schema: z.object({ message: z.string() }), + }), + run: async (payload, io, ctx) => { + return payload; + }, +}); + createExpressServer(client); From 21a3c47a7f9fbf98a2416ac0e68851deb13e11f6 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:51:56 +0000 Subject: [PATCH 15/27] io.sendEvent props --- packages/trigger-sdk/src/io.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/trigger-sdk/src/io.ts b/packages/trigger-sdk/src/io.ts index b4dacfc5e0..1a4c0af5d3 100644 --- a/packages/trigger-sdk/src/io.ts +++ b/packages/trigger-sdk/src/io.ts @@ -330,6 +330,9 @@ export class IO { text: event.name, }, ...(event?.id ? [{ label: "ID", text: event.id }] : []), + ...(options?.batchKey ? [{ label: "Batch Key", text: options.batchKey }] : []), + ...(options?.deliverAfter ? [{ label: "Deliver After", text: String(options.deliverAfter) }] : []), + ...(options?.deliverAt ? [{ label: "Deliver At", text: options.deliverAt.toISOString() }] : []), ], } ); From 11be80767867a1dece08146cb13cfc622efdfa8b Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:19:17 +0000 Subject: [PATCH 16/27] JSON editor cursor position and empty objects --- .../webapp/app/components/code/JSONEditor.tsx | 33 ++++++++++++++++--- .../route.tsx | 3 +- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/apps/webapp/app/components/code/JSONEditor.tsx b/apps/webapp/app/components/code/JSONEditor.tsx index 7d5ef0c237..5cebb9f810 100644 --- a/apps/webapp/app/components/code/JSONEditor.tsx +++ b/apps/webapp/app/components/code/JSONEditor.tsx @@ -1,5 +1,6 @@ import { json as jsonLang } from "@codemirror/lang-json"; import type { ViewUpdate } from "@codemirror/view"; +import type { Text } from "@codemirror/state"; import { CheckIcon, ClipboardIcon } from "@heroicons/react/20/solid"; import type { ReactCodeMirrorProps, UseCodeMirror } from "@uiw/react-codemirror"; import { useCodeMirror } from "@uiw/react-codemirror"; @@ -68,6 +69,7 @@ export function JSONEditor(opts: JSONEditorProps) { theme: darkTheme(), indentWithTab: false, basicSetup, + selection: getDefaultSelection(defaultValue), onChange, onUpdate, }; @@ -83,10 +85,16 @@ export function JSONEditor(opts: JSONEditorProps) { //if the defaultValue changes update the editor useEffect(() => { if (view !== undefined) { - if (view.state.doc.toString() === defaultValue) return; - view.dispatch({ - changes: { from: 0, to: view.state.doc.length, insert: defaultValue }, - }); + if (view.state.doc.toString() !== defaultValue) { + view.dispatch({ + changes: { from: 0, to: view.state.doc.length, insert: defaultValue }, + selection: getDefaultSelection(defaultValue), + }); + } else { + view.dispatch({ + selection: getDefaultSelection(defaultValue), + }); + } } }, [defaultValue, view]); @@ -150,3 +158,20 @@ export function JSONEditor(opts: JSONEditorProps) { ); } + +function isMultiline(content: string | Text) { + if (typeof content === "string") { + return content.includes("\n"); + } else { + return content.lines > 1; + } +} + +/** For multiline content, gets end of penultimate line. Otherwise, position `0`. */ +function getDefaultSelection(content: string | Text) { + if (!isMultiline(content)) { + return { anchor: 0 }; + } else { + return { anchor: content.length > 2 ? content.length - 2 : 0 }; + } +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx index d109230a3d..9051c20737 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx @@ -117,7 +117,7 @@ export const handle: Handle = { breadcrumb: (match) => , }; -const startingJson = "{\n\n}"; +const startingJson = "{\n \n}"; export default function Page() { const { environments, runs, examples } = useTypedLoaderData(); @@ -202,7 +202,6 @@ export default function Page() { //deselect the example if it's been edited if (selectedCodeSampleId) { if (v !== selectedCodeSample) { - setDefaultJson(v); setSelectedCodeSampleId(undefined); } } From d934de4cfbe5cce5de471936c3a66a18961f8fcb Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:20:28 +0000 Subject: [PATCH 17/27] Fix batched worker enqueue --- apps/webapp/app/services/events/ingestSendEvent.server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/services/events/ingestSendEvent.server.ts b/apps/webapp/app/services/events/ingestSendEvent.server.ts index fb2a411b07..579461d944 100644 --- a/apps/webapp/app/services/events/ingestSendEvent.server.ts +++ b/apps/webapp/app/services/events/ingestSendEvent.server.ts @@ -144,7 +144,7 @@ export class IngestSendEvent { }, }); - await this.enqueueWorkerEvent(tx, eventLog); + await this.enqueueWorkerEvent(tx, eventLog, batchKey); return eventLog; } From cb43a0d73c67aa12da7e07e08f9f8bcdbc14398e Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:25:49 +0000 Subject: [PATCH 18/27] Airtable error handling --- integrations/airtable/src/index.ts | 38 +++++++++++++++++- integrations/airtable/src/webhooks.ts | 58 ++++++++++++++++++++++----- 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/integrations/airtable/src/index.ts b/integrations/airtable/src/index.ts index bc9ff24026..bfb8d405de 100644 --- a/integrations/airtable/src/index.ts +++ b/integrations/airtable/src/index.ts @@ -10,7 +10,7 @@ import { type RunTaskOptions, type TriggerIntegration, } from "@trigger.dev/sdk"; -import AirtableSDK from "airtable"; +import AirtableSDK, { Error as AirtableApiError } from "airtable"; import { Base } from "./base"; import { Webhooks, createWebhookEventSource } from "./webhooks"; @@ -105,7 +105,7 @@ export class Airtable implements TriggerIntegration { ...(options ?? {}), connectionKey: this._connectionKey, }, - errorCallback + errorCallback ?? onError ); } @@ -130,3 +130,37 @@ export class Airtable implements TriggerIntegration { return new Webhooks(this.runTask.bind(this)); } } + +function isAirtableApiError(error: unknown): error is AirtableApiError { + if (typeof error !== "object" || error === null) { + return false; + } + + const airtableError = error as AirtableApiError; + + return ( + typeof airtableError.error === "string" && + typeof airtableError.message === "string" && + typeof airtableError.statusCode === "number" + ); +} + +export function onError(error: unknown): ReturnType { + if (!isAirtableApiError(error)) { + return; + } + + if (error.statusCode === 429) { + // see: https://airtable.com/developers/web/api/rate-limits + return { + retryAt: new Date(Date.now() + 30 * 1000), + }; + } + + if (error.statusCode >= 400 && error.statusCode < 500) { + // see: https://airtable.com/developers/web/api/errors#user-error-codes + return { + skipRetrying: true, + }; + } +} diff --git a/integrations/airtable/src/webhooks.ts b/integrations/airtable/src/webhooks.ts index 786d9ef7e4..2ff4de7ef1 100644 --- a/integrations/airtable/src/webhooks.ts +++ b/integrations/airtable/src/webhooks.ts @@ -6,7 +6,7 @@ import { IntegrationTaskKey, Logger, } from "@trigger.dev/sdk"; -import AirtableSDK from "airtable"; +import AirtableSDK, { Error as AirtableApiError } from "airtable"; import { z } from "zod"; import * as events from "./events"; import { Airtable, AirtableRunTask } from "./index"; @@ -46,6 +46,31 @@ type WebhookSpecification = { }; }; +const AirtableErrorBodySchema = z + .union([ + z.object({ + error: z.string(), + }), + z.object({ + error: z.object({ + type: z.string(), + message: z.string().optional(), + }), + }), + ]) + .transform((body) => { + if (typeof body.error === "string") { + return { + type: body.error, + }; + } else { + return { + type: body.error.type, + message: body.error.message, + }; + } + }); + const apiUrl = "https://api.airtable.com/v0/bases"; export class Webhooks { @@ -85,14 +110,7 @@ export class Webhooks { }); if (!response.ok) { - const errorText = await response - .text() - .then((t) => t) - .catch((e) => "No body"); - - throw new Error( - `Failed to create webhook: ${response.status} ${response.statusText}\n${errorText}` - ); + await handleWebhookError(response, "WEBHOOK_CREATE") } const webhook = await response.json(); @@ -123,7 +141,7 @@ export class Webhooks { }); if (!response.ok) { - throw new Error(`Failed to list webhooks: ${response.statusText}`); + await handleWebhookError(response, "WEBHOOK_LIST") } const webhook = await response.json(); @@ -153,7 +171,7 @@ export class Webhooks { }); if (!response.ok) { - throw new Error(`Failed to delete webhook: ${response.statusText}`); + await handleWebhookError(response, "WEBHOOK_DELETE") } }, { @@ -458,3 +476,21 @@ async function getPayload( const webhook = await response.json(); return ListWebhooksResponseSchema.parse(webhook); } + +async function handleWebhookError(response: Response, errorType: string) { + const rawErrorBody = await response.json(); + + const parsedErrorBody = AirtableErrorBodySchema.safeParse(rawErrorBody); + + if (!parsedErrorBody.success) { + throw new AirtableApiError( + `${errorType}_PARSE_ERROR`, + `${response.statusText}:\n${rawErrorBody}`, + response.status + ); + } + + const { type, message } = parsedErrorBody.data; + + throw new AirtableApiError(type, message ?? response.statusText, response.status); +} \ No newline at end of file From 7dbc93d3249f572c89c466a2b3420b15a43ab73a Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:38:23 +0000 Subject: [PATCH 19/27] New DB columns --- .../20231019123537_add_batched_events/migration.sql | 6 ++++++ packages/database/prisma/schema.prisma | 5 +++++ 2 files changed, 11 insertions(+) create mode 100644 packages/database/prisma/migrations/20231019123537_add_batched_events/migration.sql diff --git a/packages/database/prisma/migrations/20231019123537_add_batched_events/migration.sql b/packages/database/prisma/migrations/20231019123537_add_batched_events/migration.sql new file mode 100644 index 0000000000..d5ffe12e0e --- /dev/null +++ b/packages/database/prisma/migrations/20231019123537_add_batched_events/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "EventDispatcher" ADD COLUMN "batch" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "JobRun" ADD COLUMN "eventIds" TEXT[], +ADD COLUMN "payload" JSONB; diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index 7f1d219181..919cfff6e8 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -613,6 +613,7 @@ model EventDispatcher { payloadFilter Json? contextFilter Json? manual Boolean @default(false) + batch Boolean @default(false) dispatchableId String dispatchable Json @@ -686,6 +687,10 @@ model JobRun { event EventRecord @relation(fields: [eventId], references: [id], onDelete: Cascade, onUpdate: Cascade) eventId String + eventIds String[] + + payload Json? + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) organizationId String From 43b6462c5290c90c04f1bfa9a0402c8350302d7c Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:40:20 +0000 Subject: [PATCH 20/27] Add db push script --- packages/database/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/database/package.json b/packages/database/package.json index 6c82f45882..4755ee2d29 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -16,6 +16,7 @@ "db:migrate:dev": "prisma migrate dev", "db:migrate:dev:create": "prisma migrate dev --create-only", "db:migrate:deploy": "prisma migrate deploy", + "db:push": "prisma db push", "db:studio": "prisma studio", "typecheck": "tsc --noEmit" } From cb654691ed428a04a25beaec9d0b06503928b3eb Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:45:29 +0000 Subject: [PATCH 21/27] Add batching options to SDK --- packages/core/src/schemas/api.ts | 10 ++++++++++ packages/trigger-sdk/src/triggerClient.ts | 11 ++++++++--- packages/trigger-sdk/src/triggers/eventTrigger.ts | 5 +++++ packages/trigger-sdk/src/triggers/externalSource.ts | 4 ++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/core/src/schemas/api.ts b/packages/core/src/schemas/api.ts index 7d32a2e343..3a44224339 100644 --- a/packages/core/src/schemas/api.ts +++ b/packages/core/src/schemas/api.ts @@ -123,6 +123,13 @@ export const TriggerSourceSchema = z.object({ const HttpSourceResponseMetadataSchema = DeserializedJsonSchema; export type HttpSourceResponseMetadata = z.infer; +const HttpSourceResponseOptionsSchema = z.object({ + batchKey: z.string().optional(), + deliverAt: z.coerce.date().optional(), + deliverAfter: z.number().int().optional(), +}) +export type HttpSourceResponseOptions = z.infer; + export const HandleTriggerSourceSchema = z.object({ key: z.string(), secret: z.string(), @@ -455,6 +462,7 @@ export type RunSourceContext = z.infer; export const RunJobBodySchema = z.object({ event: ApiEventLogSchema, + payload: DeserializedJsonSchema, job: z.object({ id: z.string(), version: z.string(), @@ -565,6 +573,7 @@ export type RunJobResponse = z.infer; export const PreprocessRunBodySchema = z.object({ event: ApiEventLogSchema, + payload: DeserializedJsonSchema, job: z.object({ id: z.string(), version: z.string(), @@ -780,6 +789,7 @@ export const HttpSourceResponseSchema = z.object({ response: NormalizedResponseSchema, events: z.array(RawEventSchema), metadata: HttpSourceResponseMetadataSchema.optional(), + options: HttpSourceResponseOptionsSchema.optional(), }); export const RegisterTriggerBodySchemaV1 = z.object({ diff --git a/packages/trigger-sdk/src/triggerClient.ts b/packages/trigger-sdk/src/triggerClient.ts index b1766f1642..dec83be8c2 100644 --- a/packages/trigger-sdk/src/triggerClient.ts +++ b/packages/trigger-sdk/src/triggerClient.ts @@ -8,6 +8,7 @@ import { HandleTriggerSource, HttpSourceRequestHeadersSchema, HttpSourceResponseMetadata, + HttpSourceResponseOptions, IndexEndpointResponse, InitializeTriggerBodySchema, IntegrationConfig, @@ -108,6 +109,7 @@ export class TriggerClient { events: Array; response?: NormalizedResponse; metadata?: HttpSourceResponseMetadata; + options?: HttpSourceResponseOptions; } | void> > = {}; #registeredDynamicTriggers: Record< @@ -406,7 +408,7 @@ export class TriggerClient { metadata: inputMetadata, }; - const { response, events, metadata } = await this.#handleHttpSourceRequest( + const { response, events, metadata, options } = await this.#handleHttpSourceRequest( source, sourceRequest ); @@ -417,6 +419,7 @@ export class TriggerClient { events, response, metadata, + options, }, headers: this.#standardResponseHeaders, }; @@ -677,7 +680,7 @@ export class TriggerClient { async #preprocessRun(body: PreprocessRunBody, job: Job>, any>) { const context = this.#createPreprocessRunContext(body); - const parsedPayload = job.trigger.event.parsePayload(body.event.payload ?? {}); + const parsedPayload = job.trigger.event.parsePayload(body.payload ?? body.event.payload ?? {}); const properties = job.trigger.event.runProperties?.(parsedPayload) ?? []; @@ -740,7 +743,7 @@ export class TriggerClient { try { const output = await runLocalStorage.runWith({ io, ctx: context }, () => { return job.options.run( - job.trigger.event.parsePayload(body.event.payload ?? {}), + job.trigger.event.parsePayload(body.payload ?? body.event.payload ?? {}), ioWithConnections, context ); @@ -867,6 +870,7 @@ export class TriggerClient { response: NormalizedResponse; events: SendEvent[]; metadata?: HttpSourceResponseMetadata; + options?: HttpSourceResponseOptions; }> { this.#internalLogger.debug("Handling HTTP source request", { source, @@ -962,6 +966,7 @@ export class TriggerClient { }, }, metadata: results.metadata, + options: results.options, }; } diff --git a/packages/trigger-sdk/src/triggers/eventTrigger.ts b/packages/trigger-sdk/src/triggers/eventTrigger.ts index 6c2fc68b5c..16eca32dfa 100644 --- a/packages/trigger-sdk/src/triggers/eventTrigger.ts +++ b/packages/trigger-sdk/src/triggers/eventTrigger.ts @@ -10,6 +10,7 @@ type EventTriggerOptions> = name?: string | string[]; source?: string; filter?: EventFilter; + batch?: boolean; }; export class EventTrigger> @@ -25,6 +26,7 @@ export class EventTrigger> return { type: "static", title: this.#options.name ?? this.#options.event.title, + batch: !!this.#options.batch, rule: { event: this.#options.name ?? this.#options.event.name, source: this.#options.source ?? "trigger.dev", @@ -74,6 +76,8 @@ type TriggerOptions = { * ``` */ filter?: EventFilter; + /** Receive events as an array of payloads. (Will only receive more than one event at a time if events are also sent with batching enabled.) */ + batch?: boolean; examples?: EventSpecificationExample[]; }; @@ -87,6 +91,7 @@ export function eventTrigger( return new EventTrigger({ name: options.name, filter: options.filter, + batch: options.batch, event: { name: options.name, title: "Event", diff --git a/packages/trigger-sdk/src/triggers/externalSource.ts b/packages/trigger-sdk/src/triggers/externalSource.ts index 0100656408..aa18e54410 100644 --- a/packages/trigger-sdk/src/triggers/externalSource.ts +++ b/packages/trigger-sdk/src/triggers/externalSource.ts @@ -3,6 +3,7 @@ import { EventFilter, HandleTriggerSource, HttpSourceResponseMetadata, + HttpSourceResponseOptions, Logger, NormalizedResponse, RegisterTriggerSource, @@ -136,6 +137,7 @@ type HandlerFunction< events: SendEvent[]; response?: NormalizedResponse; metadata?: HttpSourceResponseMetadata; + options?: HttpSourceResponseOptions; } | void>; type KeyFunction = (params: TParams) => string; @@ -269,6 +271,7 @@ export type ExternalSourceTriggerOptions< source: TEventSource; params: ExternalSourceParams; options: TriggerOptionRecord; + batch?: boolean; }; export class ExternalSourceTrigger< @@ -286,6 +289,7 @@ export class ExternalSourceTrigger< return { type: "static", title: "External Source", + batch: !!this.options.batch, rule: { event: this.event.name, payload: deepMergeFilters( From c4141dc666087fd80d8e5e4c282d1873aa814bbc Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:51:18 +0000 Subject: [PATCH 22/27] Batch without touching prior JobRun data --- .../events/deliverBatchedEvent.server.ts | 145 +++++++++++++----- .../services/events/deliverEvent.server.ts | 2 +- .../events/invokeDispatcher.server.ts | 26 +++- .../app/services/jobs/registerJob.server.ts | 2 + .../app/services/jobs/testJob.server.ts | 2 +- .../app/services/runs/createRun.server.ts | 40 ++++- .../runs/performRunExecutionV2.server.ts | 3 + apps/webapp/app/services/runs/reRun.server.ts | 2 +- .../deliverHttpSourceRequest.server.ts | 5 +- apps/webapp/app/services/worker.server.ts | 4 +- 10 files changed, 176 insertions(+), 55 deletions(-) diff --git a/apps/webapp/app/services/events/deliverBatchedEvent.server.ts b/apps/webapp/app/services/events/deliverBatchedEvent.server.ts index e0dd972f64..2f530bfcdb 100644 --- a/apps/webapp/app/services/events/deliverBatchedEvent.server.ts +++ b/apps/webapp/app/services/events/deliverBatchedEvent.server.ts @@ -30,14 +30,53 @@ export class DeliverBatchedEventService { }, }); - for (const eventRecord of eventRecords) { + if (!eventRecords.length) { + throw new Error("No event records found."); + } + + const environmentId = eventRecords[0].environmentId; + + if (!eventRecords.every((event) => event.environmentId === environmentId)) { + throw new Error("Cross-environment batched events should not exist."); + } + + const unique = (val: unknown, i: number, array: unknown[]) => array.indexOf(val) === i; + + const uniqueEventNames = eventRecords.map((event) => event.name).filter(unique); + const uniqueEventSources = eventRecords.map((event) => event.source).filter(unique); + + const nameSourceCombinations: { name: string; source: string }[] = []; + + for (let nameIndex = 0; nameIndex < uniqueEventNames.length; nameIndex++) { + for (let sourceIndex = 0; sourceIndex < uniqueEventSources.length; sourceIndex++) { + nameSourceCombinations.push({ + name: uniqueEventNames[nameIndex], + source: uniqueEventSources[sourceIndex], + }); + } + } + + type InvocableDispatcher = { dispatcherId: string; eventRecordIds: string[] }; + + const batchDispatchersToInvoke: InvocableDispatcher[] = []; + const nonBatchDispatchersToInvoke: InvocableDispatcher[] = []; + + for (const combination of nameSourceCombinations) { + const matchingEvents = eventRecords.filter( + (event) => event.name === combination.name && event.source === combination.source + ); + + if (!matchingEvents.length) { + continue; + } + const possibleEventDispatchers = await tx.eventDispatcher.findMany({ where: { - environmentId: eventRecord.environmentId, + environmentId, event: { - has: eventRecord.name, + has: combination.name, }, - source: eventRecord.source, + source: combination.source, enabled: true, manual: false, }, @@ -45,48 +84,84 @@ export class DeliverBatchedEventService { logger.debug("Found possible event dispatchers", { possibleEventDispatchers, - eventRecord: eventRecord.id, + eventRecords: matchingEvents.map((event) => event.id), }); - const matchingEventDispatchers = possibleEventDispatchers.filter((eventDispatcher) => - this.#evaluateEventRule(eventDispatcher, eventRecord) - ); - - if (matchingEventDispatchers.length === 0) { - logger.debug("No matching event dispatchers", { - eventRecord: eventRecord.id, - }); - - return; + // filter events that match dispatcher filters + for (const eventDispatcher of possibleEventDispatchers) { + const filteredEvents = matchingEvents.filter((event) => + this.#evaluateEventRule(eventDispatcher, event) + ); + + // don't invoke dispatchers without any events + if (!filteredEvents.length) { + continue; + } + + const invocableDispatcher = { + dispatcherId: eventDispatcher.id, + eventRecordIds: filteredEvents.map((event) => event.id), + }; + + if (eventDispatcher.batch) { + batchDispatchersToInvoke.push(invocableDispatcher); + } else { + nonBatchDispatchersToInvoke.push(invocableDispatcher); + } } + } + + const eventRecordIds = eventRecords.map((event) => event.id); - logger.debug("Found matching event dispatchers", { - matchingEventDispatchers, - eventRecord: eventRecord.id, + if (!batchDispatchersToInvoke.length && !nonBatchDispatchersToInvoke.length) { + logger.debug("No matching event dispatchers", { + eventRecords: eventRecordIds, }); - await Promise.all( - matchingEventDispatchers.map((eventDispatcher) => - workerQueue.enqueue( + return; + } + + logger.debug("Found matching batch event dispatchers", { batchDispatchersToInvoke }); + + await Promise.all( + batchDispatchersToInvoke.map((dispatcher) => { + return workerQueue.enqueue( + "events.invokeDispatcher", + { + id: dispatcher.dispatcherId, + eventRecordIds: dispatcher.eventRecordIds, + }, + { tx } + ); + }) + ); + + logger.debug("Found matching non-batch event dispatchers", { nonBatchDispatchersToInvoke }); + + await Promise.all( + nonBatchDispatchersToInvoke.map(async (dispatcher) => { + // sequentially enqueue single events to preserve order + for (const eventRecordId of dispatcher.eventRecordIds) { + await workerQueue.enqueue( "events.invokeDispatcher", { - id: eventDispatcher.id, - eventRecordId: eventRecord.id, + id: dispatcher.dispatcherId, + eventRecordIds: [eventRecordId], }, { tx } - ) - ) - ); + ); + } + }) + ); - await tx.eventRecord.update({ - where: { - id: eventRecord.id, - }, - data: { - deliveredAt: new Date(), - }, - }); - } + await tx.eventRecord.updateMany({ + where: { + id: { in: eventRecordIds }, + }, + data: { + deliveredAt: new Date(), + }, + }); }, { timeout: 10000 } ); diff --git a/apps/webapp/app/services/events/deliverEvent.server.ts b/apps/webapp/app/services/events/deliverEvent.server.ts index 32e13d7212..6e9113fef5 100644 --- a/apps/webapp/app/services/events/deliverEvent.server.ts +++ b/apps/webapp/app/services/events/deliverEvent.server.ts @@ -70,7 +70,7 @@ export class DeliverEventService { "events.invokeDispatcher", { id: eventDispatcher.id, - eventRecordId: eventRecord.id, + eventRecordIds: [eventRecord.id], }, { tx } ) diff --git a/apps/webapp/app/services/events/invokeDispatcher.server.ts b/apps/webapp/app/services/events/invokeDispatcher.server.ts index f43412d19a..0440e27abf 100644 --- a/apps/webapp/app/services/events/invokeDispatcher.server.ts +++ b/apps/webapp/app/services/events/invokeDispatcher.server.ts @@ -26,7 +26,7 @@ export class InvokeDispatcherService { this.#prismaClient = prismaClient; } - public async call(id: string, eventRecordId: string) { + public async call(id: string, eventRecordIds: string[]) { const eventDispatcher = await this.#prismaClient.eventDispatcher.findUniqueOrThrow({ where: { id, @@ -49,15 +49,28 @@ export class InvokeDispatcherService { return; } - const eventRecord = await this.#prismaClient.eventRecord.findUniqueOrThrow({ + const foundEventRecords = await this.#prismaClient.eventRecord.findMany({ where: { - id: eventRecordId, + id: { in: eventRecordIds }, }, }); + if (!foundEventRecords.length) { + throw new Error("No event records found."); + } + + if (eventRecordIds.length !== foundEventRecords.length) { + logger.warn("Event record counts do not match", { + expected: eventRecordIds.length, + found: foundEventRecords.length, + }); + } + + const foundEventRecordIds = foundEventRecords.map((event) => event.id); + logger.debug("Invoking event dispatcher", { eventDispatcher, - eventRecord: eventRecord.id, + eventRecords: foundEventRecordIds, }); const dispatchable = DispatchableSchema.safeParse(eventDispatcher.dispatchable); @@ -85,10 +98,11 @@ export class InvokeDispatcherService { const createRunService = new CreateRunService(this.#prismaClient); await createRunService.call({ - eventId: eventRecord.id, + eventIds: foundEventRecordIds, job: jobVersion.job, version: jobVersion, environment: eventDispatcher.environment, + isBatched: eventDispatcher.batch, }); break; @@ -135,7 +149,7 @@ export class InvokeDispatcherService { const createRunService = new CreateRunService(this.#prismaClient); await createRunService.call({ - eventId: eventRecord.id, + eventIds: foundEventRecordIds, job: job, version: latestJobVersion, environment: eventDispatcher.environment, diff --git a/apps/webapp/app/services/jobs/registerJob.server.ts b/apps/webapp/app/services/jobs/registerJob.server.ts index 8e75e5376c..e2e3135aa3 100644 --- a/apps/webapp/app/services/jobs/registerJob.server.ts +++ b/apps/webapp/app/services/jobs/registerJob.server.ts @@ -331,6 +331,7 @@ export class RegisterJobService { id: jobVersion.id, }, dispatchableId: job.id, + batch: !!trigger.batch, }, update: { event: @@ -343,6 +344,7 @@ export class RegisterJobService { id: jobVersion.id, }, enabled: true, + batch: !!trigger.batch, }, }); diff --git a/apps/webapp/app/services/jobs/testJob.server.ts b/apps/webapp/app/services/jobs/testJob.server.ts index ab95d10856..d96cbd5cf5 100644 --- a/apps/webapp/app/services/jobs/testJob.server.ts +++ b/apps/webapp/app/services/jobs/testJob.server.ts @@ -101,7 +101,7 @@ export class TestJobService { return await createRunService.call({ environment, - eventId: eventLog.id, + eventIds: [eventLog.id], job: version.job, version, }); diff --git a/apps/webapp/app/services/runs/createRun.server.ts b/apps/webapp/app/services/runs/createRun.server.ts index 7c10897726..ec6bbc8e77 100644 --- a/apps/webapp/app/services/runs/createRun.server.ts +++ b/apps/webapp/app/services/runs/createRun.server.ts @@ -3,6 +3,7 @@ import { $transaction, PrismaClientOrTransaction } from "~/db.server"; import { prisma } from "~/db.server"; import { workerQueue } from "~/services/worker.server"; import type { AuthenticatedEnvironment } from "../apiAuth.server"; +import { logger } from "../logger.server"; export class CreateRunService { #prismaClient: PrismaClientOrTransaction; @@ -13,15 +14,21 @@ export class CreateRunService { public async call({ environment, - eventId, job, version, + eventIds, + isBatched = false, }: { environment: AuthenticatedEnvironment; - eventId: string; job: Job; version: JobVersion; + eventIds: string[]; + isBatched?: boolean; }) { + if (!eventIds.length) { + return; + } + const endpoint = await this.#prismaClient.endpoint.findUniqueOrThrow({ where: { id: version.endpointId, @@ -34,12 +41,23 @@ export class CreateRunService { }, }); - const eventRecord = await this.#prismaClient.eventRecord.findUniqueOrThrow({ + const eventRecords = await this.#prismaClient.eventRecord.findMany({ where: { - id: eventId, + id: { in: eventIds }, }, }); + if (!eventRecords.length) { + throw new Error("No event records found."); + } + + if (eventRecords.length !== eventIds.length) { + logger.warn("Event record counts do not match", { + expected: eventIds.length, + found: eventRecords.length, + }); + } + return await $transaction(this.#prismaClient, async (tx) => { // Get the current max number for the given jobId const latestJob = await tx.jobRun.findFirst({ @@ -53,6 +71,8 @@ export class CreateRunService { // Increment the number for the new execution const newNumber = (latestJob?.number ?? 0) + 1; + const firstEvent = eventRecords[0]; + // Create the new execution with the incremented number const run = await tx.jobRun.create({ data: { @@ -60,16 +80,20 @@ export class CreateRunService { preprocess: version.preprocessRuns, jobId: job.id, versionId: version.id, - eventId: eventId, + eventId: firstEvent.id, + eventIds: eventRecords.map((event) => event.id), environmentId: environment.id, organizationId: environment.organizationId, + payload: isBatched + ? eventRecords.map((event) => event.payload) ?? [{}] + : firstEvent.payload ?? {}, projectId: environment.projectId, endpointId: endpoint.id, queueId: jobQueue.id, - externalAccountId: eventRecord.externalAccountId - ? eventRecord.externalAccountId + externalAccountId: firstEvent.externalAccountId + ? firstEvent.externalAccountId : undefined, - isTest: eventRecord.isTest, + isTest: firstEvent.isTest, internal: job.internal, }, }); diff --git a/apps/webapp/app/services/runs/performRunExecutionV2.server.ts b/apps/webapp/app/services/runs/performRunExecutionV2.server.ts index f22d13edb1..220f0ab39a 100644 --- a/apps/webapp/app/services/runs/performRunExecutionV2.server.ts +++ b/apps/webapp/app/services/runs/performRunExecutionV2.server.ts @@ -77,6 +77,7 @@ export class PerformRunExecutionV2Service { const { response, parser } = await client.preprocessRunRequest({ event, + payload: run.payload as any, job: { id: run.version.job.slug, version: run.version.version, @@ -426,6 +427,7 @@ export class PerformRunExecutionV2Service { return { event, + payload: run.payload as any, job: { id: run.version.job.slug, version: run.version.version, @@ -465,6 +467,7 @@ export class PerformRunExecutionV2Service { return { event, + payload: run.payload as any, job: { id: run.version.job.slug, version: run.version.version, diff --git a/apps/webapp/app/services/runs/reRun.server.ts b/apps/webapp/app/services/runs/reRun.server.ts index 9a604c16b1..8c36fe54b0 100644 --- a/apps/webapp/app/services/runs/reRun.server.ts +++ b/apps/webapp/app/services/runs/reRun.server.ts @@ -69,7 +69,7 @@ export class ReRunService { organization: existingRun.organization, project: existingRun.project, }, - eventId: eventLog.id, + eventIds: [eventLog.id], job: existingRun.job, version: existingRun.version, }); diff --git a/apps/webapp/app/services/sources/deliverHttpSourceRequest.server.ts b/apps/webapp/app/services/sources/deliverHttpSourceRequest.server.ts index 2f002e1017..bc4107d78f 100644 --- a/apps/webapp/app/services/sources/deliverHttpSourceRequest.server.ts +++ b/apps/webapp/app/services/sources/deliverHttpSourceRequest.server.ts @@ -65,7 +65,7 @@ export class DeliverHttpSourceRequestService { httpSourceRequest.endpoint.url ); - const { response, events, metadata } = await clientApi.deliverHttpSourceRequest({ + const { response, events, metadata, options } = await clientApi.deliverHttpSourceRequest({ key: httpSourceRequest.source.key, dynamicId: httpSourceRequest.source.dynamicTrigger?.slug, secret: secret.secret, @@ -108,6 +108,9 @@ export class DeliverHttpSourceRequestService { httpSourceRequest.environment, event, { + batchKey: options?.batchKey, + deliverAt: options?.deliverAt, + deliverAfter: options?.deliverAfter, accountId: httpSourceRequest.source.externalAccount?.identifier, }, httpSourceRequest.source.dynamicSourceId diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index 2f14cb2090..c64a5a3b61 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -66,7 +66,7 @@ const workerCatalog = { deliverBatchedEvent: z.array(z.object({ id: z.string() })), "events.invokeDispatcher": z.object({ id: z.string(), - eventRecordId: z.string(), + eventRecordIds: z.array(z.string()), }), "events.deliverScheduled": z.object({ id: z.string(), @@ -243,7 +243,7 @@ function getWorkerQueue() { handler: async (payload, job) => { const service = new InvokeDispatcherService(); - await service.call(payload.id, payload.eventRecordId); + await service.call(payload.id, payload.eventRecordIds); }, }, "events.deliverScheduled": { From 0a657c4d0dc8c30fff8a7fdee8ecca48162645d7 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:52:47 +0000 Subject: [PATCH 23/27] Fix UI --- .../app/components/run/TriggerDetail.tsx | 4 ++- .../app/presenters/RunPresenter.server.ts | 2 ++ .../app/presenters/TestJobPresenter.server.ts | 35 ++++++++++++++----- .../route.tsx | 11 +++++- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/apps/webapp/app/components/run/TriggerDetail.tsx b/apps/webapp/app/components/run/TriggerDetail.tsx index aee57b22ea..4516eb9e3e 100644 --- a/apps/webapp/app/components/run/TriggerDetail.tsx +++ b/apps/webapp/app/components/run/TriggerDetail.tsx @@ -15,17 +15,19 @@ import { DisplayProperty } from "@trigger.dev/core"; export function TriggerDetail({ trigger, + payload, event, properties, }: { trigger: DetailedEvent; + payload: string; event: { title: string; icon: string; }; properties: DisplayProperty[]; }) { - const { id, name, payload, context, timestamp, deliveredAt } = trigger; + const { id, name, context, timestamp, deliveredAt } = trigger; return ( diff --git a/apps/webapp/app/presenters/RunPresenter.server.ts b/apps/webapp/app/presenters/RunPresenter.server.ts index 34934647d0..239b4fde8a 100644 --- a/apps/webapp/app/presenters/RunPresenter.server.ts +++ b/apps/webapp/app/presenters/RunPresenter.server.ts @@ -78,6 +78,7 @@ export class RunPresenter { slug: run.environment.slug, }, event: this.#prepareEventData(run.event), + payload: run.payload, tasks, runConnections: run.runConnections, missingConnections: run.missingConnections, @@ -104,6 +105,7 @@ export class RunPresenter { query({ id, userId }: RunOptions) { return this.#prismaClient.jobRun.findFirst({ select: { + payload: true, id: true, number: true, status: true, diff --git a/apps/webapp/app/presenters/TestJobPresenter.server.ts b/apps/webapp/app/presenters/TestJobPresenter.server.ts index c87b035c8d..af016a8901 100644 --- a/apps/webapp/app/presenters/TestJobPresenter.server.ts +++ b/apps/webapp/app/presenters/TestJobPresenter.server.ts @@ -1,4 +1,4 @@ -import { User } from "@trigger.dev/database"; +import { Prisma, User } from "@trigger.dev/database"; import { replacements } from "@trigger.dev/core"; import { PrismaClient, prisma } from "~/db.server"; import { Job } from "~/models/job.server"; @@ -74,6 +74,7 @@ export class TestJobPresenter { createdAt: true, number: true, status: true, + payload: true, event: { select: { payload: true, @@ -111,7 +112,7 @@ export class TestJobPresenter { alias.version.examples.map((example) => ({ ...example, icon: example.icon ?? undefined, - payload: example.payload ? JSON.stringify(example.payload, exampleReplacer, 2) : undefined, + payload: prettyJsonValue(example.payload, exampleReplacer), })) ); @@ -127,13 +128,16 @@ export class TestJobPresenter { ), })), examples, - runs: job.runs.map((r) => ({ - id: r.id, - number: r.number, - status: r.status, - created: r.createdAt, - payload: r.event.payload ? JSON.stringify(r.event.payload, null, 2) : undefined, - })), + runs: job.runs.map((r) => { + const payload = r.payload ?? r.event.payload; + return { + id: r.id, + number: r.number, + status: r.status, + created: r.createdAt, + payload: prettyJsonValue(payload), + }; + }), }; } } @@ -153,3 +157,16 @@ function exampleReplacer(key: string, value: any) { return value; } + +function prettyJsonValue( + value: Prisma.JsonValue, + replacer?: (key: string, value: any) => any, + space = 2 +) { + if (value === null) { + return; + } + + const pretty = JSON.stringify(value, replacer, space); + return pretty === "{}" ? "{\n \n}" : pretty; +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.trigger/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.trigger/route.tsx index dcdd2b7e65..7a4cc43c8a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.trigger/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.trigger/route.tsx @@ -28,5 +28,14 @@ export default function Page() { const job = useJob(); const run = useRun(); - return ; + const payload = run.payload !== null ? JSON.stringify(run.payload, null, 2) : trigger.payload; + + return ( + + ); } From bcb8e2556e29417cbaf1e9cd3b7ac1edb1adc080 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:58:32 +0000 Subject: [PATCH 24/27] Airtable provisional webhook tests --- integrations/airtable/src/index.ts | 32 ++++++----- integrations/airtable/src/webhooks.ts | 13 ++++- references/job-catalog/src/airtable.ts | 79 +++++++++++++------------- 3 files changed, 69 insertions(+), 55 deletions(-) diff --git a/integrations/airtable/src/index.ts b/integrations/airtable/src/index.ts index bfb8d405de..c4b5b8e6f3 100644 --- a/integrations/airtable/src/index.ts +++ b/integrations/airtable/src/index.ts @@ -12,7 +12,14 @@ import { } from "@trigger.dev/sdk"; import AirtableSDK, { Error as AirtableApiError } from "airtable"; import { Base } from "./base"; -import { Webhooks, createWebhookEventSource } from "./webhooks"; +import * as events from "./events"; +import { + WebhookChangeType, + WebhookDataType, + Webhooks, + createTrigger, + createWebhookEventSource, +} from "./webhooks"; export * from "./types"; export * from "./base"; @@ -113,18 +120,17 @@ export class Airtable implements TriggerIntegration { return new Base(this.runTask.bind(this), baseId); } - //todo these require batch support because they send too many events - // onTableChanges(params: { - // baseId: string; - // tableId?: string; - // changeTypes?: WebhookChangeType[]; - // dataTypes?: WebhookDataType[]; - // }) { - // return createTrigger(this.source, events.onTableChanged, params, { - // changeTypes: params.changeTypes, - // dataTypes: ["tableData", "tableFields", "tableMetadata"], - // }); - // } + onTableChanges(params: { + baseId: string; + tableId?: string; + changeTypes?: WebhookChangeType[]; + dataTypes?: WebhookDataType[]; + }) { + return createTrigger(this.source, events.onTableChanged, params, { + changeTypes: params.changeTypes, + dataTypes: ["tableData", "tableFields", "tableMetadata"], + }); + } webhooks() { return new Webhooks(this.runTask.bind(this)); diff --git a/integrations/airtable/src/webhooks.ts b/integrations/airtable/src/webhooks.ts index 2ff4de7ef1..3e03b17928 100644 --- a/integrations/airtable/src/webhooks.ts +++ b/integrations/airtable/src/webhooks.ts @@ -218,13 +218,15 @@ export function createTrigger( dataTypes: WebhookDataType[]; changeTypes?: WebhookChangeType[]; fromSources?: WebhookFromSource[]; - } + }, + batch = true ): CreateTriggersResult { return new ExternalSourceTrigger({ event, params, source, options, + batch, }); } @@ -262,6 +264,7 @@ export function createWebhookEventSource( id: "airtable.webhook", schema: z.object({ baseId: z.string(), tableId: z.string().optional() }), optionSchema: z.object({ + changeTypes: z.array(WebhookChangeTypeSchema).optional(), dataTypes: z.array(WebhookDataTypeSchema), fromSources: z.array(WebhookFromSourceSchema).optional(), }), @@ -289,7 +292,9 @@ export function createWebhookEventSource( const specification: WebhookSpecification = { filters: { dataTypes: options.dataTypes.desired as WebhookDataType[], - changeTypes: options.event.desired as WebhookChangeType[], + changeTypes: options.changeTypes + ? (options.changeTypes.desired as WebhookChangeType[]) + : ["add", "remove", "update"], fromSources: (options.fromSources?.desired ?? [ "client", "anonymousUser", @@ -424,6 +429,10 @@ async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integ })) : [], metadata: response?.cursor ? { cursor: response.cursor } : undefined, + options: { + batchKey: "airtable-webhooks", + deliverAfter: 10, + }, }; } diff --git a/references/job-catalog/src/airtable.ts b/references/job-catalog/src/airtable.ts index 703046bf3e..78cc0b4007 100644 --- a/references/job-catalog/src/airtable.ts +++ b/references/job-catalog/src/airtable.ts @@ -77,47 +77,46 @@ client.defineJob({ }, }); -//todo webhooks require batch support -// client.defineJob({ -// id: "airtable-delete-webhooks", -// name: "Airtable Example 2: webhook admin", -// version: "0.1.0", -// trigger: eventTrigger({ -// name: "airtable.example", -// schema: z.object({ -// baseId: z.string(), -// deleteWebhooks: z.boolean().optional(), -// }), -// }), -// integrations: { -// airtable, -// }, -// run: async (payload, io, ctx) => { -// const webhooks = await io.airtable.webhooks().list("list webhooks", { baseId: payload.baseId }); - -// if (payload.deleteWebhooks === true) { -// for (const webhook of webhooks.webhooks) { -// await io.airtable.webhooks().delete(`delete webhook: ${webhook.id}`, { -// baseId: payload.baseId, -// webhookId: webhook.id, -// }); -// } -// } -// }, -// }); +client.defineJob({ + id: "airtable-delete-webhooks", + name: "Airtable Example 2: webhook admin", + version: "0.1.0", + trigger: eventTrigger({ + name: "airtable.example", + schema: z.object({ + baseId: z.string(), + deleteWebhooks: z.boolean().optional(), + }), + }), + integrations: { + airtable, + }, + run: async (payload, io, ctx) => { + const webhooks = await io.airtable.webhooks().list("list webhooks", { baseId: payload.baseId }); + + if (payload.deleteWebhooks === true) { + for (const webhook of webhooks.webhooks) { + await io.airtable.webhooks().delete(`delete webhook: ${webhook.id}`, { + baseId: payload.baseId, + webhookId: webhook.id, + }); + } + } + }, +}); //todo changes the structure of the trigger so it's airtable.base("val").onChanges({ -// client.defineJob({ -// id: "airtable-on-table", -// name: "Airtable Example: onTable", -// version: "0.1.0", -// trigger: airtable.onTableChanges({ -// baseId: "appSX6ly4nZGfdUSy", -// tableId: "tblr5BReu2yeOMk7n", -// }), -// run: async (payload, io, ctx) => { -// await io.logger.log(`transaction number ${payload.baseTransactionNumber}`); -// }, -// }); +client.defineJob({ + id: "airtable-on-table", + name: "Airtable Example: onTable", + version: "0.1.0", + trigger: airtable.onTableChanges({ + baseId: "appSX6ly4nZGfdUSy", + tableId: "tblr5BReu2yeOMk7n", + }), + run: async (payload, io, ctx) => { + // await io.logger.log(`transaction number ${payload.baseTransactionNumber}`); + }, +}); createExpressServer(client); From 116426002d6291c83626048defa8fa100603d07a Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:59:15 +0000 Subject: [PATCH 25/27] Update static trigger metadata --- packages/core/src/schemas/triggers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/schemas/triggers.ts b/packages/core/src/schemas/triggers.ts index edc5256bc6..04984d6af1 100644 --- a/packages/core/src/schemas/triggers.ts +++ b/packages/core/src/schemas/triggers.ts @@ -30,6 +30,7 @@ export const DynamicTriggerMetadataSchema = z.object({ export const StaticTriggerMetadataSchema = z.object({ type: z.literal("static"), + batch: z.boolean().optional(), title: z.union([z.string(), z.array(z.string())]), properties: z.array(DisplayPropertySchema).optional(), rule: EventRuleSchema, From 23ac3031c23c414db495904a7c17f0722b090be5 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:07:26 +0000 Subject: [PATCH 26/27] Better tests for delivery time preservation --- references/job-catalog/src/events.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/references/job-catalog/src/events.ts b/references/job-catalog/src/events.ts index 3973bddfa0..662da1db02 100644 --- a/references/job-catalog/src/events.ts +++ b/references/job-catalog/src/events.ts @@ -82,23 +82,27 @@ client.defineJob({ }), run: async (payload, io, ctx) => { // unbatched - await io.sendEvent("unbatched", + await io.sendEvent( + "unbatched", { name: "new.message", payload: { message: "Not batching this!" } }, { deliverAfter: 5 } ); // batched - await io.sendEvent("batched-1", - { name: "new.message", payload: { message: "Hey there!" } }, - { batchKey: "user123", deliverAfter: 10 } + await io.sendEvent( + "batched-1", + { name: "new.message", payload: { message: "Message 1" } }, + { batchKey: "amazing-batchkey", deliverAfter: 10 } ); - await io.sendEvent("batched-2", - { name: "new.message", payload: { message: "It's me!" } }, - { batchKey: "user123" } // deliverAfter: 10 - should be preserved + await io.sendEvent( + "batched-2", + { name: "new.message", payload: { message: "Message 2" } }, + { batchKey: "amazing-batchkey", deliverAfter: 200 } // deliverAfter: 10 - should be preserved ); - await io.sendEvent("batched-3", - { name: "new.message", payload: { message: "Where are you?" } }, - { batchKey: "user123" } // deliverAfter: 10 - should be preserved + await io.sendEvent( + "batched-3", + { name: "new.message", payload: { message: "Message 3" } }, + { batchKey: "amazing-batchkey", deliverAfter: 300 } // deliverAfter: 10 - should be preserved ); }, }); From f3f51a4eff255b31f17cac50dcf04d3165853955 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:19:54 +0000 Subject: [PATCH 27/27] Update lockfile --- pnpm-lock.yaml | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3738f06f98..a729dcff11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,7 +145,7 @@ importers: eslint-config-prettier: ^8.5.0 express: ^4.18.1 framer-motion: ^10.12.11 - graphile-worker: ^0.13.0 + graphile-worker: 0.14.0-rc.0 highlight.run: ^7.3.4 humanize-duration: ^3.27.3 intl-parse-accept-language: ^1.0.0 @@ -246,7 +246,7 @@ importers: emails: link:../../packages/emails express: 4.18.2 framer-motion: 10.12.11_biqbaboplfbrettd7655fr4n2y - graphile-worker: 0.13.0 + graphile-worker: 0.14.0-rc.0 highlight.run: 7.3.4 humanize-duration: 3.27.3 intl-parse-accept-language: 1.0.0 @@ -2259,11 +2259,6 @@ packages: resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-identifier/7.19.1: - resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} - engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-validator-identifier/7.22.15: resolution: {integrity: sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==} engines: {node: '>=6.9.0'} @@ -5179,7 +5174,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/helper-string-parser': 7.21.5 - '@babel/helper-validator-identifier': 7.19.1 + '@babel/helper-validator-identifier': 7.22.15 to-fast-properties: 2.0.0 dev: true @@ -18795,7 +18790,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.59.6_eslint@8.42.0 + '@typescript-eslint/parser': 5.59.6_binxsscxvozjxebftqdoazsxm4 debug: 3.2.7 eslint: 8.42.0 eslint-import-resolver-node: 0.3.7 @@ -18880,7 +18875,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.59.6_eslint@8.42.0 + '@typescript-eslint/parser': 5.59.6_binxsscxvozjxebftqdoazsxm4 array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 @@ -21019,9 +21014,9 @@ packages: /graphemer/1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - /graphile-worker/0.13.0: - resolution: {integrity: sha512-8Hl5XV6hkabZRhYzvbUfvjJfPFR5EPxYRVWlzQC2rqYHrjULTLBgBYZna5R9ukbnsbWSvn4vVrzOBIOgIC1jjw==} - engines: {node: '>=10.0.0'} + /graphile-worker/0.14.0-rc.0: + resolution: {integrity: sha512-KV6nP1ljKlo/qROkB4RJS19OdnrM1yzWjDruf0BgLWx6gDTT+FECeHAFC+gxsvShm/rTyDB4OIPwqMMu/oiOqg==} + engines: {node: '>=14.0.0'} hasBin: true dependencies: '@graphile/logger': 0.2.0 @@ -21031,8 +21026,8 @@ packages: cosmiconfig: 7.1.0 json5: 2.2.3 pg: 8.10.0 - tslib: 2.4.1 - yargs: 16.2.0 + tslib: 2.6.2 + yargs: 17.7.2 transitivePeerDependencies: - pg-native dev: false @@ -32325,6 +32320,7 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 20.2.9 + dev: true /yargs/17.1.1: resolution: {integrity: sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ==}