Files
triggerdotdev--trigger.dev/apps/webapp/app/services/worker.server.ts
Eric Allam ef1576c73f WIP
2023-09-15 11:02:59 +01:00

344 lines
11 KiB
TypeScript

import { DeliverEmailSchema } from "@/../../packages/emails/src";
import { ScheduledPayloadSchema } from "@trigger.dev/core";
import { z } from "zod";
import { prisma } from "~/db.server";
import { env } from "~/env.server";
import { ZodWorker } from "~/platform/zodWorker.server";
import { sendEmail } from "./email.server";
import { IndexEndpointService } from "./endpoints/indexEndpoint.server";
import { RecurringEndpointIndexService } from "./endpoints/recurringEndpointIndex.server";
import { DeliverEventService } from "./events/deliverEvent.server";
import { InvokeDispatcherService } from "./events/invokeDispatcher.server";
import { integrationAuthRepository } from "./externalApis/integrationAuthRepository.server";
import { IntegrationConnectionCreatedService } from "./externalApis/integrationConnectionCreated.server";
import { MissingConnectionCreatedService } from "./runs/missingConnectionCreated.server";
import { PerformRunExecutionV1Service } from "./runs/performRunExecutionV1.server";
import { PerformRunExecutionV2Service } from "./runs/performRunExecutionV2.server";
import { StartRunService } from "./runs/startRun.server";
import { DeliverScheduledEventService } from "./schedules/deliverScheduledEvent.server";
import { ActivateSourceService } from "./sources/activateSource.server";
import { DeliverHttpSourceRequestService } from "./sources/deliverHttpSourceRequest.server";
import { PerformTaskOperationService } from "./tasks/performTaskOperation.server";
import { addMissingVersionField } from "@trigger.dev/core";
const workerCatalog = {
indexEndpoint: z.object({
id: z.string(),
source: z.enum(["MANUAL", "API", "INTERNAL", "HOOK"]).optional(),
sourceData: z.any().optional(),
reason: z.string().optional(),
}),
scheduleEmail: DeliverEmailSchema,
startRun: z.object({ id: z.string() }),
performTaskOperation: z.object({
id: z.string(),
}),
deliverHttpSourceRequest: z.object({ id: z.string() }),
refreshOAuthToken: z.object({
organizationId: z.string(),
connectionId: z.string(),
}),
activateSource: z.preprocess(
addMissingVersionField,
z.discriminatedUnion("version", [
z.object({
version: z.literal("1"),
id: z.string(),
orphanedEvents: z.array(z.string()).optional(),
}),
z.object({
version: z.literal("2"),
id: z.string(),
orphanedOptions: z.record(z.string(), z.array(z.string())).optional(),
}),
])
),
deliverEvent: z.object({ id: z.string() }),
"events.invokeDispatcher": z.object({
id: z.string(),
eventRecordId: z.string(),
}),
"events.deliverScheduled": z.object({
id: z.string(),
payload: ScheduledPayloadSchema,
}),
missingConnectionCreated: z.object({
id: z.string(),
}),
connectionCreated: z.object({
id: z.string(),
}),
executeBackgroundFunctionTask: z.object({
id: z.string(),
}),
};
const executionWorkerCatalog = {
performRunExecution: z.object({
id: z.string(),
}),
performRunExecutionV2: z.object({
id: z.string(),
reason: z.enum(["EXECUTE_JOB", "PREPROCESS"]),
resumeTaskId: z.string().optional(),
isRetry: z.boolean(),
}),
};
let workerQueue: ZodWorker<typeof workerCatalog>;
let executionWorker: ZodWorker<typeof executionWorkerCatalog>;
declare global {
var __worker__: ZodWorker<typeof workerCatalog>;
var __executionWorker__: ZodWorker<typeof executionWorkerCatalog>;
}
// this is needed because in development we don't want to restart
// the server with every change, but we want to make sure we don't
// create a new connection to the DB with every change either.
// in production we'll have a single connection to the DB.
if (env.NODE_ENV === "production") {
workerQueue = getWorkerQueue();
executionWorker = getExecutionWorkerQueue();
} else {
if (!global.__worker__) {
global.__worker__ = getWorkerQueue();
}
workerQueue = global.__worker__;
if (!global.__executionWorker__) {
global.__executionWorker__ = getExecutionWorkerQueue();
}
executionWorker = global.__executionWorker__;
}
export async function init() {
if (env.WORKER_ENABLED === "true") {
await workerQueue.initialize();
}
if (env.EXECUTION_WORKER_ENABLED === "true") {
await executionWorker.initialize();
}
}
function getWorkerQueue() {
return new ZodWorker({
name: "workerQueue",
prisma,
runnerOptions: {
connectionString: env.DATABASE_URL,
concurrency: env.WORKER_CONCURRENCY,
pollInterval: env.WORKER_POLL_INTERVAL,
noPreparedStatements: env.DATABASE_URL !== env.DIRECT_URL,
schema: env.WORKER_SCHEMA,
maxPoolSize: env.WORKER_CONCURRENCY,
},
schema: workerCatalog,
recurringTasks: {
// Run this every 5 minutes
autoIndexProductionEndpoints: {
pattern: "*/5 * * * *",
handler: async (payload, job) => {
const service = new RecurringEndpointIndexService();
await service.call(payload.ts);
},
},
// Run this every hour
purgeOldIndexings: {
pattern: "0 * * * *",
handler: async (payload, job) => {
// Delete indexings that are older than 7 days
await prisma.endpointIndex.deleteMany({
where: {
createdAt: {
lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
},
},
});
},
},
},
tasks: {
"events.invokeDispatcher": {
priority: 0, // smaller number = higher priority
maxAttempts: 3,
handler: async (payload, job) => {
const service = new InvokeDispatcherService();
await service.call(payload.id, payload.eventRecordId);
},
},
"events.deliverScheduled": {
priority: 0, // smaller number = higher priority
maxAttempts: 5,
handler: async ({ id, payload }, job) => {
const service = new DeliverScheduledEventService();
await service.call(id, payload);
},
},
connectionCreated: {
priority: 10, // smaller number = higher priority
maxAttempts: 3,
handler: async (payload, job) => {
const service = new IntegrationConnectionCreatedService();
await service.call(payload.id);
},
},
missingConnectionCreated: {
priority: 10, // smaller number = higher priority
maxAttempts: 3,
handler: async (payload, job) => {
const service = new MissingConnectionCreatedService();
await service.call(payload.id);
},
},
activateSource: {
priority: 10, // smaller number = higher priority
maxAttempts: 3,
handler: async (payload, graphileJob) => {
const service = new ActivateSourceService();
switch (payload.version) {
case "1": {
//change the input data to match the new schema
await service.call(
payload.id,
graphileJob.id,
payload.orphanedEvents
? {
event: payload.orphanedEvents,
}
: undefined
);
break;
}
case "2": {
await service.call(payload.id, graphileJob.id, payload.orphanedOptions);
break;
}
}
},
},
deliverHttpSourceRequest: {
priority: 1, // smaller number = higher priority
maxAttempts: 14,
handler: async (payload, job) => {
const service = new DeliverHttpSourceRequestService();
await service.call(payload.id);
},
},
startRun: {
priority: 0, // smaller number = higher priority
maxAttempts: 4,
handler: async (payload, job) => {
const service = new StartRunService();
await service.call(payload.id);
},
},
performTaskOperation: {
priority: 0, // smaller number = higher priority
queueName: (payload) => `tasks:${payload.id}`,
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformTaskOperationService();
await service.call(payload.id);
},
},
scheduleEmail: {
queueName: "internal-queue",
priority: 100,
maxAttempts: 3,
handler: async (payload, job) => {
await sendEmail(payload);
},
},
indexEndpoint: {
priority: 1, // smaller number = higher priority
maxAttempts: 7,
handler: async (payload, job) => {
const service = new IndexEndpointService();
await service.call(payload.id, payload.source, payload.reason, payload.sourceData);
},
},
deliverEvent: {
priority: 0, // smaller number = higher priority
maxAttempts: 5,
handler: async (payload, job) => {
const service = new DeliverEventService();
await service.call(payload.id);
},
},
refreshOAuthToken: {
priority: 8, // smaller number = higher priority
queueName: "internal-queue",
maxAttempts: 7,
handler: async (payload, job) => {
await integrationAuthRepository.refreshConnection({
connectionId: payload.connectionId,
});
},
},
executeBackgroundFunctionTask: {
priority: 0, // smaller number = higher priority
maxAttempts: 3,
handler: async (payload, job) => {},
},
},
});
}
function getExecutionWorkerQueue() {
return new ZodWorker({
name: "executionWorker",
prisma,
runnerOptions: {
connectionString: env.DATABASE_URL,
concurrency: env.EXECUTION_WORKER_CONCURRENCY,
pollInterval: env.EXECUTION_WORKER_POLL_INTERVAL,
noPreparedStatements: env.DATABASE_URL !== env.DIRECT_URL,
schema: env.WORKER_SCHEMA,
maxPoolSize: env.EXECUTION_WORKER_CONCURRENCY,
},
schema: executionWorkerCatalog,
tasks: {
performRunExecution: {
priority: 0, // smaller number = higher priority
maxAttempts: 1,
handler: async (payload, job) => {
// This is a legacy task that we don't use anymore, but needs to be here for backwards compatibility
// TODO: remove this once all performRunExecution tasks have been processed
const service = new PerformRunExecutionV1Service();
await service.call(payload.id);
},
},
performRunExecutionV2: {
priority: 0, // smaller number = higher priority
maxAttempts: 12,
handler: async (payload, job) => {
const service = new PerformRunExecutionV2Service();
await service.call({
id: payload.id,
reason: payload.reason,
resumeTaskId: payload.resumeTaskId,
isRetry: payload.isRetry,
});
},
},
},
});
}
export { executionWorker, workerQueue };