@nazaire/orchestra
Version:
A framework for distributing work over many machines, integrated with Node.js workers to utilise many threads per machine.
136 lines (109 loc) • 3.52 kB
text/typescript
import { nanoid } from "nanoid";
import {
Network,
Message,
MessageType,
StrictMessage,
NetworkClient,
} from "./Network/index.js";
import { Job } from "./Job/Job.js";
import { JobQueue } from "./Job/JobQueue.js";
export class Composer extends NetworkClient {
private queue = new JobQueue();
constructor(
network: Network,
private readonly options?: {
debug?: boolean;
}
) {
super("composer", network);
this.on(MessageType.CREATE_JOB, this.onCreateJobMessage.bind(this));
this.on(MessageType.QUERY_JOBS, this.onQueryJobsMessage.bind(this));
this.on(
MessageType.INSTRUMENT_CONNECTED,
this.onInstrumentConnected.bind(this)
);
this.on(MessageType.JOB_REQUEST, this.onJobRequestMessage.bind(this));
this.on(MessageType.JOB_COMPLETED, this.onJobCompletedMessage.bind(this));
}
private async onQueryJobsMessage(msg: StrictMessage<MessageType.QUERY_JOBS>) {
const jobs = this.queue.getJobs().filter((job) => {
if (msg.data.id) {
return job.id === msg.data.id;
}
if (msg.data.status) {
return job.status === msg.data.status;
}
return true;
});
const response = this.createResponseTo(msg, {
type: MessageType.QUERY_JOBS_RESPONSE,
data: jobs,
});
await this.send(response);
}
private async onCreateJobMessage(msg: StrictMessage<MessageType.CREATE_JOB>) {
const job = this.queue.createJob(msg.data.options, msg.data.priority);
if (this.options?.debug) {
console.log(
`Composer: Created job ${job.id} at index ${this.queue.indexOf(
job.id
)}. Waiting jobs: ${this.queue.size}`,
this.queue.getValues()
);
}
const response = this.createResponseTo(msg, {
type: MessageType.CREATE_JOB_RESPONSE,
data: job,
});
await this.send(response);
this.notifyWorkersIfAvailableWork();
}
private async notifyWorkersIfAvailableWork() {
if (this.queue.size > 0) {
// notify workers of work
const message = this.createMessage({
type: MessageType.JOB_AVAILABLE,
destination: "*",
data: null,
});
await this.send(message);
}
}
private async onInstrumentConnected(
_msg: StrictMessage<MessageType.INSTRUMENT_CONNECTED>
) {
this.notifyWorkersIfAvailableWork();
}
private async onJobRequestMessage(
msg: StrictMessage<MessageType.JOB_REQUEST>
) {
const jobId = this.queue.next();
let job: Job | null = null;
if (jobId) job = this.queue.start(jobId);
const response = this.createResponseTo(msg, {
type: MessageType.JOB_RESPONSE,
data: job,
});
// todo: is there an edge case if the Instrument reaches it's timeout before this message is received?
// todo: we should have a confirmation message from the Instrument that it has received the message
// todo: and if it hasn't, we should requeue the job
// todo: or we should have a interval check on the Composer side to determine if a Job has stalled or been lost
await this.send(response);
}
private async onJobCompletedMessage(
msg: StrictMessage<MessageType.JOB_COMPLETED>
) {
const job = this.queue.complete(
msg.data.id,
msg.data.result,
msg.data.error || null
);
if (this.options?.debug) {
console.log(
`Composer: Job ${job.id} completed. Waiting jobs: ${this.queue.size}`
);
}
this.notifyWorkersIfAvailableWork();
}
}