@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
104 lines • 4.26 kB
JavaScript
import { callInNextEventLoop, nextEventLoop } from "../../util/eventLoop.js";
import { LinkedList } from "../array.js";
import { QueueError, QueueErrorCode } from "./errors.js";
import { QueueType, defaultQueueOpts } from "./options.js";
/**
* JobQueue that stores arguments in the job array instead of closures.
* Supports a single itemProcessor, for arbitrary functions use the JobFnQueue
*/
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
export class JobItemQueue {
constructor(itemProcessor, opts, metrics) {
this.itemProcessor = itemProcessor;
/**
* We choose to use LinkedList instead of regular array to improve shift() / push() / pop() performance.
* See the LinkedList benchmark for more details.
* */
this.jobs = new LinkedList();
this.runningJobs = 0;
this.lastYield = 0;
this.dropAllJobs = () => {
this.jobs.clear();
};
this.runJob = async () => {
if (this.opts.signal.aborted || this.runningJobs >= this.opts.maxConcurrency) {
return;
}
// Default to FIFO. LIFO -> pop() remove last item, FIFO -> shift() remove first item
const job = this.opts.type === QueueType.LIFO ? this.jobs.pop() : this.jobs.shift();
if (!job) {
return;
}
this.runningJobs++;
// If the job, metrics or any code below throws: the job will reject never going stale.
// Only downside is the job promise may be resolved twice, but that's not an issue
try {
const timer = this.metrics?.jobTime.startTimer();
this.metrics?.jobWaitTime.observe((Date.now() - job.addedTimeMs) / 1000);
const result = await this.itemProcessor(...job.args);
job.resolve(result);
if (timer)
timer();
// Yield to the macro queue
if (Date.now() - this.lastYield > this.opts.yieldEveryMs) {
this.lastYield = Date.now();
await nextEventLoop();
}
}
catch (e) {
job.reject(e);
}
this.runningJobs = Math.max(0, this.runningJobs - 1);
// Potentially run a new job
void this.runJob();
};
this.abortAllJobs = () => {
while (this.jobs.length > 0) {
const job = this.jobs.pop();
if (job)
job.reject(new QueueError({ code: QueueErrorCode.QUEUE_ABORTED }));
}
};
this.opts = { ...defaultQueueOpts, ...opts };
this.opts.signal.addEventListener("abort", this.abortAllJobs, { once: true });
if (metrics) {
this.metrics = metrics;
metrics.length.addCollect(() => {
metrics.length.set(this.jobs.length);
metrics.concurrency.set(this.runningJobs);
});
}
}
get jobLen() {
return this.jobs.length;
}
push(...args) {
if (this.opts.signal.aborted) {
throw new QueueError({ code: QueueErrorCode.QUEUE_ABORTED });
}
if (this.jobs.length + 1 > this.opts.maxLength) {
this.metrics?.droppedJobs.inc();
if (this.opts.type === QueueType.LIFO) {
// In LIFO queues keep the latest job and drop the oldest
this.jobs.shift();
}
else {
// In FIFO queues drop the latest job
throw new QueueError({ code: QueueErrorCode.QUEUE_MAX_LENGTH });
}
}
return new Promise((resolve, reject) => {
this.jobs.push({ args, resolve, reject, addedTimeMs: Date.now() });
if (this.jobs.length === 1 && this.opts.noYieldIfOneItem) {
void this.runJob();
}
else if (this.runningJobs < this.opts.maxConcurrency) {
callInNextEventLoop(this.runJob);
}
});
}
getItems() {
return this.jobs.map((job) => ({ args: job.args, addedTimeMs: job.addedTimeMs }));
}
}
//# sourceMappingURL=itemQueue.js.map