Skip to content

Commit 3c1f9fa

Browse files
committed
feat(webapp): gracefully shut down the v3 engine behind a flag
Adds DEPRECATE_V3_ENABLED (default off). When on, triggers that resolve to v3 are rejected with a message pointing at the v4 migration guide, the legacy dev websocket is closed, the v3 shared-queue consumer won't start, and the v3 run-lifecycle background jobs (heartbeat timeout, TTL expiry, retry, resume, delayed-run enqueue, scheduled fires) become no-ops so abandoned v3 runs stop generating database load. Every gate also checks the run or project is v3, so v4 is unaffected.
1 parent a90a495 commit 3c1f9fa

13 files changed

Lines changed: 148 additions & 1 deletion
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
When the v3 engine is retired, triggering a v3 task and connecting the v3 dev CLI now fail with a clear message pointing to the v4 migration guide instead of failing opaquely. Enforcement is off by default, so self-hosted instances still running v3 are unaffected until they migrate.

apps/webapp/app/env.server.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,16 @@ const EnvironmentSchema = z
535535
// log-only mode before enforcement.
536536
DEPRECATE_V3_CLI_DEPLOYS_ENABLED: z.string().default("0"),
537537

538+
// Master switch for the v3 engine (RunEngineVersion.V1) shutdown. When
539+
// enabled it: rejects triggers that resolve to V1 (single, batch, schedule,
540+
// replay, triggerAndWait) with a graceful error pointing at the v4 migration
541+
// guide; closes the legacy `trigger dev` websocket used by v3 CLIs; and turns
542+
// the V1 run-lifecycle background jobs (heartbeat timeout, TTL expiry, retry,
543+
// resume, scheduled fires) into no-ops so abandoned V1 runs stop generating
544+
// database load. v4 (V2) is never affected (every gate also checks the run is
545+
// V1). Defaults to off so self-hosted instances still on V1 keep working.
546+
DEPRECATE_V3_ENABLED: z.string().default("0"),
547+
538548
OBJECT_STORE_BASE_URL: z.string().optional(),
539549
OBJECT_STORE_BUCKET: z.string().optional(),
540550
OBJECT_STORE_ACCESS_KEY_ID: z.string().optional(),
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { env } from "~/env.server";
2+
3+
/**
4+
* Graceful sunset of the v3 engine (RunEngineVersion.V1).
5+
*
6+
* v3 maps to engine V1 (MarQS + Graphile); v4 is engine V2 (run-engine). A
7+
* single master flag (DEPRECATE_V3_ENABLED, default off) gates every shutdown
8+
* behaviour so the cloud can flip the switch while self-hosted instances still
9+
* on V1 keep working until they migrate. This mirrors
10+
* DEPRECATE_V3_CLI_DEPLOYS_ENABLED, which already gates deploys.
11+
*
12+
* The flag controls three surfaces:
13+
* 1. Triggers that resolve to V1 are rejected with a graceful error.
14+
* 2. The legacy `trigger dev` websocket (v3 CLIs only) is closed.
15+
* 3. V1 run-lifecycle background jobs become no-ops to shed database load.
16+
*
17+
* Every call site also checks the run/project is actually V1, so v4 (V2) is
18+
* never affected.
19+
*/
20+
21+
export const V3_MIGRATION_URL = "https://trigger.dev/docs/migrating-from-v3";
22+
23+
export const V3_TRIGGER_DEPRECATION_MESSAGE = `Trigger.dev v3 is no longer supported. Please upgrade your project to v4 to keep triggering tasks: ${V3_MIGRATION_URL}`;
24+
25+
// Sent as a websocket close reason, which is capped at 123 bytes, so keep it short.
26+
export const V3_DEV_DEPRECATION_MESSAGE = `Trigger.dev v3 is no longer supported. Upgrade to v4: ${V3_MIGRATION_URL}`;
27+
28+
/**
29+
* Whether the v3 (engine V1) shutdown is being enforced. Guard every V1-only
30+
* code path with `isV3Disabled() && <run/project is V1>` so v4 is untouched.
31+
*/
32+
export function isV3Disabled(): boolean {
33+
return env.DEPRECATE_V3_ENABLED === "1";
34+
}

