UNPKG

slavery-js

Version:

A simple clustering app that allows you to scale an application on multiple thread, containers or machines

180 lines (159 loc) 6.88 kB
import type { Request } from './types/index.js'; type RequestQueueOptions = { heartbeat?: number; requestTimeout?: number; // Timeout for requests onError?: 'throw' | 'log' | 'ignore' | 'return'; } type RequestQueueParameters = { process_request: Function; get_slave: Function; options?: RequestQueueOptions; } type addRequestParameters = { method: string; type: 'run' | 'exec'; parameters: any; selector: string; } class RequestQueue { /* This class will keep track of all the requests that are made to the service, * how long each request takes to be processed, * how many requests are in the queue, * when the requests are being processed, and * request individually. */ private queue: Request[] = []; private process_request: Function; private currentId: number = 0; private get_slave: Function; private isIntervalRunning: boolean = false; private interval: NodeJS.Timeout; private heartbeat; private turnover_times: number[] = []; // Stores time taken for the last 500 requests private MAX_TURNOVER_ENTRIES = 500; // Limit storage to last 500 requests private requestTimeout: number; // Timeout for requests constructor({ process_request, get_slave, options }: RequestQueueParameters) { /* * Set an interval to check if there are items in the queue. * If there are, pop the first element and process it. * If there are no elements, wait for the next element to be added. */ // process wrapper for maintainig the request this.process_request = async (slave: any, request: Request) => { let result = await process_request(slave, request); return { result, id: request.id } } this.get_slave = get_slave; this.heartbeat = options?.heartbeat || 10; // Check every 10ms if the request is completed this.requestTimeout = options?.requestTimeout || 5 * 60 * 1000; // Default timeout is 5 minutes if (!this.process_request) throw new Error('Process request cannot be null'); if (!this.get_slave) throw new Error('Get slave cannot be null'); // run interval this.interval = setInterval( async () => { // do not run another function if the previous one is still running if (this.isIntervalRunning) return; // if there are no items in the queue if (this.queue.length === 0){ this.isIntervalRunning = false; return; } // start the request function this.isIntervalRunning = true; // start to look at the queue for (let request of this.queue) { if(request.completed){ // remove the request at the index this.queue = this.queue.filter((r) => r.id !== request.id); request.onComplete(); this.isIntervalRunning = false; // get the turnover time this.addTurnOverTime(request.startTime); return; }else if(request.isProcessing){ // if check if the request has timed out if(Date.now() - request.startTime > this.requestTimeout){ request.completed = true; request.result = { isError: true, error: new Error(`slavery-js: Request '${request.method}', timed out after: ${this.requestTimeout}. You can change this behavior by setting the 'timeout', and 'onError' in the options at slavery({})`), } } }else{ // if the is neither completed nor processing request.isProcessing = true; // get slave const slave = await this.get_slave(request.selector); this.isIntervalRunning = false; // process the request this.process_request(slave, request) .then(({ result, id }: { result: any, id: number }) => { let request = this.getRequest(id); if(request){ // complete the request request.completed = true; request.result = result; } }) .catch((err : any) => { throw new Error('slavery-js [RequestQueue]: this Error should not be reachable, please report this issue', err); }) } } this.isIntervalRunning = false; }, this.heartbeat); } public addRequest( { method, type, parameters, selector }: addRequestParameters ): Promise<any> { // Add request to the queue and return a promise // that will be resolved when the request is completed return new Promise(async resolve => { // is id has reached the max value, reset it to 0 if(this.currentId >= Number.MAX_SAFE_INTEGER) this.currentId = 0; let request: Request = { id: ++this.currentId, method: method, type: type, parameters: parameters, selector: selector || undefined, completed: false, isProcessing: false, onComplete: () => { resolve(request.result) }, startTime: Date.now(), result: null, }; // add the request to the queue this.queue.push(request); }); } private getRequest(id: number): Request | null { // Get request by id const request = this.queue.find(r => r.id === id); if (request) { return request; } else { return null; } } private addTurnOverTime(startTime: number){ const timeTaken = Date.now() - startTime; this.turnover_times.push(timeTaken); // Keep only the last 500 entries if (this.turnover_times.length > this.MAX_TURNOVER_ENTRIES) this.turnover_times.shift(); // Remove the oldest entry } public queueSize() : number { return this.queue.length; } public getTurnoverRatio(): number { if (this.turnover_times.length === 0) return 0; const sum = this.turnover_times.reduce((acc, time) => acc + time, 0); return sum / this.turnover_times.length; } public getTurnoverTimes(): number[] { return this.turnover_times; } public exit() { this.queue = []; clearInterval(this.interval); } } export default RequestQueue;