UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

104 lines 4.26 kB
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