175 lines
5.6 KiB
TypeScript
175 lines
5.6 KiB
TypeScript
import { ScheduleEngine } from "@internal/schedule-engine";
|
|
import type { TriggerScheduledTaskErrorType } from "@internal/schedule-engine";
|
|
import { stringifyIO } from "@trigger.dev/core/v3";
|
|
import { prisma } from "~/db.server";
|
|
import { env } from "~/env.server";
|
|
import { devPresence } from "~/presenters/v3/DevPresence.server";
|
|
import { logger } from "~/services/logger.server";
|
|
import { singleton } from "~/utils/singleton";
|
|
import { OutOfEntitlementError, TriggerTaskService } from "./services/triggerTask.server";
|
|
import { meter, tracer } from "./tracer.server";
|
|
import { workerQueue } from "~/services/worker.server";
|
|
import { ServiceValidationError } from "./services/common.server";
|
|
import { isV3Disabled } from "./engineDeprecation.server";
|
|
|
|
export const scheduleEngine = singleton("ScheduleEngine", createScheduleEngine);
|
|
|
|
export type { ScheduleEngine };
|
|
|
|
async function isDevEnvironmentConnectedHandler(environmentId: string) {
|
|
const environment = await prisma.runtimeEnvironment.findFirst({
|
|
where: {
|
|
id: environmentId,
|
|
},
|
|
select: {
|
|
currentSession: {
|
|
select: {
|
|
disconnectedAt: true,
|
|
},
|
|
},
|
|
project: {
|
|
select: {
|
|
engine: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!environment) {
|
|
return false;
|
|
}
|
|
|
|
if (environment.project.engine === "V1") {
|
|
const v3Disconnected = !environment.currentSession || environment.currentSession.disconnectedAt;
|
|
|
|
return !v3Disconnected;
|
|
}
|
|
|
|
const v4Connected = await devPresence.isConnected(environmentId);
|
|
|
|
return v4Connected;
|
|
}
|
|
|
|
function createScheduleEngine() {
|
|
const engine = new ScheduleEngine({
|
|
prisma,
|
|
logLevel: env.SCHEDULE_ENGINE_LOG_LEVEL,
|
|
redis: {
|
|
host: env.SCHEDULE_WORKER_REDIS_HOST ?? "localhost",
|
|
port: env.SCHEDULE_WORKER_REDIS_PORT ?? 6379,
|
|
username: env.SCHEDULE_WORKER_REDIS_USERNAME,
|
|
password: env.SCHEDULE_WORKER_REDIS_PASSWORD,
|
|
keyPrefix: "schedule:",
|
|
enableAutoPipelining: true,
|
|
...(env.SCHEDULE_WORKER_REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }),
|
|
},
|
|
worker: {
|
|
concurrency: env.SCHEDULE_WORKER_CONCURRENCY_LIMIT,
|
|
workers: env.SCHEDULE_WORKER_CONCURRENCY_WORKERS,
|
|
tasksPerWorker: env.SCHEDULE_WORKER_CONCURRENCY_TASKS_PER_WORKER,
|
|
pollIntervalMs: env.SCHEDULE_WORKER_POLL_INTERVAL,
|
|
shutdownTimeoutMs: env.SCHEDULE_WORKER_SHUTDOWN_TIMEOUT_MS,
|
|
disabled: env.SCHEDULE_WORKER_ENABLED === "0",
|
|
},
|
|
distributionWindow: {
|
|
seconds: env.SCHEDULE_WORKER_DISTRIBUTION_WINDOW_SECONDS,
|
|
},
|
|
tracer,
|
|
meter,
|
|
onTriggerScheduledTask: async ({
|
|
taskIdentifier,
|
|
environment,
|
|
payload,
|
|
scheduleInstanceId,
|
|
scheduleId,
|
|
exactScheduleTime,
|
|
}) => {
|
|
try {
|
|
// v3 (engine V1) shutdown: skip firing schedules for V1 projects so the
|
|
// cron doesn't keep doing trigger work just to be rejected. Return success
|
|
// so the schedule engine treats it as handled and doesn't retry. v4 is
|
|
// unaffected.
|
|
if (isV3Disabled() && environment.project.engine === "V1") {
|
|
logger.debug("[ScheduleEngine] Skipping scheduled fire for shut-down v3 project", {
|
|
taskIdentifier,
|
|
scheduleId,
|
|
});
|
|
return { success: true };
|
|
}
|
|
|
|
// This will trigger either v1 or v2 depending on the engine of the project
|
|
const triggerService = new TriggerTaskService();
|
|
|
|
const payloadPacket = await stringifyIO(payload);
|
|
|
|
logger.debug("Triggering scheduled task", {
|
|
taskIdentifier,
|
|
environment,
|
|
payload,
|
|
scheduleInstanceId,
|
|
scheduleId,
|
|
exactScheduleTime,
|
|
});
|
|
|
|
const result = await triggerService.call(
|
|
taskIdentifier,
|
|
environment,
|
|
{ payload: payloadPacket.data, options: { payloadType: payloadPacket.dataType } },
|
|
{
|
|
customIcon: "scheduled",
|
|
scheduleId,
|
|
scheduleInstanceId,
|
|
queueTimestamp: exactScheduleTime,
|
|
overrideCreatedAt: exactScheduleTime,
|
|
triggerSource: "schedule",
|
|
triggerAction: "trigger",
|
|
}
|
|
);
|
|
|
|
return { success: !!result };
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
let errorType: TriggerScheduledTaskErrorType = "SYSTEM_ERROR";
|
|
|
|
if (
|
|
error instanceof ServiceValidationError &&
|
|
errorMessage.includes("queue size limit for this environment has been reached")
|
|
) {
|
|
errorType = "QUEUE_LIMIT";
|
|
} else if (error instanceof OutOfEntitlementError) {
|
|
// The org is out of entitlements. This is an expected outcome, not a
|
|
// system error, so the engine logs it as a warning rather than
|
|
// reporting it as an error.
|
|
errorType = "OUT_OF_ENTITLEMENTS";
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
error: errorMessage,
|
|
errorType,
|
|
};
|
|
}
|
|
},
|
|
isDevEnvironmentConnectedHandler: isDevEnvironmentConnectedHandler,
|
|
onRegisterScheduleInstance: removeDeprecatedWorkerQueueItem,
|
|
});
|
|
|
|
return engine;
|
|
}
|
|
|
|
async function removeDeprecatedWorkerQueueItem(instanceId: string) {
|
|
// We need to dequeue the instance from the existing workerQueue
|
|
try {
|
|
await workerQueue.dequeue(`scheduled-task-instance:${instanceId}`);
|
|
|
|
logger.debug("Removed deprecated worker queue item", {
|
|
instanceId,
|
|
});
|
|
} catch (error) {
|
|
logger.error("Error dequeuing scheduled task instance from deprecated queue", {
|
|
instanceId,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
}
|