Skip to content

Commit 2a7beda

Browse files
myftijaTrigger.dev RepoOps
authored andcommitted
feat: stamp the worker cell and lane on runs at attempt start
Records, per run, which worker cluster (cell) and lane (`container` or `microvm`) the most recent attempt executed in, so runs can be attributed to the cluster that served them and queried by it. The supervisor adds both values to every attempt start it proxies for a runner, overriding anything the runner sends. The engine merges them into the run's existing `annotations` inside the update that already starts the attempt, replacing the previous stamp as a unit. Replication projects them into two new `task_runs_v2` columns, `worker_cell` and `worker_lane`. The cell name comes from the new optional `TRIGGER_WORKER_CELL_NAME` supervisor env var (helm: `supervisor.config.cellName`); the lane is derived from the selected backend. Both also go into the worker instance metadata sent on connect, and admins see them on the run inspector. Neither is exposed to customers. Mono-RevId: 87b2c6a5ce7724b4d32c2826fc74c266e3604c3a
1 parent 93465d8 commit 2a7beda

36 files changed

Lines changed: 937 additions & 34 deletions

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ apps/**/public/build
6464
.husky
6565
/packages/react-hooks/src/package.json
6666
/packages/core/src/package.json
67+
/packages/build/src/package.json
6768
/packages/trigger-sdk/src/package.json
6869
/packages/python/src/package.json
6970
**/.claude/settings.local.json

‎apps/supervisor/src/env.test.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,21 @@ describe("Env superRefine - backpressure source awareness", () => {
184184
).toThrow();
185185
});
186186
});
187+
188+
describe("worker cell name", () => {
189+
it("treats an empty value as unset", () => {
190+
expect(
191+
Env.parse({ ...base, TRIGGER_WORKER_CELL_NAME: "" }).TRIGGER_WORKER_CELL_NAME
192+
).toBeUndefined();
193+
});
194+
195+
it("keeps a well-formed name", () => {
196+
expect(
197+
Env.parse({ ...base, TRIGGER_WORKER_CELL_NAME: "us-west-2.cell_1" }).TRIGGER_WORKER_CELL_NAME
198+
).toBe("us-west-2.cell_1");
199+
});
200+
201+
it("refuses a malformed name at boot", () => {
202+
expect(() => Env.parse({ ...base, TRIGGER_WORKER_CELL_NAME: "bad/name" })).toThrow();
203+
});
204+
});

