v3: machine config (#978)

* add and use machine config

* assign tasks to worker nodes only

* add secure flag to zod connection

* changeset

* add pre stop hook

* don't use secure connection by default

* pass more identifiers to provider and apply labels
This commit is contained in:
nicktrn
2024-03-28 14:21:49 +00:00
committed by GitHub
parent 719c0a0b94
commit b66d5525ef
22 changed files with 332 additions and 135 deletions
+8
View File
@@ -0,0 +1,8 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/core-apps": patch
"trigger.dev": patch
"@trigger.dev/core": patch
---
add machine config and secure zod connection
+2 -1
View File
@@ -1,3 +1,4 @@
HTTP_SERVER_PORT=8020 HTTP_SERVER_PORT=8020
PLATFORM_ENABLED=true PLATFORM_ENABLED=true
PLATFORM_WS_PORT=3030 PLATFORM_WS_PORT=3030
SECURE_CONNECTION=false
+2
View File
@@ -26,6 +26,7 @@ const PLATFORM_ENABLED = ["1", "true"].includes(process.env.PLATFORM_ENABLED ??
const PLATFORM_HOST = process.env.PLATFORM_HOST || "127.0.0.1"; const PLATFORM_HOST = process.env.PLATFORM_HOST || "127.0.0.1";
const PLATFORM_WS_PORT = process.env.PLATFORM_WS_PORT || 3030; const PLATFORM_WS_PORT = process.env.PLATFORM_WS_PORT || 3030;
const PLATFORM_SECRET = process.env.PLATFORM_SECRET || "coordinator-secret"; const PLATFORM_SECRET = process.env.PLATFORM_SECRET || "coordinator-secret";
const SECURE_CONNECTION = ["1", "true"].includes(process.env.SECURE_CONNECTION ?? "false");
const logger = new SimpleLogger(`[${NODE_NAME}]`); const logger = new SimpleLogger(`[${NODE_NAME}]`);
@@ -365,6 +366,7 @@ class TaskCoordinator {
namespace: "coordinator", namespace: "coordinator",
host: PLATFORM_HOST, host: PLATFORM_HOST,
port: Number(PLATFORM_WS_PORT), port: Number(PLATFORM_WS_PORT),
secure: SECURE_CONNECTION,
clientMessages: CoordinatorToPlatformMessages, clientMessages: CoordinatorToPlatformMessages,
serverMessages: PlatformToCoordinatorMessages, serverMessages: PlatformToCoordinatorMessages,
authToken: PLATFORM_SECRET, authToken: PLATFORM_SECRET,
+1
View File
@@ -2,6 +2,7 @@ HTTP_SERVER_PORT=8050
PLATFORM_WS_PORT=3030 PLATFORM_WS_PORT=3030
PLATFORM_SECRET=provider-secret PLATFORM_SECRET=provider-secret
SECURE_CONNECTION=false
# Use this if you are on macOS # Use this if you are on macOS
# COORDINATOR_HOST="host.docker.internal" # COORDINATOR_HOST="host.docker.internal"
+32 -13
View File
@@ -8,6 +8,7 @@ import {
TaskOperationsIndexOptions, TaskOperationsIndexOptions,
} from "@trigger.dev/core-apps"; } from "@trigger.dev/core-apps";
import { setTimeout } from "node:timers/promises"; import { setTimeout } from "node:timers/promises";
import { PostStartCauses, PreStopCauses } from "@trigger.dev/core/v3";
const MACHINE_NAME = process.env.MACHINE_NAME || "local"; const MACHINE_NAME = process.env.MACHINE_NAME || "local";
const COORDINATOR_PORT = process.env.COORDINATOR_PORT || 8020; const COORDINATOR_PORT = process.env.COORDINATOR_PORT || 8020;
@@ -190,6 +191,9 @@ class DockerTaskOperations implements TaskOperations {
async delete(opts: { runId: string }) { async delete(opts: { runId: string }) {
await this.#initialize(); await this.#initialize();
const containerName = this.#getRunContainerName(opts.runId);
await this.#sendPreStop(containerName);
logger.log("noop: delete"); logger.log("noop: delete");
} }
@@ -208,6 +212,26 @@ class DockerTaskOperations implements TaskOperations {
} }
async #sendPostStart(containerName: string): Promise<void> { async #sendPostStart(containerName: string): Promise<void> {
try {
const port = await this.#getHttpServerPort(containerName);
logger.debug(await this.#runLifecycleCommand(containerName, port, "postStart", "restore"));
} catch (error) {
logger.error("postStart error", { error });
throw new Error("postStart command failed");
}
}
async #sendPreStop(containerName: string): Promise<void> {
try {
const port = await this.#getHttpServerPort(containerName);
logger.debug(await this.#runLifecycleCommand(containerName, port, "preStop", "terminate"));
} catch (error) {
logger.error("preStop error", { error });
throw new Error("preStop command failed");
}
}
async #getHttpServerPort(containerName: string): Promise<number> {
// We first get the correct port, which is random during dev as we run with host networking and need to avoid clashes // We first get the correct port, which is random during dev as we run with host networking and need to avoid clashes
// FIXME: Skip this in prod // FIXME: Skip this in prod
const logs = logger.debug(await $`docker logs ${containerName}`); const logs = logger.debug(await $`docker logs ${containerName}`);
@@ -219,19 +243,14 @@ class DockerTaskOperations implements TaskOperations {
throw new Error("failed to extract port from logs"); throw new Error("failed to extract port from logs");
} }
try { return port;
logger.debug(await this.#runLifecycleCommand(containerName, port, "postStart", "restore"));
} catch (error) {
logger.error("postStart error", { error });
throw new Error("postStart command failed");
}
} }
async #runLifecycleCommand( async #runLifecycleCommand<THookType extends "postStart" | "preStop">(
containerName: string, containerName: string,
port: number, port: number,
type: "postStart" | "preStop", type: THookType,
cause: "index" | "create" | "restore", cause: THookType extends "postStart" ? PostStartCauses : PreStopCauses,
retryCount = 0 retryCount = 0
): Promise<ExecaChildProcess> { ): Promise<ExecaChildProcess> {
try { try {
@@ -244,15 +263,15 @@ class DockerTaskOperations implements TaskOperations {
`127.0.0.1:${port}/${type}?cause=${cause}`, `127.0.0.1:${port}/${type}?cause=${cause}`,
]); ]);
} catch (error: any) { } catch (error: any) {
if (retryCount < 6) { if (type === "postStart" && retryCount < 6) {
logger.debug("retriable postStart error", { retryCount, message: error?.message }); logger.debug(`retriable ${type} error`, { retryCount, message: error?.message });
await setTimeout(exponentialBackoff(retryCount + 1, 2, 50, 1150, 50)); await setTimeout(exponentialBackoff(retryCount + 1, 2, 50, 1150, 50));
return this.#runLifecycleCommand(containerName, port, type, cause, retryCount + 1); return this.#runLifecycleCommand(containerName, port, type, cause, retryCount + 1);
} }
logger.error("final postStart error", { message: error?.message }); logger.error(`final ${type} error`, { message: error?.message });
throw new Error(`postStart command failed after ${retryCount - 1} retries`); throw new Error(`${type} command failed after ${retryCount - 1} retries`);
} }
} }
} }
+1
View File
@@ -2,6 +2,7 @@ HTTP_SERVER_PORT=8060
PLATFORM_WS_PORT=3030 PLATFORM_WS_PORT=3030
PLATFORM_SECRET=provider-secret PLATFORM_SECRET=provider-secret
SECURE_CONNECTION=false
# Use this if you are on macOS # Use this if you are on macOS
# COORDINATOR_HOST="host.docker.internal" # COORDINATOR_HOST="host.docker.internal"
+70 -9
View File
@@ -7,6 +7,7 @@ import {
TaskOperationsIndexOptions, TaskOperationsIndexOptions,
TaskOperationsRestoreOptions, TaskOperationsRestoreOptions,
} from "@trigger.dev/core-apps"; } from "@trigger.dev/core-apps";
import { Machine, PostStartCauses, PreStopCauses, EnvironmentType } from "@trigger.dev/core/v3";
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
const RUNTIME_ENV = process.env.KUBERNETES_PORT ? "kubernetes" : "local"; const RUNTIME_ENV = process.env.KUBERNETES_PORT ? "kubernetes" : "local";
@@ -55,6 +56,12 @@ class KubernetesTaskOperations implements TaskOperations {
metadata: { metadata: {
labels: { labels: {
app: "task-index", app: "task-index",
"app.kubernetes.io/part-of": "trigger-worker",
"app.kubernetes.io/component": "index",
env: opts.envId,
envtype: this.#envTypeToLabelValue(opts.envType),
org: opts.orgId,
project: opts.projectId,
}, },
}, },
spec: { spec: {
@@ -64,6 +71,9 @@ class KubernetesTaskOperations implements TaskOperations {
name: "registry-trigger", name: "registry-trigger",
}, },
], ],
nodeSelector: {
nodetype: "worker",
},
containers: [ containers: [
{ {
name: this.#getIndexContainerName(opts.shortCode), name: this.#getIndexContainerName(opts.shortCode),
@@ -79,6 +89,13 @@ class KubernetesTaskOperations implements TaskOperations {
// memory: "50Mi", // memory: "50Mi",
// }, // },
// }, // },
lifecycle: {
preStop: {
exec: {
command: this.#getLifecycleCommand("preStop", "terminate"),
},
},
},
env: [ env: [
{ {
name: "DEBUG", name: "DEBUG",
@@ -151,6 +168,13 @@ class KubernetesTaskOperations implements TaskOperations {
namespace: this.#namespace.metadata.name, namespace: this.#namespace.metadata.name,
labels: { labels: {
app: "task-run", app: "task-run",
"app.kubernetes.io/part-of": "trigger-worker",
"app.kubernetes.io/component": "create",
env: opts.envId,
envtype: this.#envTypeToLabelValue(opts.envType),
org: opts.orgId,
project: opts.projectId,
run: opts.runId,
}, },
}, },
spec: { spec: {
@@ -160,6 +184,9 @@ class KubernetesTaskOperations implements TaskOperations {
name: "registry-trigger", name: "registry-trigger",
}, },
], ],
nodeSelector: {
nodetype: "worker",
},
containers: [ containers: [
{ {
name: this.#getRunContainerName(opts.runId), name: this.#getRunContainerName(opts.runId),
@@ -169,9 +196,9 @@ class KubernetesTaskOperations implements TaskOperations {
containerPort: 8000, containerPort: 8000,
}, },
], ],
// resources: { resources: {
// limits: opts.machine, limits: this.#getResourcesFromMachineConfig(opts.machine),
// }, },
lifecycle: { lifecycle: {
postStart: { postStart: {
exec: { exec: {
@@ -180,7 +207,7 @@ class KubernetesTaskOperations implements TaskOperations {
}, },
preStop: { preStop: {
exec: { exec: {
command: this.#getLifecycleCommand("preStop", "create"), command: this.#getLifecycleCommand("preStop", "terminate"),
}, },
}, },
}, },
@@ -262,6 +289,14 @@ class KubernetesTaskOperations implements TaskOperations {
namespace: this.#namespace.metadata.name, namespace: this.#namespace.metadata.name,
labels: { labels: {
app: "task-run", app: "task-run",
"app.kubernetes.io/part-of": "trigger-worker",
"app.kubernetes.io/component": "restore",
env: opts.envId,
envtype: this.#envTypeToLabelValue(opts.envType),
org: opts.orgId,
project: opts.projectId,
run: opts.runId,
checkpoint: opts.checkpointId,
}, },
}, },
spec: { spec: {
@@ -271,6 +306,9 @@ class KubernetesTaskOperations implements TaskOperations {
name: "registry-trigger", name: "registry-trigger",
}, },
], ],
nodeSelector: {
nodetype: "worker",
},
initContainers: [ initContainers: [
{ {
name: "pull-base-image", name: "pull-base-image",
@@ -309,9 +347,9 @@ class KubernetesTaskOperations implements TaskOperations {
containerPort: 8000, containerPort: 8000,
}, },
], ],
// resources: { resources: {
// limits: opts.machine, limits: this.#getResourcesFromMachineConfig(opts.machine),
// }, },
lifecycle: { lifecycle: {
postStart: { postStart: {
exec: { exec: {
@@ -320,7 +358,7 @@ class KubernetesTaskOperations implements TaskOperations {
}, },
preStop: { preStop: {
exec: { exec: {
command: this.#getLifecycleCommand("preStop", "restore"), command: this.#getLifecycleCommand("preStop", "terminate"),
}, },
}, },
}, },
@@ -355,7 +393,30 @@ class KubernetesTaskOperations implements TaskOperations {
await this.#getPod(opts.runId, this.#namespace); await this.#getPod(opts.runId, this.#namespace);
} }
#getLifecycleCommand(type: "postStart" | "preStop", cause: "index" | "create" | "restore") { #envTypeToLabelValue(type: EnvironmentType) {
switch (type) {
case "PRODUCTION":
return "prod";
case "STAGING":
return "stg";
case "DEVELOPMENT":
return "dev";
case "PREVIEW":
return "preview";
}
}
#getResourcesFromMachineConfig(config: Machine) {
return {
cpu: `${config.cpu}`,
memory: `${config.memory}G`,
};
}
#getLifecycleCommand<THookType extends "postStart" | "preStop">(
type: THookType,
cause: THookType extends "postStart" ? PostStartCauses : PreStopCauses
) {
return ["/bin/sh", "-c", `sleep 1; wget -q -O- 127.0.0.1:8000/${type}?cause=${cause}`]; return ["/bin/sh", "-c", `sleep 1; wget -q -O- 127.0.0.1:8000/${type}?cause=${cause}`];
} }
@@ -1,5 +1,6 @@
import { Context, ROOT_CONTEXT, Span, SpanKind, context, trace } from "@opentelemetry/api"; import { Context, ROOT_CONTEXT, Span, SpanKind, context, trace } from "@opentelemetry/api";
import { import {
Machine,
ProdTaskRunExecution, ProdTaskRunExecution,
ProdTaskRunExecutionPayload, ProdTaskRunExecutionPayload,
TaskRunError, TaskRunError,
@@ -438,10 +439,27 @@ export class SharedQueueConsumer {
queueId: queue.id, queueId: queue.id,
runtimeEnvironmentId: environment.id, runtimeEnvironmentId: environment.id,
}, },
include: {
backgroundWorkerTask: true,
},
}); });
const isRetry = taskRunAttempt.number > 1; const isRetry = taskRunAttempt.number > 1;
const { machineConfig } = taskRunAttempt.backgroundWorkerTask;
const machine = Machine.safeParse(machineConfig ?? {});
if (!machine.success) {
logger.error("Failed to parse machine config", {
queueMessage: message.data,
messageId: message.messageId,
attemptId: taskRunAttempt.id,
machineConfig,
});
await this.#ackAndDoMoreWork(message.messageId);
return;
}
try { try {
if (messageBody.data.checkpointEventId) { if (messageBody.data.checkpointEventId) {
const restoreService = new RestoreCheckpointService(); const restoreService = new RestoreCheckpointService();
@@ -470,11 +488,16 @@ export class SharedQueueConsumer {
backgroundWorkerId: deployment.worker.friendlyId, backgroundWorkerId: deployment.worker.friendlyId,
data: { data: {
type: "SCHEDULE_ATTEMPT", type: "SCHEDULE_ATTEMPT",
id: taskRunAttempt.id,
image: deployment.imageReference, image: deployment.imageReference,
envId: environment.id,
runId: taskRunAttempt.taskRunId,
version: deployment.version, version: deployment.version,
machine: machine.data,
// identifiers
id: taskRunAttempt.id,
envId: environment.id,
envType: environment.type,
orgId: environment.organizationId,
projectId: environment.projectId,
runId: taskRunAttempt.taskRunId,
}, },
}); });
} }
@@ -101,6 +101,7 @@ export async function createBackgroundTasks(
exportName: task.exportName, exportName: task.exportName,
retryConfig: task.retry, retryConfig: task.retry,
queueConfig: task.queue, queueConfig: task.queue,
machineConfig: task.machine,
}, },
}); });
@@ -49,9 +49,13 @@ export class IndexDeploymentService extends BaseService {
version: "v1", version: "v1",
shortCode: deployment.shortCode, shortCode: deployment.shortCode,
imageTag: deployment.imageReference, imageTag: deployment.imageReference,
envId: deployment.environmentId,
apiKey: deployment.environment.apiKey, apiKey: deployment.environment.apiKey,
apiUrl: env.APP_ORIGIN, apiUrl: env.APP_ORIGIN,
// identifiers
envId: deployment.environmentId,
envType: deployment.environment.type,
projectId: deployment.projectId,
orgId: deployment.environment.organizationId,
}); });
logger.debug("Index ACK received", { responses }); logger.debug("Index ACK received", { responses });
@@ -3,6 +3,7 @@ import { logger } from "~/services/logger.server";
import { socketIo } from "../handleSocketIo.server"; import { socketIo } from "../handleSocketIo.server";
import { CreateCheckpointRestoreEventService } from "./createCheckpointRestoreEvent.server"; import { CreateCheckpointRestoreEventService } from "./createCheckpointRestoreEvent.server";
import { BaseService } from "./baseService.server"; import { BaseService } from "./baseService.server";
import { Machine } from "@trigger.dev/core/v3";
const RESTORABLE_RUN_STATUSES: TaskRunStatus[] = ["WAITING_TO_RESUME"]; const RESTORABLE_RUN_STATUSES: TaskRunStatus[] = ["WAITING_TO_RESUME"];
const RESTORABLE_ATTEMPT_STATUSES: TaskRunAttemptStatus[] = ["PAUSED"]; const RESTORABLE_ATTEMPT_STATUSES: TaskRunAttemptStatus[] = ["PAUSED"];
@@ -30,8 +31,14 @@ export class RestoreCheckpointService extends BaseService {
attempt: { attempt: {
select: { select: {
status: true, status: true,
backgroundWorkerTask: {
select: {
machineConfig: true,
},
},
}, },
}, },
runtimeEnvironment: true,
}, },
}, },
}, },
@@ -63,17 +70,34 @@ export class RestoreCheckpointService extends BaseService {
return; return;
} }
const { machineConfig } = checkpoint.attempt.backgroundWorkerTask;
const machine = Machine.safeParse(machineConfig ?? {});
if (!machine.success) {
logger.error("Failed to parse machine config", {
attemptId: checkpoint.attemptId,
machineConfig: checkpoint.attempt.backgroundWorkerTask.machineConfig,
});
return;
}
const eventService = new CreateCheckpointRestoreEventService(this._prisma); const eventService = new CreateCheckpointRestoreEventService(this._prisma);
await eventService.restore({ checkpointId: checkpoint.id }); await eventService.restore({ checkpointId: checkpoint.id });
socketIo.providerNamespace.emit("RESTORE", { socketIo.providerNamespace.emit("RESTORE", {
version: "v1", version: "v1",
checkpointId: checkpoint.id,
runId: checkpoint.runId,
type: checkpoint.type, type: checkpoint.type,
location: checkpoint.location, location: checkpoint.location,
reason: checkpoint.reason ?? undefined, reason: checkpoint.reason ?? undefined,
imageRef: checkpoint.imageRef, imageRef: checkpoint.imageRef,
machine: machine.data,
// identifiers
checkpointId: checkpoint.id,
envId: checkpoint.runtimeEnvironment.id,
envType: checkpoint.runtimeEnvironment.type,
orgId: checkpoint.runtimeEnvironment.organizationId,
projectId: checkpoint.runtimeEnvironment.projectId,
runId: checkpoint.runId,
}); });
return checkpoint; return checkpoint;
@@ -84,6 +84,7 @@ function getTasks(): Array<TaskMetadataWithFunctions> {
filePath: (taskFile as any).filePath, filePath: (taskFile as any).filePath,
queue: (task as any).__trigger.queue, queue: (task as any).__trigger.queue,
retry: (task as any).__trigger.retry, retry: (task as any).__trigger.retry,
machine: (task as any).__trigger.machine,
fns: (task as any).__trigger.fns, fns: (task as any).__trigger.fns,
}); });
} }
@@ -1,6 +1,8 @@
import { import {
Config, Config,
CoordinatorToProdWorkerMessages, CoordinatorToProdWorkerMessages,
PostStartCauses,
PreStopCauses,
ProdWorkerToCoordinatorMessages, ProdWorkerToCoordinatorMessages,
TaskResource, TaskResource,
WaitReason, WaitReason,
@@ -567,23 +569,15 @@ class ProdWorker {
} }
case "/preStop": { case "/preStop": {
const schema = z.enum(["index", "create", "restore"]); const cause = PreStopCauses.safeParse(url.searchParams.get("cause"));
const cause = schema.safeParse(url.searchParams.get("cause"));
if (!cause.success) { if (!cause.success) {
logger.error("Failed to parse cause", { cause }); logger.error("Failed to parse cause", { cause });
return; return reply.text("Failed to parse cause", 400);
} }
switch (cause.data) { switch (cause.data) {
case "index": { case "terminate": {
break;
}
case "create": {
break;
}
case "restore": {
break; break;
} }
default: { default: {
@@ -597,13 +591,11 @@ class ProdWorker {
} }
case "/postStart": { case "/postStart": {
const schema = z.enum(["index", "create", "restore"]); const cause = PostStartCauses.safeParse(url.searchParams.get("cause"));
const cause = schema.safeParse(url.searchParams.get("cause"));
if (!cause.success) { if (!cause.success) {
logger.error("Failed to parse cause", { cause }); logger.error("Failed to parse cause", { cause });
return; return reply.text("Failed to parse cause", 400);
} }
switch (cause.data) { switch (cause.data) {
@@ -678,11 +670,7 @@ class ProdWorker {
} }
for (const task of this.#backgroundWorker.tasks) { for (const task of this.#backgroundWorker.tasks) {
taskResources.push({ taskResources.push(task);
id: task.id,
filePath: task.filePath,
exportName: task.exportName,
});
packageVersion = task.packageVersion; packageVersion = task.packageVersion;
} }
@@ -78,6 +78,7 @@ function getTasks(): Array<TaskMetadataWithFunctions> {
filePath: (taskFile as any).filePath, filePath: (taskFile as any).filePath,
queue: (task as any).__trigger.queue, queue: (task as any).__trigger.queue,
retry: (task as any).__trigger.retry, retry: (task as any).__trigger.retry,
machine: (task as any).__trigger.machine,
fns: (task as any).__trigger.fns, fns: (task as any).__trigger.fns,
}); });
} }
+70 -59
View File
@@ -2,6 +2,7 @@ import { createServer } from "node:http";
import { import {
ClientToSharedQueueMessages, ClientToSharedQueueMessages,
clientWebsocketMessages, clientWebsocketMessages,
EnvironmentType,
Machine, Machine,
PlatformToProviderMessages, PlatformToProviderMessages,
ProviderToPlatformMessages, ProviderToPlatformMessages,
@@ -18,30 +19,46 @@ const MACHINE_NAME = process.env.MACHINE_NAME || "local";
const PLATFORM_HOST = process.env.PLATFORM_HOST || "127.0.0.1"; const PLATFORM_HOST = process.env.PLATFORM_HOST || "127.0.0.1";
const PLATFORM_WS_PORT = process.env.PLATFORM_WS_PORT || 3030; const PLATFORM_WS_PORT = process.env.PLATFORM_WS_PORT || 3030;
const PLATFORM_SECRET = process.env.PLATFORM_SECRET || "provider-secret"; const PLATFORM_SECRET = process.env.PLATFORM_SECRET || "provider-secret";
const SECURE_CONNECTION = ["1", "true"].includes(process.env.SECURE_CONNECTION ?? "false");
const logger = new SimpleLogger(`[${MACHINE_NAME}]`); const logger = new SimpleLogger(`[${MACHINE_NAME}]`);
export interface TaskOperationsIndexOptions { export interface TaskOperationsIndexOptions {
shortCode: string; shortCode: string;
imageRef: string; imageRef: string;
envId: string;
apiKey: string; apiKey: string;
apiUrl: string; apiUrl: string;
// identifiers
envId: string;
envType: EnvironmentType;
orgId: string;
projectId: string;
} }
export interface TaskOperationsCreateOptions { export interface TaskOperationsCreateOptions {
runId: string;
image: string; image: string;
machine: Machine; machine: Machine;
envId: string;
version: string; version: string;
// identifiers
envId: string;
envType: EnvironmentType;
orgId: string;
projectId: string;
runId: string;
attemptId: string;
} }
export interface TaskOperationsRestoreOptions { export interface TaskOperationsRestoreOptions {
runId: string;
imageRef: string; imageRef: string;
checkpointRef: string; checkpointRef: string;
machine: Machine; machine: Machine;
// identifiers
envId: string;
envType: EnvironmentType;
orgId: string;
projectId: string;
runId: string;
checkpointId: string;
} }
export interface TaskOperations { export interface TaskOperations {
@@ -87,6 +104,7 @@ export class ProviderShell implements Provider {
namespace: "shared-queue", namespace: "shared-queue",
host: PLATFORM_HOST, host: PLATFORM_HOST,
port: Number(PLATFORM_WS_PORT), port: Number(PLATFORM_WS_PORT),
secure: SECURE_CONNECTION,
clientMessages: ClientToSharedQueueMessages, clientMessages: ClientToSharedQueueMessages,
serverMessages: SharedQueueToClientMessages, serverMessages: SharedQueueToClientMessages,
authToken: PLATFORM_SECRET, authToken: PLATFORM_SECRET,
@@ -101,11 +119,16 @@ export class ProviderShell implements Provider {
if (message.data.type === "SCHEDULE_ATTEMPT") { if (message.data.type === "SCHEDULE_ATTEMPT") {
try { try {
this.tasks.create({ this.tasks.create({
envId: message.data.envId,
runId: message.data.runId,
image: message.data.image, image: message.data.image,
machine: {}, machine: message.data.machine,
version: message.version, version: message.data.version,
// identifiers
envId: message.data.envId,
envType: message.data.envType,
orgId: message.data.orgId,
projectId: message.data.projectId,
runId: message.data.runId,
attemptId: message.data.id,
}); });
} catch (error) { } catch (error) {
logger.error("create failed", error); logger.error("create failed", error);
@@ -138,6 +161,7 @@ export class ProviderShell implements Provider {
namespace: "provider", namespace: "provider",
host: PLATFORM_HOST, host: PLATFORM_HOST,
port: Number(PLATFORM_WS_PORT), port: Number(PLATFORM_WS_PORT),
secure: SECURE_CONNECTION,
clientMessages: ProviderToPlatformMessages, clientMessages: ProviderToPlatformMessages,
serverMessages: PlatformToProviderMessages, serverMessages: PlatformToProviderMessages,
authToken: PLATFORM_SECRET, authToken: PLATFORM_SECRET,
@@ -165,9 +189,13 @@ export class ProviderShell implements Provider {
await this.tasks.index({ await this.tasks.index({
shortCode: message.shortCode, shortCode: message.shortCode,
imageRef: message.imageTag, imageRef: message.imageTag,
envId: message.envId,
apiKey: message.apiKey, apiKey: message.apiKey,
apiUrl: message.apiUrl, apiUrl: message.apiUrl,
// identifiers
envId: message.envId,
envType: message.envType,
orgId: message.orgId,
projectId: message.projectId,
}); });
} catch (error) { } catch (error) {
logger.error("index failed", error); logger.error("index failed", error);
@@ -206,13 +234,16 @@ export class ProviderShell implements Provider {
try { try {
await this.tasks.restore({ await this.tasks.restore({
runId: message.runId,
checkpointRef: message.location, checkpointRef: message.location,
machine: { machine: message.machine,
cpu: "1",
memory: "100Mi",
},
imageRef: message.imageRef, imageRef: message.imageRef,
// identifiers
envId: message.envId,
envType: message.envType,
orgId: message.orgId,
projectId: message.projectId,
runId: message.runId,
checkpointId: message.checkpointId,
}); });
} catch (error) { } catch (error) {
logger.error("restore failed", error); logger.error("restore failed", error);
@@ -230,54 +261,34 @@ export class ProviderShell implements Provider {
const reply = new HttpReply(res); const reply = new HttpReply(res);
switch (req.url) { try {
case "/health": { const url = new URL(req.url ?? "", `http://${req.headers.host}`);
return reply.text("ok");
}
case "/whoami": {
return reply.text(`${MACHINE_NAME}`);
}
case "/close": {
this.#platformSocket.close();
return reply.text("platform socket closed");
}
case "/delete": {
const body = await getTextBody(req);
await this.tasks.delete({ runId: body }); switch (url.pathname) {
case "/health": {
return reply.text("ok");
}
case "/whoami": {
return reply.text(`${MACHINE_NAME}`);
}
case "/close": {
this.#platformSocket.close();
return reply.text("platform socket closed");
}
case "/delete": {
const body = await getTextBody(req);
return reply.text(`sent delete request: ${body}`); await this.tasks.delete({ runId: body });
}
case "/invoke": { return reply.text(`sent delete request: ${body}`);
const body = await getTextBody(req); }
default: {
await this.tasks.create({ return reply.empty(404);
envId: "placeholder", }
image: body,
machine: {
cpu: "1",
memory: "100Mi",
},
runId: "<missing>",
version: "<missing>",
});
return reply.text(`sent restore request: ${body}`);
}
case "/restore": {
const body = await getTextBody(req);
const items = body.split("&");
const image = items[0];
const baseImageTag = items[1] ?? image;
// await this.tasks.restore({});
return reply.text(`sent restore request: ${body}`);
}
default: {
return reply.empty(404);
} }
} catch (error) {
logger.error("HTTP server error", { error });
reply.empty(500);
} }
}); });
+32 -3
View File
@@ -1,6 +1,29 @@
import { z } from "zod"; import { z } from "zod";
import { TaskRunExecution, TaskRunExecutionResult } from "./common"; import { TaskRunExecution, TaskRunExecutionResult } from "./common";
export const EnvironmentType = z.enum(["PRODUCTION", "STAGING", "DEVELOPMENT", "PREVIEW"])
export type EnvironmentType = z.infer<typeof EnvironmentType>;
export const MachineCpu = z
.union([z.literal(0.25), z.literal(0.5), z.literal(1), z.literal(2), z.literal(4)])
.default(0.5);
export type MachineCpu = z.infer<typeof MachineCpu>;
export const MachineMemory = z
.union([z.literal(0.25), z.literal(0.5), z.literal(1), z.literal(2), z.literal(4), z.literal(8)])
.default(1);
export type MachineMemory = z.infer<typeof MachineMemory>;
export const Machine = z.object({
version: z.literal("v1").default("v1"),
cpu: MachineCpu,
memory: MachineMemory,
});
export type Machine = z.infer<typeof Machine>;
export const TaskRunExecutionPayload = z.object({ export const TaskRunExecutionPayload = z.object({
execution: TaskRunExecution, execution: TaskRunExecution,
traceContext: z.record(z.unknown()), traceContext: z.record(z.unknown()),
@@ -39,11 +62,16 @@ export const BackgroundWorkerServerMessages = z.discriminatedUnion("type", [
}), }),
z.object({ z.object({
type: z.literal("SCHEDULE_ATTEMPT"), type: z.literal("SCHEDULE_ATTEMPT"),
id: z.string(),
image: z.string(), image: z.string(),
envId: z.string(),
runId: z.string(),
version: z.string(), version: z.string(),
machine: Machine,
// identifiers
id: z.string(), // attempt
envId: z.string(),
envType: EnvironmentType,
orgId: z.string(),
projectId: z.string(),
runId: z.string(),
}), }),
]); ]);
@@ -229,6 +257,7 @@ export const TaskMetadata = z.object({
packageVersion: z.string(), packageVersion: z.string(),
queue: QueueOptions.optional(), queue: QueueOptions.optional(),
retry: RetryOptions.optional(), retry: RetryOptions.optional(),
machine: Machine.partial().optional(),
}); });
export type TaskMetadata = z.infer<typeof TaskMetadata>; export type TaskMetadata = z.infer<typeof TaskMetadata>;
+2 -1
View File
@@ -1,5 +1,5 @@
import { z } from "zod"; import { z } from "zod";
import { QueueOptions, RetryOptions } from "./messages"; import { Machine, QueueOptions, RetryOptions } from "./messages";
export const TaskResource = z.object({ export const TaskResource = z.object({
id: z.string(), id: z.string(),
@@ -7,6 +7,7 @@ export const TaskResource = z.object({
exportName: z.string(), exportName: z.string(),
queue: QueueOptions.optional(), queue: QueueOptions.optional(),
retry: RetryOptions.optional(), retry: RetryOptions.optional(),
machine: Machine.partial().optional(),
}); });
export type TaskResource = z.infer<typeof TaskResource>; export type TaskResource = z.infer<typeof TaskResource>;
+22 -17
View File
@@ -7,9 +7,17 @@ import {
ProdTaskRunExecution, ProdTaskRunExecution,
ProdTaskRunExecutionPayload, ProdTaskRunExecutionPayload,
RetryOptions, RetryOptions,
Machine,
EnvironmentType,
} from "./messages"; } from "./messages";
import { TaskResource } from "./resources"; import { TaskResource } from "./resources";
export const PostStartCauses = z.enum(["index", "create", "restore"]);
export type PostStartCauses = z.infer<typeof PostStartCauses>;
export const PreStopCauses = z.enum(["terminate"]);
export type PreStopCauses = z.infer<typeof PreStopCauses>;
const RegexSchema = z.custom<RegExp>((val) => { const RegexSchema = z.custom<RegExp>((val) => {
try { try {
// Check to see if val is a regex // Check to see if val is a regex
@@ -42,13 +50,6 @@ export type ResolvedConfig = RequireKeys<
"triggerDirectories" | "triggerUrl" | "projectDir" | "tsconfigPath" "triggerDirectories" | "triggerUrl" | "projectDir" | "tsconfigPath"
>; >;
export const Machine = z.object({
cpu: z.string().default("1").optional(),
memory: z.string().default("500Mi").optional(),
});
export type Machine = z.infer<typeof Machine>;
export const WaitReason = z.enum(["WAIT_FOR_DURATION", "WAIT_FOR_TASK", "WAIT_FOR_BATCH"]); export const WaitReason = z.enum(["WAIT_FOR_DURATION", "WAIT_FOR_TASK", "WAIT_FOR_BATCH"]);
export type WaitReason = z.infer<typeof WaitReason>; export type WaitReason = z.infer<typeof WaitReason>;
@@ -85,9 +86,13 @@ export const PlatformToProviderMessages = {
version: z.literal("v1").default("v1"), version: z.literal("v1").default("v1"),
imageTag: z.string(), imageTag: z.string(),
shortCode: z.string(), shortCode: z.string(),
envId: z.string(),
apiKey: z.string(), apiKey: z.string(),
apiUrl: z.string(), apiUrl: z.string(),
// identifiers
envId: z.string(),
envType: EnvironmentType,
orgId: z.string(),
projectId: z.string(),
}), }),
callback: z.discriminatedUnion("success", [ callback: z.discriminatedUnion("success", [
z.object({ z.object({
@@ -103,22 +108,22 @@ export const PlatformToProviderMessages = {
}), }),
]), ]),
}, },
INVOKE: { // TODO: this should be a shared queue message instead
message: z.object({
version: z.literal("v1").default("v1"),
name: z.string(),
machine: Machine,
}),
},
RESTORE: { RESTORE: {
message: z.object({ message: z.object({
version: z.literal("v1").default("v1"), version: z.literal("v1").default("v1"),
checkpointId: z.string(),
runId: z.string(),
type: z.enum(["DOCKER", "KUBERNETES"]), type: z.enum(["DOCKER", "KUBERNETES"]),
location: z.string(), location: z.string(),
reason: z.string().optional(), reason: z.string().optional(),
imageRef: z.string(), imageRef: z.string(),
machine: Machine,
// identifiers
checkpointId: z.string(),
envId: z.string(),
envType: EnvironmentType,
orgId: z.string(),
projectId: z.string(),
runId: z.string(),
}), }),
}, },
DELETE: { DELETE: {
+13 -3
View File
@@ -263,7 +263,8 @@ interface ZodSocketConnectionOptions<
TServerMessages extends ZodSocketMessageCatalogSchema, TServerMessages extends ZodSocketMessageCatalogSchema,
> { > {
host: string; host: string;
port: number; port?: number;
secure?: boolean;
namespace: string; namespace: string;
clientMessages: TClientMessages; clientMessages: TClientMessages;
serverMessages: TServerMessages; serverMessages: TServerMessages;
@@ -302,15 +303,24 @@ export class ZodSocketConnection<
#logger: StructuredLogger; #logger: StructuredLogger;
constructor(opts: ZodSocketConnectionOptions<TClientMessages, TServerMessages>) { constructor(opts: ZodSocketConnectionOptions<TClientMessages, TServerMessages>) {
this.socket = io(`ws://${opts.host}:${opts.port}/${opts.namespace}`, { const uri = `${opts.secure ? "wss" : "ws"}://${opts.host}:${
opts.port ?? (opts.secure ? "443" : "80")
}/${opts.namespace}`;
const logger = new SimpleStructuredLogger(opts.namespace, LogLevel.info);
logger.log("new zod socket", { uri });
this.socket = io(uri, {
transports: ["websocket"], transports: ["websocket"],
auth: { auth: {
token: opts.authToken, token: opts.authToken,
}, },
extraHeaders: opts.extraHeaders, extraHeaders: opts.extraHeaders,
reconnectionDelay: 500,
reconnectionDelayMax: 1000,
}); });
this.#logger = new SimpleStructuredLogger(opts.namespace, LogLevel.info, { this.#logger = logger.child({
socketId: this.socket.id, socketId: this.socket.id,
}); });
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "BackgroundWorkerTask" ADD COLUMN "machineConfig" JSONB;
+1
View File
@@ -1559,6 +1559,7 @@ model BackgroundWorkerTask {
queueConfig Json? queueConfig Json?
retryConfig Json? retryConfig Json?
machineConfig Json?
@@unique([workerId, slug]) @@unique([workerId, slug])
} }
+6 -3
View File
@@ -9,6 +9,8 @@ import {
HandleErrorResult, HandleErrorResult,
InitFnParams, InitFnParams,
InitOutput, InitOutput,
MachineCpu,
MachineMemory,
MiddlewareFnParams, MiddlewareFnParams,
QueueOptions, QueueOptions,
RetryOptions, RetryOptions,
@@ -114,8 +116,8 @@ export type TaskOptions<TPayload, TOutput = any, TInitOutput extends InitOutput
* - 2 * - 2
* - 4 * - 4
*/ */
cpu?: 0.25 | 0.5 | 1 | 2 | 4; cpu?: MachineCpu;
/** In GBs of RAM. The default is 0.5. /** In GBs of RAM. The default is 1.
* *
* Possible values: * Possible values:
* - 0.25 * - 0.25
@@ -125,7 +127,7 @@ export type TaskOptions<TPayload, TOutput = any, TInitOutput extends InitOutput
* - 4 * - 4
* - 8 * - 8
*/ */
memory?: 0.25 | 0.5 | 1 | 2 | 4 | 8; memory?: MachineMemory;
}; };
/** This gets called when a task is triggered. It's where you put the code you want to execute. /** This gets called when a task is triggered. It's where you put the code you want to execute.
* *
@@ -489,6 +491,7 @@ export function createTask<TInput, TOutput, TInitOutput extends InitOutput>(
packageVersion: packageJson.version, packageVersion: packageJson.version,
queue: params.queue, queue: params.queue,
retry: params.retry ? { ...defaultRetryOptions, ...params.retry } : undefined, retry: params.retry ? { ...defaultRetryOptions, ...params.retry } : undefined,
machine: params.machine,
fns: { fns: {
run: params.run, run: params.run,
init: params.init, init: params.init,