@cap-js-community/event-queue
Version:
An event queue that enables secure transactional processing of asynchronous and periodic events, featuring instant event processing with Redis Pub/Sub and load distribution across all application instances.
190 lines (164 loc) • 5.46 kB
JavaScript
"use strict";
const cds = require("@sap/cds");
const config = require("../config");
const EventQueueError = require("../EventQueueError");
const { Priorities } = require("../constants");
const SetIntervalDriftSafe = require("./SetIntervalDriftSafe");
const PRIORITIES = Object.values(Priorities).reverse();
const PRIORITY_MULTIPLICATION = PRIORITIES.reduce((result, element, index) => {
result[element] = index + 1;
return result;
}, {});
const COMPONENT_NAME = "/eventQueue/WorkerQueue";
const NANO_TO_MS = 1e6;
const MIN_TO_MS = 60 * 1000;
const INCREASE_PRIORITY_AFTER = 3;
let lastLogTs;
const THRESHOLD = {
INFO: 35 * 1000,
WARN: 55 * 1000,
ERROR: 75 * 1000,
};
const CHECK_INTERVAL_QUEUE = 60 * 1000;
class WorkerQueue {
#concurrencyLimit;
#runningPromises;
#runningLoad;
#queue;
static #instance;
constructor(concurrency) {
if (Number.isNaN(concurrency) || concurrency <= 0) {
this.#concurrencyLimit = 1;
} else {
this.#concurrencyLimit = concurrency;
}
this.#runningPromises = [];
this.#runningLoad = 0;
this.#queue = PRIORITIES.reduce((result, priority) => {
result[priority] = [];
return result;
}, {});
const runner = new SetIntervalDriftSafe(CHECK_INTERVAL_QUEUE);
runner.run(this.#adjustPriority.bind(this));
}
addToQueue(load, label, priority = Priorities.Medium, increasePriorityOverTime, cb) {
if (load > this.#concurrencyLimit) {
throw EventQueueError.loadHigherThanLimit(load, label);
}
if (!PRIORITIES.includes(priority)) {
throw EventQueueError.priorityNotAllowed(priority, label);
}
const startTime = process.hrtime.bigint();
const p = new Promise((resolve, reject) => {
this.#queue[priority].push([load, label, cb, resolve, reject, increasePriorityOverTime, startTime]);
});
this.#checkForNext();
return p;
}
#adjustPriority() {
const checkTime = process.hrtime.bigint();
const priorityValues = Object.values(Priorities);
for (let i = 0; i < priorityValues.length - 1; i++) {
const priority = priorityValues[i];
const nextPriority = priorityValues[i + 1];
for (let i = 0; i < this.queue[priority].length; i++) {
const queueEntry = this.queue[priority][i];
// NOTE: index 5 - increasingPrioOverTime
if (!queueEntry[5]) {
continue;
}
// NOTE: index 6 original time; index 7 shifted time
const startTime = queueEntry[7] ?? queueEntry[6];
if (Math.round(Number(checkTime - startTime) / NANO_TO_MS) > INCREASE_PRIORITY_AFTER * MIN_TO_MS) {
const [entry] = this.queue[priority].splice(i, 1);
entry.push(checkTime);
this.queue[nextPriority].push(entry);
}
}
}
}
_executeFunction(priority, load, label, cb, resolve, reject, skipIncreasingPrioOverTime, startTime) {
this.#checkAndLogWaitingTime(startTime, label, priority);
const promise = Promise.resolve().then(() => cb());
this.#runningPromises.push(promise);
this.#runningLoad = this.#runningLoad + load;
promise
.finally(() => {
this.#runningLoad = this.#runningLoad - load;
this.#runningPromises.splice(this.#runningPromises.indexOf(promise), 1);
this.#checkForNext();
})
.then((...results) => {
resolve(...results);
})
.catch((err) => {
cds.log(COMPONENT_NAME).error("Error happened in WorkQueue. Errors should be caught before!", err, { label });
reject(err);
});
if (this.#runningLoad !== this.#concurrencyLimit) {
this.#checkForNext();
}
}
#checkForNext() {
if (!Object.values(this.#queue).some((queue) => queue.length) || this.#runningLoad === this.#concurrencyLimit) {
return;
}
let entryFound = false;
for (const priority of PRIORITIES) {
for (let i = 0; i < this.#queue[priority].length; i++) {
const [load] = this.#queue[priority][i];
if (this.#runningLoad + load <= this.#concurrencyLimit) {
const [args] = this.#queue[priority].splice(i, 1);
this._executeFunction(priority, ...args);
entryFound = true;
break;
}
}
if (entryFound) {
break;
}
}
}
get runningPromises() {
return this.#runningPromises;
}
/**
@return { WorkerQueue }
**/
static get instance() {
if (!WorkerQueue.#instance) {
WorkerQueue.#instance = new WorkerQueue(config.instanceLoadLimit);
}
return WorkerQueue.#instance;
}
get queue() {
return this.#queue;
}
get runningLoad() {
return this.#runningLoad;
}
#checkAndLogWaitingTime(startTime, label, priority) {
const ts = Date.now();
if (ts - lastLogTs <= 1000) {
return;
}
lastLogTs = ts;
const diffMs = Math.round(Number(process.hrtime.bigint() - startTime) / NANO_TO_MS);
const priorityMultiplication = PRIORITY_MULTIPLICATION[priority];
let logLevel;
if (diffMs >= THRESHOLD.ERROR * priorityMultiplication) {
logLevel = "error";
} else if (diffMs >= THRESHOLD.WARN * priorityMultiplication) {
logLevel = "warn";
} else if (diffMs >= THRESHOLD.INFO * priorityMultiplication) {
logLevel = "info";
} else {
logLevel = "debug";
}
cds.log(COMPONENT_NAME)[logLevel]("Waiting time in worker queue", {
diffMs,
label,
});
}
}
module.exports = WorkerQueue;