‎apps/supervisor/src/env.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ export const Env = z
1414
.object({
1515
// This will come from `spec.nodeName` in k8s
1616
TRIGGER_WORKER_INSTANCE_NAME: z.string().default(randomUUID()),
17+
// Name of the worker cluster (cell) this supervisor serves. Stamped on every run it starts.
18+
TRIGGER_WORKER_CELL_NAME: z.preprocess(
19+
(value) => (value === "" ? undefined : value),
20+
z
21+
.string()
22+
.regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/)
23+
.optional()
24+
),
1725
TRIGGER_WORKER_HEARTBEAT_INTERVAL_SECONDS: z.coerce.number().default(30),
1826

1927
// Opt-in, dev-only: stream this process's logs over a local telnet/TCP socket on this port.

‎apps/supervisor/src/index.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
RunnerRestoreInformer,
2424
settleRestoreFailure,
2525
} from "./workloadManager/runCrd.js";
26+
import { workerLaneFor, type WorkloadManagerBackend } from "./workerLane.js";
2627
import { RestoreWatcher, type RestoreFailure } from "./services/restoreWatcher.js";
2728
import { DockerWorkloadManager } from "./workloadManager/docker.js";
2829
import {
@@ -99,7 +100,7 @@ class ManagedSupervisor {
99100
private readonly metricsServer?: HttpServer;
100101
private readonly workloadServer: WorkloadServer;
101102
private readonly workloadManager: WorkloadManager;
102-
private readonly workloadManagerBackend: "kubernetes" | "run-crd" | "docker";
103+
private readonly workloadManagerBackend: WorkloadManagerBackend;
103104
private readonly runCrdManager?: RunCrdWorkloadManager;
104105
private readonly restoreInformer?: RunnerRestoreInformer;
105106
private readonly restoreWatcher?: RestoreWatcher;
@@ -339,7 +340,13 @@ class ManagedSupervisor {
339340

340341
const workerToken = getWorkerToken();
341342

343+
const workerStamp = {
344+
workerCell: env.TRIGGER_WORKER_CELL_NAME,
345+
workerLane: workerLaneFor(this.workloadManagerBackend, env.KUBERNETES_RUNNER_RUNTIME),
346+
};
347+
342348
this.workerSession = new SupervisorSession({
349+
metadata: workerStamp,
343350
resolveResponseSchema,
344351
workerToken,
345352
apiUrl: env.TRIGGER_API_URL,
@@ -567,6 +574,7 @@ class ManagedSupervisor {
567574

568575
// Responds to workload requests only
569576
this.workloadServer = new WorkloadServer({
577+
workerStamp,
570578
port: env.TRIGGER_WORKLOAD_API_PORT_INTERNAL,
571579
host: env.TRIGGER_WORKLOAD_API_HOST_INTERNAL,
572580
workerClient: this.workerSession.httpClient,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { describe, expect, it } from "vitest";
2+
import { workerLaneFor } from "./workerLane.js";
3+
4+
describe("workerLaneFor", () => {
5+
it("follows the configured runtime on the run-crd backend", () => {
6+
expect(workerLaneFor("run-crd", "microvm")).toBe("microvm");
7+
expect(workerLaneFor("run-crd", "container")).toBe("container");
8+
});
9+
10+
it("is always container on the pod and docker backends", () => {
11+
expect(workerLaneFor("kubernetes", "microvm")).toBe("container");
12+
expect(workerLaneFor("docker", "microvm")).toBe("container");
13+
});
14+
});

‎apps/supervisor/src/workerLane.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { RunnerRuntime } from "./workloadManager/runCrd.js";
2+
3+
export type WorkloadManagerBackend = "kubernetes" | "run-crd" | "docker";
4+
5+
/** The isolation lane every run started by a supervisor on this backend executes in. */
6+
export function workerLaneFor(
7+
backend: WorkloadManagerBackend,
8+
runnerRuntime: RunnerRuntime
9+
): RunnerRuntime {
10+
switch (backend) {
11+
case "run-crd":
12+
return runnerRuntime;
13+
case "kubernetes":
14+
case "docker":
15+
return "container";
16+
}
17+
}

‎apps/supervisor/src/workloadServer/index.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ type WorkloadServerOptions = {
113113
wideEventOpts: WideEventOptions;
114114
/** When true, high-frequency HTTP routes also emit wide events. */
115115
wideEventsNoisyRoutes: boolean;
116+
/** Where runs started through this server execute. Overrides anything the runner sends. */
117+
workerStamp?: { workerCell?: string; workerLane?: string };
116118
};
117119

118120
export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
@@ -142,6 +144,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
142144
>();
143145

144146
private readonly workerClient: SupervisorHttpClient;
147+
private readonly workerStamp: { workerCell?: string; workerLane?: string };
145148

146149
constructor(opts: WorkloadServerOptions) {
147150
super();
@@ -150,6 +153,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
150153
const port = opts.port;
151154

152155
this.workerClient = opts.workerClient;
156+
this.workerStamp = opts.workerStamp ?? {};
153157
this.checkpointClient = opts.checkpointClient;
154158
this.wideEventOpts = opts.wideEventOpts;
155159
this.wideEventsNoisyRoutes = opts.wideEventsNoisyRoutes;
@@ -402,7 +406,11 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
402406
const startResponse = await this.workerClient.startRunAttempt(
403407
params.runFriendlyId,
404408
params.snapshotFriendlyId,
405-
body,
409+
{
410+
...body,
411+
workerCell: this.workerStamp.workerCell,
412+
workerLane: this.workerStamp.workerLane,
413+
},
406414
this.runnerIdFromRequest(req),
407415
auth.environmentId
408416
);
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { WORKLOAD_HEADERS } from "@trigger.dev/core/v3/workers";
2+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
3+
4+
vi.mock("std-env", () => ({
5+
env: {
6+
TRIGGER_API_URL: "http://localhost:3030",
7+
TRIGGER_WORKER_TOKEN: "test-token",
8+
MANAGED_WORKER_SECRET: "test-secret",
9+
OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4318",
10+
},
11+
}));
12+
13+
const { WorkloadServer } = await import("./index.js");
14+
15+
const PORT = 18734;
16+
const BASE = `http://127.0.0.1:${PORT}`;
17+
18+
const startRunAttempt = vi.fn(async () => ({
19+
success: true as const,
20+
data: { envVars: {} },
21+
}));
22+
23+
const workerClient = { startRunAttempt } as any;
24+
25+
let server: InstanceType<typeof WorkloadServer>;
26+
27+
beforeAll(async () => {
28+
server = new WorkloadServer({
29+
port: PORT,
30+
workerClient,
31+
wideEventOpts: { service: "supervisor", env: { nodeId: "test" }, enabled: false },
32+
wideEventsNoisyRoutes: false,
33+
workerStamp: { workerCell: "cell-a", workerLane: "microvm" },
34+
});
35+
await server.start();
36+
});
37+
38+
afterAll(async () => {
39+
await server.stop();
40+
});
41+
42+
function startAttempt(body: Record<string, unknown>) {
43+
return fetch(`${BASE}/api/v1/workload-actions/runs/run_1/snapshots/snap_1/attempts/start`, {
44+
method: "POST",
45+
headers: {
46+
"content-type": "application/json",
47+
[WORKLOAD_HEADERS.RUNNER_ID]: "runner_1",
48+
},
49+
body: JSON.stringify(body),
50+
});
51+
}
52+
53+
describe("WorkloadServer attempt start", () => {
54+
it("forwards the runner body with the supervisor's cell and lane stamped on", async () => {
55+
const res = await startAttempt({ isWarmStart: true });
56+
57+
expect(res.status).toBe(200);
58+
expect(startRunAttempt).toHaveBeenLastCalledWith(
59+
"run_1",
60+
"snap_1",
61+
{ isWarmStart: true, workerCell: "cell-a", workerLane: "microvm" },
62+
"runner_1",
63+
undefined
64+
);
65+
});
66+
67+
it("does not let the runner choose its own cell or lane", async () => {
68+
const res = await startAttempt({ workerCell: "spoofed", workerLane: "container" });
69+
70+
expect(res.status).toBe(200);
71+
expect(startRunAttempt).toHaveBeenLastCalledWith(
72+
"run_1",
73+
"snap_1",
74+
{ workerCell: "cell-a", workerLane: "microvm" },
75+
"runner_1",
76+
undefined
77+
);
78+
});
79+
});

‎apps/webapp/app/presenters/v3/SpanPresenter.server.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
prettyPrintPacket,
55
RunAnnotations,
66
SemanticInternalAttributes,
7+
WorkerStampAnnotations,
78
type TaskRunContext,
89
TaskRunError,
910
TriggerTraceContext,
@@ -325,6 +326,7 @@ export class SpanPresenter extends BasePresenter {
325326
const externalTraceId = this.#getExternalTraceId(run.traceContext);
326327

327328
const taskKind = RunAnnotations.safeParse(run.annotations).data?.taskKind;
329+
const workerStamp = WorkerStampAnnotations.safeParse(run.annotations).data;
328330
const isAgentRun = taskKind === "AGENT";
329331
const isScheduled = taskKind === "SCHEDULED";
330332
const isWebhook = taskKind === "WEBHOOK";
@@ -467,6 +469,8 @@ export class SpanPresenter extends BasePresenter {
467469
engine: run.engine,
468470
region,
469471
workerQueue: run.workerQueue,
472+
workerCell: workerStamp?.workerCell,
473+
workerLane: workerStamp?.workerLane,
470474
traceId: run.traceId,
471475
spanId: run.spanId,
472476
isCached: !!linkedRunId,

‎apps/webapp/app/routes/engine.v1.worker-actions.runs.$runFriendlyId.snapshots.$snapshotFriendlyId.attempts.start.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export const action = createActionWorkerApiRoute(
2626
runFriendlyId,
2727
snapshotFriendlyId,
2828
isWarmStart: body.isWarmStart,
29+
workerCell: body.workerCell,
30+
workerLane: body.workerLane,
2931
runnerId,
3032
environmentId,
3133
});

0 commit comments

Comments
 (0)