@nazaire/orchestra
Version:
A framework for distributing work over many machines, integrated with Node.js workers to utilise many threads per machine.
182 lines (154 loc) • 4.49 kB
text/typescript
import { nanoid } from "nanoid";
import {
StrictMessage,
Network,
NetworkClient,
MessageType,
} from "./Network/index.js";
import { Worker } from "worker_threads";
import { Job } from "./Job/Job.js";
import { Workspace } from "./Workspace.js";
import { WorkerMessage, WorkerMessageType } from "./Worker/WorkerMessage.js";
export class Instrument extends NetworkClient {
constructor(
network: Network,
private readonly workspace: Workspace<any, any>,
public readonly workers: number,
private readonly options?: {
debug?: boolean;
}
) {
super("instrument_" + nanoid(), network);
this.on(MessageType.JOB_AVAILABLE, this.handleJobAvailable.bind(this));
// announce that we are connected
this.send(
this.createMessage({
type: MessageType.INSTRUMENT_CONNECTED,
destination: "composer",
data: null,
})
);
}
private activeJobs = new Map<string, Worker>();
private async handleJobAvailable(
msg: StrictMessage<MessageType.JOB_AVAILABLE>
) {
if (this.activeJobs.size >= this.workers) {
return;
}
const workRequest = this.createResponseTo(msg, {
type: MessageType.JOB_REQUEST,
data: null,
});
const response = await this.sendAndAwaitResponse<MessageType.JOB_RESPONSE>(
workRequest,
1000
);
if (response.data === null) {
// No work available.
return;
} else {
this.startWorker(response.data);
}
}
private async startWorker(job: Job) {
const complete = async (result: any, error: string | null) => {
const message = this.createMessage({
type: MessageType.JOB_COMPLETED,
destination: "*",
data: {
...job,
status: "completed",
result,
error,
},
});
await this.send(message);
this.activeJobs.delete(job.id);
};
try {
// create a worker
const worker = new Worker(
this.workspace.getPath(String(job.options.script)),
{
workerData: {
params: job.options.params,
},
}
);
if (this.options?.debug) {
console.log(
`Instrument: Started worker for job ${job.id}. Worker count: ${this.activeJobs.size}/${this.workers}`
);
}
this.activeJobs.set(job.id, worker);
let result: any = null,
error: any = null;
const complete = async (result: any, error: string | null) => {
if (this.activeJobs.get(job.id) !== worker) {
return;
}
const message = this.createMessage({
type: MessageType.JOB_COMPLETED,
destination: "*",
data: {
...job,
status: "completed",
result,
error,
},
});
await this.send(message);
this.activeJobs.delete(job.id);
if (this.options?.debug) {
console.log(
`Instrument: Job ${job.id} completed with result: ${String(
result
)} and error: ${String(error)}`
);
}
};
// node.js worker bindings
worker.on("message", (message: WorkerMessage<WorkerMessageType>) => {
switch (message.type) {
case WorkerMessageType.WORK_DATA:
this.sendData(
this.createMessage({
type: MessageType.JOB_DATA,
destination: "*",
data: {
id: job.id,
data: message.data,
},
})
);
break;
case WorkerMessageType.WORK_RESULT:
result = message.data.result;
}
});
worker.on("error", (workError) => {
// an error was thrown inside the worker
error = workError;
worker.terminate();
});
worker.on("exit", (code) => {
// the worker is now dead
if (this.options?.debug) {
console.log(
`Instrument: Worker for job ${job.id} exited with code ${code}.`
);
}
if (code != 0 && !error) {
// the error should be set, but just in case we'll have a fallback error
error = String(new Error("Worker exited with code " + code));
}
// complete the job
complete(result, error);
});
} catch (error) {
console.error(error);
complete(null, String(error));
}
}
}