Files
Eric Allam a2c2d920b7 Use redis worker for run heartbeats and alerts (#1669)
* Move the task run heartbeats to RedisWorker

* Move alerts to redis worker, improving redis worker

* Fix typecheck errors

* Use single threaded tests for redis worker

* Enable/disable the redis workers independently

* Remove preview release from PR checks
2025-02-06 10:21:57 +00:00

823 lines
26 KiB
TypeScript

import { DeliverEmailSchema } from "emails";
import { ScheduledPayloadSchema, addMissingVersionField } from "@trigger.dev/core";
import { ZodWorker } from "@internal/zod-worker";
import { z } from "zod";
import { $replica, prisma } from "~/db.server";
import { env } from "~/env.server";
import { MarqsConcurrencyMonitor } from "~/v3/marqs/concurrencyMonitor.server";
import { RequeueV2Message } from "~/v3/marqs/requeueV2Message.server";
import { DeliverAlertService } from "~/v3/services/alerts/deliverAlert.server";
import { PerformDeploymentAlertsService } from "~/v3/services/alerts/performDeploymentAlerts.server";
import { PerformBulkActionService } from "~/v3/services/bulk/performBulkAction.server";
import { CancelTaskAttemptDependenciesService } from "~/v3/services/cancelTaskAttemptDependencies.server";
import { EnqueueDelayedRunService } from "~/v3/services/enqueueDelayedRun.server";
import { ExecuteTasksWaitingForDeployService } from "~/v3/services/executeTasksWaitingForDeploy";
import { ExpireEnqueuedRunService } from "~/v3/services/expireEnqueuedRun.server";
import { IndexDeploymentService } from "~/v3/services/indexDeployment.server";
import { ResumeBatchRunService } from "~/v3/services/resumeBatchRun.server";
import { ResumeTaskDependencyService } from "~/v3/services/resumeTaskDependency.server";
import { ResumeTaskRunDependenciesService } from "~/v3/services/resumeTaskRunDependencies.server";
import { RetryAttemptService } from "~/v3/services/retryAttempt.server";
import { TimeoutDeploymentService } from "~/v3/services/timeoutDeployment.server";
import { TriggerScheduledTaskService } from "~/v3/services/triggerScheduledTask.server";
import { GraphileMigrationHelperService } from "./db/graphileMigrationHelper.server";
import { ExpireDispatcherService } from "./dispatchers/expireDispatcher.server";
import { InvokeEphemeralDispatcherService } from "./dispatchers/invokeEphemeralEventDispatcher.server";
import { sendEmail } from "./email.server";
import { IndexEndpointService } from "./endpoints/indexEndpoint.server";
import { PerformEndpointIndexService } from "./endpoints/performEndpointIndexService";
import { ProbeEndpointService } from "./endpoints/probeEndpoint.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 { reportInvocationUsage } from "./platform.v3.server";
import { executionRateLimiter } from "./runExecutionRateLimiter.server";
import { DeliverRunSubscriptionService } from "./runs/deliverRunSubscription.server";
import { DeliverRunSubscriptionsService } from "./runs/deliverRunSubscriptions.server";
import { MissingConnectionCreatedService } from "./runs/missingConnectionCreated.server";
import { PerformRunExecutionV3Service } from "./runs/performRunExecutionV3.server";
import { ResumeRunService } from "./runs/resumeRun.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 { DeliverWebhookRequestService } from "./sources/deliverWebhookRequest.server";
import { PerformTaskOperationService } from "./tasks/performTaskOperation.server";
import { ProcessCallbackTimeoutService } from "./tasks/processCallbackTimeout.server";
import { ResumeTaskService } from "./tasks/resumeTask.server";
import { PerformTaskRunAlertsService } from "~/v3/services/alerts/performTaskRunAlerts.server";
import {
CancelDevSessionRunsService,
CancelDevSessionRunsServiceOptions,
} from "~/v3/services/cancelDevSessionRuns.server";
import { logger } from "./logger.server";
import { BatchProcessingOptions, BatchTriggerV3Service } from "~/v3/services/batchTriggerV3.server";
const workerCatalog = {
indexEndpoint: z.object({
id: z.string(),
source: z.enum(["MANUAL", "API", "INTERNAL", "HOOK"]).optional(),
sourceData: z.any().optional(),
reason: z.string().optional(),
}),
performEndpointIndexing: z.object({
id: z.string(),
}),
scheduleEmail: DeliverEmailSchema,
startRun: z.object({ id: z.string() }),
processCallbackTimeout: z.object({
id: z.string(),
}),
deliverHttpSourceRequest: z.object({ id: z.string() }),
deliverWebhookRequest: 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(),
}),
probeEndpoint: z.object({
id: z.string(),
}),
simulate: z.object({
seconds: z.number(),
}),
deliverRunSubscriptions: z.object({
id: z.string(),
}),
deliverRunSubscription: z.object({
id: z.string(),
}),
resumeTask: z.object({
id: z.string(),
}),
expireDispatcher: z.object({
id: z.string(),
}),
resumeRun: z.object({
id: z.string(),
}),
// v3 tasks
"v3.indexDeployment": z.object({
id: z.string(),
}),
"v3.resumeTaskRunDependencies": z.object({
attemptId: z.string(),
}),
"v3.resumeBatchRun": z.object({
batchRunId: z.string(),
}),
"v3.resumeTaskDependency": z.object({
dependencyId: z.string(),
sourceTaskAttemptId: z.string(),
}),
"v3.timeoutDeployment": z.object({
deploymentId: z.string(),
fromStatus: z.string(),
errorMessage: z.string(),
}),
"v3.executeTasksWaitingForDeploy": z.object({
backgroundWorkerId: z.string(),
}),
"v3.triggerScheduledTask": z.object({
instanceId: z.string(),
}),
"v3.performTaskRunAlerts": z.object({
runId: z.string(),
}),
"v3.deliverAlert": z.object({
alertId: z.string(),
}),
"v3.performDeploymentAlerts": z.object({
deploymentId: z.string(),
}),
"v3.performBulkAction": z.object({
bulkActionGroupId: z.string(),
}),
"v3.performBulkActionItem": z.object({
bulkActionItemId: z.string(),
}),
"v3.requeueTaskRun": z.object({
runId: z.string(),
}),
"v3.retryAttempt": z.object({
runId: z.string(),
}),
"v2.requeueMessage": z.object({
runId: z.string(),
}),
"v3.reportUsage": z.object({
orgId: z.string(),
data: z.object({
costInCents: z.string(),
}),
additionalData: z.record(z.any()).optional(),
}),
"v3.enqueueDelayedRun": z.object({
runId: z.string(),
}),
"v3.expireRun": z.object({
runId: z.string(),
}),
"v3.cancelTaskAttemptDependencies": z.object({
attemptId: z.string(),
}),
"v3.cancelDevSessionRuns": CancelDevSessionRunsServiceOptions,
"v3.processBatchTaskRun": BatchProcessingOptions,
};
const executionWorkerCatalog = {
performRunExecutionV2: z.object({
id: z.string(),
reason: z.enum(["EXECUTE_JOB", "PREPROCESS"]),
resumeTaskId: z.string().optional(),
isRetry: z.boolean(),
}),
performRunExecutionV3: z.object({
id: z.string(),
reason: z.enum(["EXECUTE_JOB", "PREPROCESS"]),
}),
};
const taskOperationWorkerCatalog = {
performTaskOperation: z.object({
id: z.string(),
}),
invokeEphemeralDispatcher: z.object({
id: z.string(),
eventRecordId: z.string(),
}),
};
let workerQueue: ZodWorker<typeof workerCatalog>;
let executionWorker: ZodWorker<typeof executionWorkerCatalog>;
let taskOperationWorker: ZodWorker<typeof taskOperationWorkerCatalog>;
declare global {
var __worker__: ZodWorker<typeof workerCatalog>;
var __executionWorker__: ZodWorker<typeof executionWorkerCatalog>;
var __taskOperationWorker__: ZodWorker<typeof taskOperationWorkerCatalog>;
}
// 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();
taskOperationWorker = getTaskOperationWorkerQueue();
} else {
if (!global.__worker__) {
global.__worker__ = getWorkerQueue();
}
workerQueue = global.__worker__;
if (!global.__executionWorker__) {
global.__executionWorker__ = getExecutionWorkerQueue();
}
executionWorker = global.__executionWorker__;
if (!global.__taskOperationWorker__) {
global.__taskOperationWorker__ = getTaskOperationWorkerQueue();
}
taskOperationWorker = global.__taskOperationWorker__;
}
export async function init() {
const migrationHelper = new GraphileMigrationHelperService();
await migrationHelper.call();
if (env.WORKER_ENABLED === "true") {
await workerQueue.initialize();
}
if (env.EXECUTION_WORKER_ENABLED === "true") {
await executionWorker.initialize();
}
if (env.TASK_OPERATION_WORKER_ENABLED === "true") {
await taskOperationWorker.initialize();
}
}
function getWorkerQueue() {
return new ZodWorker({
name: "workerQueue",
prisma,
replica: $replica,
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 + 1,
},
logger: logger,
shutdownTimeoutInMs: env.GRACEFUL_SHUTDOWN_TIMEOUT,
schema: workerCatalog,
recurringTasks: {
// Run this every 5 minutes
autoIndexProductionEndpoints: {
match: "*/30 * * * *",
handler: async (payload, job) => {
const service = new RecurringEndpointIndexService();
await service.call(payload.ts);
},
},
scheduleImminentDeferredEvents: {
match: "*/10 * * * *",
handler: async (payload, job) => {
await DeliverScheduledEventService.scheduleImminentDeferredEvents();
},
},
// Run this every hour
purgeOldIndexings: {
match: "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),
},
},
});
},
},
"marqs.v3.queueConcurrencyMonitor": {
// run every 5 minutes
match: "*/5 * * * *",
handler: async (payload, job, helpers) => {
await MarqsConcurrencyMonitor.initiateV3Monitoring(helpers.abortSignal);
},
},
"marqs.v2.queueConcurrencyMonitor": {
match: "*/5 * * * *", // run every 5 minutes
handler: async (payload, job, helpers) => {
await MarqsConcurrencyMonitor.initiateV2Monitoring(helpers.abortSignal);
},
},
},
tasks: {
"events.invokeDispatcher": {
priority: 0, // smaller number = higher priority
maxAttempts: 6,
handler: async (payload, job) => {
const service = new InvokeDispatcherService();
await service.call(payload.id, payload.eventRecordId);
},
},
"events.deliverScheduled": {
priority: 0, // smaller number = higher priority
maxAttempts: 8,
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: 0, // 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: 0, // smaller number = higher priority
maxAttempts: 14,
handler: async (payload, job) => {
const service = new DeliverHttpSourceRequestService();
await service.call(payload.id);
},
},
deliverWebhookRequest: {
priority: 0, // smaller number = higher priority
maxAttempts: 14,
handler: async (payload, job) => {
const service = new DeliverWebhookRequestService();
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);
},
},
processCallbackTimeout: {
priority: 0, // smaller number = higher priority
maxAttempts: 3,
handler: async (payload, job) => {
const service = new ProcessCallbackTimeoutService();
await service.call(payload.id);
},
},
scheduleEmail: {
priority: 0,
maxAttempts: 3,
handler: async (payload, job) => {
await sendEmail(payload);
},
},
indexEndpoint: {
priority: 0, // 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);
},
},
performEndpointIndexing: {
priority: 0, // smaller number = higher priority
maxAttempts: 7,
handler: async (payload, job) => {
const service = new PerformEndpointIndexService();
await service.call(payload.id);
},
},
deliverEvent: {
priority: 0, // smaller number = higher priority
maxAttempts: 5,
handler: async (payload, job) => {
const service = new DeliverEventService();
await service.call(payload.id);
},
},
refreshOAuthToken: {
priority: 0, // smaller number = higher priority
maxAttempts: 7,
handler: async (payload, job) => {
await integrationAuthRepository.refreshConnection({
connectionId: payload.connectionId,
});
},
},
probeEndpoint: {
priority: 0,
maxAttempts: 1,
handler: async (payload, job) => {
const service = new ProbeEndpointService();
await service.call(payload.id);
},
},
simulate: {
maxAttempts: 5,
handler: async (payload, job) => {
await new Promise((resolve) => setTimeout(resolve, payload.seconds * 1000));
},
},
deliverRunSubscriptions: {
priority: 0, // smaller number = higher priority
maxAttempts: 5,
handler: async (payload, job) => {
const service = new DeliverRunSubscriptionsService();
await service.call(payload.id);
},
},
deliverRunSubscription: {
priority: 0, // smaller number = higher priority
maxAttempts: 13,
handler: async (payload, job) => {
const service = new DeliverRunSubscriptionService();
await service.call(payload.id);
},
},
resumeTask: {
priority: 0,
maxAttempts: 3,
handler: async (payload, job) => {
const service = new ResumeTaskService();
return await service.call(payload.id);
},
},
expireDispatcher: {
priority: 0,
maxAttempts: 3,
handler: async (payload) => {
const service = new ExpireDispatcherService();
return await service.call(payload.id);
},
},
resumeRun: {
priority: 0,
maxAttempts: 10,
handler: async (payload, job) => {
const service = new ResumeRunService();
return await service.call(payload.id);
},
},
// v3 tasks
"v3.indexDeployment": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new IndexDeploymentService();
return await service.call(payload.id);
},
},
"v3.resumeTaskRunDependencies": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new ResumeTaskRunDependenciesService();
return await service.call(payload.attemptId);
},
},
"v3.resumeBatchRun": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new ResumeBatchRunService();
await service.call(payload.batchRunId);
},
},
"v3.resumeTaskDependency": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new ResumeTaskDependencyService();
return await service.call(payload.dependencyId, payload.sourceTaskAttemptId);
},
},
"v3.timeoutDeployment": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new TimeoutDeploymentService();
return await service.call(payload.deploymentId, payload.fromStatus, payload.errorMessage);
},
},
"v3.executeTasksWaitingForDeploy": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new ExecuteTasksWaitingForDeployService();
return await service.call(payload.backgroundWorkerId);
},
},
"v3.triggerScheduledTask": {
priority: 0,
maxAttempts: 3, // total delay of 30 seconds
handler: async (payload, job) => {
const service = new TriggerScheduledTaskService();
return await service.call(payload.instanceId, job.attempts === job.max_attempts);
},
},
"v3.performTaskRunAlerts": {
priority: 0,
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformTaskRunAlertsService();
return await service.call(payload.runId);
},
},
"v3.deliverAlert": {
priority: 0,
maxAttempts: 8,
handler: async (payload, job) => {
const service = new DeliverAlertService();
return await service.call(payload.alertId);
},
},
"v3.performDeploymentAlerts": {
priority: 0,
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformDeploymentAlertsService();
return await service.call(payload.deploymentId);
},
},
"v3.performBulkAction": {
priority: 0,
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformBulkActionService();
return await service.call(payload.bulkActionGroupId);
},
},
"v3.performBulkActionItem": {
priority: 0,
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformBulkActionService();
await service.performBulkActionItem(payload.bulkActionItemId);
},
},
"v3.requeueTaskRun": {
priority: 0,
maxAttempts: 3,
handler: async (payload, job) => {}, // This is now handled by redisWorker
},
"v3.retryAttempt": {
priority: 0,
maxAttempts: 3,
handler: async (payload, job) => {
const service = new RetryAttemptService();
return await service.call(payload.runId);
},
},
"v2.requeueMessage": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new RequeueV2Message();
await service.call(payload.runId);
},
},
"v3.reportUsage": {
priority: 0,
maxAttempts: 8,
handler: async (payload, job) => {
await reportInvocationUsage(
payload.orgId,
Number(payload.data.costInCents),
payload.additionalData
);
},
},
"v3.enqueueDelayedRun": {
priority: 0,
maxAttempts: 8,
handler: async (payload, job) => {
const service = new EnqueueDelayedRunService();
return await service.call(payload.runId);
},
},
"v3.expireRun": {
priority: 0,
maxAttempts: 8,
handler: async (payload, job) => {
const service = new ExpireEnqueuedRunService();
return await service.call(payload.runId);
},
},
"v3.cancelTaskAttemptDependencies": {
priority: 0,
maxAttempts: 8,
handler: async (payload, job) => {
const service = new CancelTaskAttemptDependenciesService();
return await service.call(payload.attemptId);
},
},
"v3.cancelDevSessionRuns": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new CancelDevSessionRunsService();
return await service.call(payload);
},
},
"v3.processBatchTaskRun": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new BatchTriggerV3Service(payload.strategy);
await service.processBatchTaskRun(payload);
},
},
},
});
}
function getExecutionWorkerQueue() {
return new ZodWorker({
name: "executionWorker",
prisma,
replica: $replica,
logger: logger,
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 + 1,
},
shutdownTimeoutInMs: env.GRACEFUL_SHUTDOWN_TIMEOUT,
schema: executionWorkerCatalog,
rateLimiter: executionRateLimiter,
tasks: {
performRunExecutionV2: {
priority: 0, // smaller number = higher priority
maxAttempts: 12,
handler: async (payload, job) => {
const service = new PerformRunExecutionV3Service();
await service.call({
id: payload.id,
reason: payload.reason,
resumeTaskId: payload.resumeTaskId,
isRetry: payload.isRetry,
lastAttempt: job.max_attempts === job.attempts,
});
},
},
performRunExecutionV3: {
priority: 0, // smaller number = higher priority
maxAttempts: 12,
handler: async (payload, job) => {
const service = new PerformRunExecutionV3Service();
const driftInMs = Date.now() - job.run_at.getTime();
await service.call(
{
id: payload.id,
reason: payload.reason,
isRetry: false,
lastAttempt: job.max_attempts === job.attempts,
},
driftInMs
);
},
},
},
});
}
function getTaskOperationWorkerQueue() {
return new ZodWorker({
name: "taskOperationWorker",
prisma,
replica: $replica,
logger: logger,
runnerOptions: {
connectionString: env.DATABASE_URL,
concurrency: env.TASK_OPERATION_WORKER_CONCURRENCY,
pollInterval: env.TASK_OPERATION_WORKER_POLL_INTERVAL,
noPreparedStatements: env.DATABASE_URL !== env.DIRECT_URL,
schema: env.WORKER_SCHEMA,
maxPoolSize: env.TASK_OPERATION_WORKER_CONCURRENCY + 1,
},
shutdownTimeoutInMs: env.GRACEFUL_SHUTDOWN_TIMEOUT,
schema: taskOperationWorkerCatalog,
tasks: {
performTaskOperation: {
priority: 0, // smaller number = higher priority
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformTaskOperationService();
await service.call(payload.id);
},
},
invokeEphemeralDispatcher: {
priority: 0, // smaller number = higher priority
maxAttempts: 10,
handler: async (payload, job) => {
const service = new InvokeEphemeralDispatcherService();
await service.call(payload.id, payload.eventRecordId);
},
},
},
});
}
export { executionWorker, taskOperationWorker, workerQueue };