implement separate web workers #252

Merged
toast merged 2 commits from web-worker into main 2022-12-03 13:33:24 +00:00
5 changed files with 58 additions and 15 deletions

View file

@ -78,8 +78,11 @@ redis:
# Whether disable HSTS
#disableHsts: true
# Number of worker processes
#clusterLimit: 1
# Number of worker processes by type.
# The sum must not exceed the number of available cores.
#clusterLimits:
# web: 1
# queue: 1
# Job concurrency per worker
# deliverJobConcurrency: 128

View file

@ -66,7 +66,7 @@ export async function masterMain(): Promise<void> {
bootLogger.succ('FoundKey initialized');
if (!envOption.disableClustering) {
await spawnWorkers(config.clusterLimit);
await spawnWorkers(config.clusterLimits);
}
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
@ -139,16 +139,26 @@ async function connectDb(): Promise<void> {
}
}
async function spawnWorkers(limit = 1): Promise<void> {
const workers = Math.min(limit, os.cpus().length);
bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
await Promise.all([...Array(workers)].map(spawnWorker));
bootLogger.succ('All workers started');
async function spawnWorkers(clusterLimits: Required<Config['clusterLimits']>): Promise<void> {
const modes = ['web', 'queue'];
const cpus = os.cpus().length;
for (const mode of modes.filter(mode => clusterLimits[mode] > cpus)) {
bootLogger.warn(`configuration warning: cluster limit for ${mode} exceeds number of cores (${cpus})`);
}
const total = modes.reduce((acc, mode) => acc + clusterLimits[mode], 0);
const workers = new Array(total);
workers.fill('web', 0, clusterLimits.web);
workers.fill('queue', clusterLimits.web);
toast marked this conversation as resolved
Review

This construction is a little odd and seems to me to be prone to off-by-ones.
I would write it as this:

const web = new Array(clusterLimits.web).map(() => spawnWorker('web'));
const queue = new Array(clusterLimits.queue).map(() => spawnWorker('queue'));
const all = [...web, ...queue]);
console.log(`Starting ${all.length} workers...`);
await Promise.all(all);
This construction is a little odd and seems to me to be prone to off-by-ones. I would write it as this: ```js const web = new Array(clusterLimits.web).map(() => spawnWorker('web')); const queue = new Array(clusterLimits.queue).map(() => spawnWorker('queue')); const all = [...web, ...queue]); console.log(`Starting ${all.length} workers...`); await Promise.all(all); ```
Review

Your example code wouldn't work because new Array(n) generates an array with n empty slots and map basically ignores those.

Your example code wouldn't work because `new Array(n)` generates an array with `n` empty slots and `map` basically ignores those.
Review

I've thought about it a bit and it's fine.
Could be a TODO for cleanup later, but this is fine and works as expected.

I've thought about it a bit and it's fine. Could be a TODO for cleanup later, but this is fine and works as expected.
bootLogger.info(`Starting ${total} workers...`);
await Promise.all(workers.map(mode => spawnWorker(mode)));
bootLogger.succ(`All workers started`);
}
function spawnWorker(): Promise<void> {
function spawnWorker(mode: 'web' | 'queue'): Promise<void> {
return new Promise(res => {
const worker = cluster.fork();
const worker = cluster.fork({ mode });
worker.on('message', message => {
if (message === 'listenFailed') {
bootLogger.error('The server Listen failed due to the previous error.');

View file

@ -1,3 +1,4 @@
import os from 'node:os';
import cluster from 'node:cluster';
import { initDb } from '@/db/postgre.js';
@ -7,11 +8,20 @@ import { initDb } from '@/db/postgre.js';
export async function workerMain(): Promise<void> {
await initDb();
// start server
await import('../server/index.js').then(x => x.default());
if (!process.env.mode || process.env.mode === 'web') {
// start server
await import('../server/index.js').then(x => x.default());
}
// start job queue
import('../queue/index.js').then(x => x.default());
if (!process.env.mode || process.env.mode === 'queue') {
// start job queue
import('../queue/index.js').then(x => x.default());
if (process.env.mode === 'queue') {
// if this is an exclusive queue worker, renice to have higher priority
os.setPriority(os.constants.priority.PRIORITY_BELOW_NORMAL);
}
}
if (cluster.isWorker) {
// Send a 'ready' message to parent process

View file

@ -54,6 +54,23 @@ export default function load(): Config {
if (!config.redis.prefix) config.redis.prefix = mixin.host;
if (!config.clusterLimits) {
config.clusterLimits = {
web: 1,
queue: 1,
};
} else {
config.clusterLimits = {
web: 1,
queue: 1,
...config.clusterLimits,
};
if (config.clusterLimits.web < 1 || config.clusterLimits.queue < 1) {
throw new Error('invalid cluster limits');
}
}
return Object.assign(config, mixin);
}

View file

@ -45,7 +45,10 @@ export type Source = {
accesslog?: string;
clusterLimit?: number;
clusterLimits?: {
web?: number;
queue?: number;
};
id: string;