Run workers in a separate Docker container

We've outgrown the run-everything-on-one-machine setup. This change moves background jobs to a different Docker image in production. It also adds a `jobKey` to certain jobs so if we try to process the same cell multiple times it'll only actually run the job once.
This commit is contained in:
Kyle Corbitt
2023-08-18 11:16:00 -07:00
parent b1802fc04b
commit 10dd53e7f6
6 changed files with 24 additions and 13 deletions

View File

@@ -10,6 +10,4 @@ pnpm tsx src/promptConstructor/migrate.ts
echo "Starting the server" echo "Starting the server"
pnpm concurrently --kill-others \ pnpm start
"pnpm start" \
"pnpm tsx src/server/tasks/worker.ts"

View File

@@ -26,6 +26,10 @@ export const env = createEnv({
SMTP_PORT: z.string().default("placeholder"), SMTP_PORT: z.string().default("placeholder"),
SMTP_LOGIN: z.string().default("placeholder"), SMTP_LOGIN: z.string().default("placeholder"),
SMTP_PASSWORD: z.string().default("placeholder"), SMTP_PASSWORD: z.string().default("placeholder"),
WORKER_CONCURRENCY: z
.string()
.default("10")
.transform((val) => parseInt(val)),
}, },
/** /**
@@ -68,6 +72,7 @@ export const env = createEnv({
SMTP_PORT: process.env.SMTP_PORT, SMTP_PORT: process.env.SMTP_PORT,
SMTP_LOGIN: process.env.SMTP_LOGIN, SMTP_LOGIN: process.env.SMTP_LOGIN,
SMTP_PASSWORD: process.env.SMTP_PASSWORD, SMTP_PASSWORD: process.env.SMTP_PASSWORD,
WORKER_CONCURRENCY: process.env.WORKER_CONCURRENCY,
}, },
/** /**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.

View File

@@ -1,4 +1,4 @@
import { type Helpers, type Task, makeWorkerUtils } from "graphile-worker"; import { type Helpers, type Task, makeWorkerUtils, TaskSpec } from "graphile-worker";
import { env } from "~/env.mjs"; import { env } from "~/env.mjs";
let workerUtilsPromise: ReturnType<typeof makeWorkerUtils> | null = null; let workerUtilsPromise: ReturnType<typeof makeWorkerUtils> | null = null;
@@ -16,9 +16,11 @@ function defineTask<TPayload>(
taskIdentifier: string, taskIdentifier: string,
taskHandler: (payload: TPayload, helpers: Helpers) => Promise<void>, taskHandler: (payload: TPayload, helpers: Helpers) => Promise<void>,
) { ) {
const enqueue = async (payload: TPayload, runAt?: Date) => { const enqueue = async (payload: TPayload, spec?: TaskSpec) => {
console.log("Enqueuing task", taskIdentifier, payload); console.log("Enqueuing task", taskIdentifier, payload);
await (await workerUtils()).addJob(taskIdentifier, payload, { runAt });
const utils = await workerUtils();
return await utils.addJob(taskIdentifier, payload, spec);
}; };
const handler = (payload: TPayload, helpers: Helpers) => { const handler = (payload: TPayload, helpers: Helpers) => {

View File

@@ -153,7 +153,7 @@ export const queryModel = defineTask<QueryModelJob>("queryModel", async (task) =
stream, stream,
numPreviousTries: numPreviousTries + 1, numPreviousTries: numPreviousTries + 1,
}, },
retryTime, { runAt: retryTime, jobKey: cellId },
); );
await prisma.scenarioVariantCell.update({ await prisma.scenarioVariantCell.update({
where: { id: cellId }, where: { id: cellId },
@@ -184,6 +184,6 @@ export const queueQueryModel = async (cellId: string, stream: boolean) => {
jobQueuedAt: new Date(), jobQueuedAt: new Date(),
}, },
}), }),
queryModel.enqueue({ cellId, stream, numPreviousTries: 0 }), queryModel.enqueue({ cellId, stream, numPreviousTries: 0 }, { jobKey: cellId }),
]); ]);
}; };

View File

@@ -17,7 +17,7 @@ const taskList = registeredTasks.reduce((acc, task) => {
// Run a worker to execute jobs: // Run a worker to execute jobs:
const runner = await run({ const runner = await run({
connectionString: env.DATABASE_URL, connectionString: env.DATABASE_URL,
concurrency: 10, concurrency: env.WORKER_CONCURRENCY,
// Install signal handlers for graceful shutdown on SIGINT, SIGTERM, etc // Install signal handlers for graceful shutdown on SIGINT, SIGTERM, etc
noHandleSignals: false, noHandleSignals: false,
pollInterval: 1000, pollInterval: 1000,

View File

@@ -7,7 +7,7 @@ databases:
services: services:
- type: web - type: web
name: querykey-prod-web name: querykey-prod-web
env: docker runtime: docker
dockerfilePath: ./app/Dockerfile dockerfilePath: ./app/Dockerfile
dockerContext: . dockerContext: .
plan: standard plan: standard
@@ -21,8 +21,6 @@ services:
name: querykey-prod name: querykey-prod
property: connectionString property: connectionString
- fromGroup: querykey-prod - fromGroup: querykey-prod
- key: NEXT_PUBLIC_SOCKET_URL
value: https://querykey-prod-wss.onrender.com
# Render support says we need to manually set this because otherwise # Render support says we need to manually set this because otherwise
# sometimes it checks a different random port that NextJS opens for # sometimes it checks a different random port that NextJS opens for
# liveness and the liveness check fails. # liveness and the liveness check fails.
@@ -31,8 +29,16 @@ services:
- type: web - type: web
name: querykey-prod-wss name: querykey-prod-wss
env: docker runtime: docker
dockerfilePath: ./app/Dockerfile dockerfilePath: ./app/Dockerfile
dockerContext: . dockerContext: .
plan: free plan: free
dockerCommand: pnpm tsx src/wss-server.ts dockerCommand: pnpm tsx src/wss-server.ts
- type: worker
name: querykey-prod-worker
runtime: docker
dockerfilePath: ./app/Dockerfile
dockerContext: .
plan: starter
dockerCommand: pnpm tsx src/server/tasks/worker.ts