apps/webapp/app/v3/handleSocketIo.server.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { ResumeAttemptService } from "./services/resumeAttempt.server";
3636
import { UpdateFatalRunErrorService } from "./services/updateFatalRunError.server";
3737
import { WorkerGroupTokenService } from "./services/worker/workerGroupTokenService.server";
3838
import { SharedSocketConnection } from "./sharedSocketConnection";
39+
import { isV3Disabled } from "./engineDeprecation.server";
3940

4041
export const socketIo = singleton("socketIo", initalizeIoServer);
4142

@@ -426,6 +427,16 @@ function createSharedQueueConsumerNamespace(io: Server) {
426427
clientMessages: ClientToSharedQueueMessages,
427428
serverMessages: SharedQueueToClientMessages,
428429
onConnection: async (socket, handler, sender, logger) => {
430+
// v3 (engine V1) shutdown: don't start the MarQS shared-queue consumer, so no
431+
// deployed V1 runs are dequeued. This namespace is V1-only; v4 dequeues through
432+
// the run-engine worker path. This is the code-level equivalent of taking the
433+
// v3 coordinator offline.
434+
if (isV3Disabled()) {
435+
logger.warn("Refusing /shared-queue connection: v3 engine is shut down");
436+
socket.disconnect(true);
437+
return;
438+
}
439+
429440
const sharedSocketConnection = new SharedSocketConnection({
430441
// @ts-ignore - for some reason the built ZodNamespace Server type is not compatible with the Server type here, but only when doing typechecking
431442
namespace: sharedQueue.namespace,

apps/webapp/app/v3/handleWebsockets.server.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { singleton } from "../utils/singleton";
66
import { AuthenticatedSocketConnection } from "./authenticatedSocketConnection.server";
77
import { Gauge } from "prom-client";
88
import { metricsRegister } from "~/metrics.server";
9+
import { isV3Disabled, V3_DEV_DEPRECATION_MESSAGE } from "./engineDeprecation.server";
910

1011
export const wss = singleton("wss", initalizeWebSocketServer);
1112

@@ -58,6 +59,19 @@ async function handleWebSocketConnection(ws: WebSocket, req: IncomingMessage) {
5859

5960
const authenticatedEnv = authenticationResult.environment;
6061

62+
// This legacy websocket is only used by the v3 `trigger dev` CLI (v4 uses a
63+
// different dev transport). When the v3 shutdown is on, close it with a
64+
// graceful reason instead of letting the CLI sit connected with no work.
65+
if (isV3Disabled()) {
66+
logger.warn("Rejected deprecated v3 dev CLI websocket connection", {
67+
environmentId: authenticatedEnv.id,
68+
projectId: authenticatedEnv.projectId,
69+
organizationId: authenticatedEnv.organizationId,
70+
});
71+
ws.close(1008, V3_DEV_DEPRECATION_MESSAGE);
72+
return;
73+
}
74+
6175
const authenticatedConnection = new AuthenticatedSocketConnection(
6276
ws,
6377
authenticatedEnv,

apps/webapp/app/v3/scheduleEngine.server.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { TriggerTaskService } from "./services/triggerTask.server";
1010
import { meter, tracer } from "./tracer.server";
1111
import { workerQueue } from "~/services/worker.server";
1212
import { ServiceValidationError } from "./services/common.server";
13+
import { isV3Disabled } from "./engineDeprecation.server";
1314

1415
export const scheduleEngine = singleton("ScheduleEngine", createScheduleEngine);
1516

@@ -84,6 +85,18 @@ function createScheduleEngine() {
8485
exactScheduleTime,
8586
}) => {
8687
try {
88+
// v3 (engine V1) shutdown: skip firing schedules for V1 projects so the
89+
// cron doesn't keep doing trigger work just to be rejected. Return success
90+
// so the schedule engine treats it as handled and doesn't retry. v4 is
91+
// unaffected.
92+
if (isV3Disabled() && environment.project.engine === "V1") {
93+
logger.debug("[ScheduleEngine] Skipping scheduled fire for shut-down v3 project", {
94+
taskIdentifier,
95+
scheduleId,
96+
});
97+
return { success: true };
98+
}
99+
87100
// This will trigger either v1 or v2 depending on the engine of the project
88101
const triggerService = new TriggerTaskService();
89102

apps/webapp/app/v3/services/enqueueDelayedRun.server.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { commonWorker } from "../commonWorker.server";
55
import { BaseService } from "./baseService.server";
66
import { enqueueRun } from "./enqueueRun.server";
77
import { ExpireEnqueuedRunService } from "./expireEnqueuedRun.server";
8+
import { isV3Disabled } from "../engineDeprecation.server";
89

910
export class EnqueueDelayedRunService extends BaseService {
1011
public static async enqueue(runId: string, runAt?: Date) {
@@ -75,6 +76,12 @@ export class EnqueueDelayedRunService extends BaseService {
7576
return;
7677
}
7778

79+
// v3 (engine V1) shutdown: don't enqueue delayed V1 runs into MarQS. v4 is unaffected.
80+
if (isV3Disabled() && run.engine === "V1") {
81+
logger.debug("[EnqueueDelayedRunService] Skipping enqueue for shut-down v3 run", { runId });
82+
return;
83+
}
84+
7885
if (run.status !== "DELAYED") {
7986
logger.debug("Delayed run cannot be enqueued because it's not in DELAYED status", {
8087
run,

apps/webapp/app/v3/services/expireEnqueuedRun.server.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { BaseService } from "./baseService.server";
55
import { FinalizeTaskRunService } from "./finalizeTaskRun.server";
66
import { tryCatch } from "@trigger.dev/core/utils";
77
import { getEventRepositoryForStore } from "../eventRepository/index.server";
8+
import { isV3Disabled } from "../engineDeprecation.server";
89

910
export class ExpireEnqueuedRunService extends BaseService {
1011
public static async ack(runId: string, tx?: PrismaClientOrTransaction) {
@@ -48,6 +49,12 @@ export class ExpireEnqueuedRunService extends BaseService {
4849
return;
4950
}
5051

52+
// v3 (engine V1) shutdown: skip expiring abandoned V1 runs. v4 is unaffected.
53+
if (isV3Disabled() && run.engine === "V1") {
54+
logger.debug("[ExpireEnqueuedRunService] Skipping expiry for shut-down v3 run", { runId });
55+
return;
56+
}
57+
5158
if (run.status !== "PENDING") {
5259
logger.debug("Run cannot be expired because it's not in PENDING status", {
5360
run,

apps/webapp/app/v3/services/resumeBatchRun.server.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { BaseService } from "./baseService.server";
55
import { logger } from "~/services/logger.server";
66
import { BatchTaskRun } from "@trigger.dev/database";
77
import { workerQueue } from "~/services/worker.server";
8+
import { isV3Disabled } from "../engineDeprecation.server";
89

910
const finishedBatchRunStatuses = ["COMPLETED", "FAILED", "CANCELED"];
1011

@@ -43,6 +44,14 @@ export class ResumeBatchRunService extends BaseService {
4344
return "ERROR";
4445
}
4546

47+
// v3 (engine V1) shutdown: don't resume batches for abandoned V1 projects. v4 is unaffected.
48+
if (isV3Disabled() && batchRun.runtimeEnvironment.project.engine === "V1") {
49+
logger.debug("[ResumeBatchRunService] Skipping resume for shut-down v3 batch", {
50+
batchRunId,
51+
});
52+
return "ERROR";
53+
}
54+
4655
if (batchRun.batchVersion === "v3") {
4756
return await this.#handleV3BatchRun(batchRun);
4857
} else {

apps/webapp/app/v3/services/resumeTaskDependency.server.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { logger } from "~/services/logger.server";
33
import { marqs } from "~/v3/marqs/index.server";
44
import { commonWorker } from "../commonWorker.server";
55
import { BaseService } from "./baseService.server";
6+
import { isV3Disabled } from "../engineDeprecation.server";
67

78
export class ResumeTaskDependencyService extends BaseService {
89
public async call(dependencyId: string, sourceTaskAttemptId: string) {
@@ -32,6 +33,14 @@ export class ResumeTaskDependencyService extends BaseService {
3233
return;
3334
}
3435

36+
// v3 (engine V1) shutdown: don't resume dependencies for abandoned V1 runs. v4 is unaffected.
37+
if (isV3Disabled() && dependency.taskRun.engine === "V1") {
38+
logger.debug("[ResumeTaskDependencyService] Skipping resume for shut-down v3 run", {
39+
dependencyId,
40+
});
41+
return;
42+
}
43+
3544
if (dependency.taskRun.runtimeEnvironment.type === "DEVELOPMENT") {
3645
return;
3746
}

0 commit comments

Comments
 (0)