slavery-js
Version:
A simple clustering app that allows you to scale an application on multiple thread, containers or machines
155 lines (141 loc) • 6.25 kB
text/typescript
import os from 'os';
import { log } from '../utils/index.js';
interface balancerConfig {
minSlaves?: number;
maxSlaves?: number;
queueScaleUpThreshold?: number;
queueScaleDownThreshold?: number;
maxIdleRateThreshold?: number;
minIdleRateThreshold?: number;
cpuThreshold?: number;
memThreshold?: number;
checkInterval?: number;
checkSlaves: (() => { idleCount: number | undefined, workingCount:number | undefined }) | undefined;
checkQueueSize: (() => number) | undefined;
addSlave: (() => void) | undefined;
removeSlave: (() => void) | undefined;
}
class ProcessBalancer {
private prevQueueSize: number = 0;
private interval: NodeJS.Timeout | undefined;
private queueScaleUpThreshold: number;
private queueScaleDownThreshold: number;
private maxIdleRateThreshold: number;
private minIdleRateThreshold: number;
private cpuThreshold: number;
private memThreshold: number;
private checkInterval: number;
private checkQueueSize: (() => number) | undefined;
private checkSlaves: (() => { idleCount: number | undefined, workingCount:number | undefined }) | undefined;
private addSlave: (() => void) | undefined;
private removeSlave: (() => void) | undefined;
constructor(config: balancerConfig) {
// if there is at least 3 request in the queue, allow to scale up
this.queueScaleUpThreshold = config.queueScaleUpThreshold || 3;
// if there is at most one request on the queue, allow to scale down
this.queueScaleDownThreshold = config.queueScaleDownThreshold || 0;
// if we are using a lot of resources, don't scale up
this.cpuThreshold = config.cpuThreshold || 90;
this.memThreshold = config.memThreshold || 90;
// if 80 percent of all slaves are idle, don't allow to make more
this.maxIdleRateThreshold = config.maxIdleRateThreshold || 0.8
// if 10 percent of all slaves are idle, don't remove any
this.minIdleRateThreshold = config.minIdleRateThreshold || 0.1
// how often do we check
this.checkInterval = config.checkInterval || 500;
// function need to check
this.checkQueueSize = config.checkQueueSize;
this.checkSlaves = config.checkSlaves;
this.addSlave = config.addSlave;
this.removeSlave = config.removeSlave;
// check if we got all the need callbacks
this.checkRequiredFunctions();
// Initialize monitoring
this.interval = this.startMonitoring();
}
private getCpuUsage(): number {
let cpus = os.cpus();
let totalLoad = cpus.reduce((acc, cpu) => {
let total = Object.values(cpu.times).reduce((t, v) => t + v, 0);
return acc + (cpu.times.user / total) * 100;
}, 0);
return totalLoad / cpus.length;
}
private getMemoryUsage(): number {
return ((os.totalmem() - os.freemem()) / os.totalmem()) * 100;
}
private monitorSystem(): void {
if(this.checkQueueSize === undefined) throw Error('checkQueueSize is undefined');
if(this.checkSlaves === undefined) throw Error('checkSlaves is undefined');
this.checkRequiredFunctions();
const queueSize = this.checkQueueSize();
const { idleCount, workingCount } = this.checkSlaves();
if(idleCount === undefined || workingCount === undefined)
throw new Error('checkSlaves function returned idleCount or workingCount with value of undefined')
const idleRate = idleCount / workingCount + idleCount
const queueGrowth = queueSize - this.prevQueueSize;
this.prevQueueSize = queueSize;
const avgCpu = this.getCpuUsage();
const avgMem = this.getMemoryUsage();
/*
log(`[ProcessBalancer]
Queue Size: ${queueSize},
Growth: ${queueGrowth},
CPU: ${avgCpu.toFixed(2)}%,
MEM: ${avgMem.toFixed(2)}%,
Idle Slaves: ${idleCount},
Working Slaves: ${workingCount},
Total Slaves: ${idleCount + workingCount},
Idle Ratio: ${(idleCount / workingCount).toFixed(2)}`
);
*/
if (
// if the queue size is passed a threshold: 3
queueSize > this.queueScaleUpThreshold &&
// and it is growing
queueGrowth > 0 &&
// and the average CPU and MEM usage is below 90%
avgCpu < this.cpuThreshold &&
avgMem < this.memThreshold &&
// and the ratio of idle slaves to working slaves is greater than than threshold
idleRate < this.maxIdleRateThreshold
){
log('Scaling up, adding a node');
//@ts-ignore
this.addSlave();
}
if ( // if the queue size is less than or equal to the threshold
queueSize <= this.queueScaleDownThreshold &&
// if there is at least one
idleCount > 1 &&
// if the queue size is degreesing or not growing
queueGrowth <= 0 &&
// if the idle rate is low
idleRate > this.minIdleRateThreshold
){
log('Scaling down, removing a node');
//@ts-ignore
this.removeSlave();
}
}
private startMonitoring(): NodeJS.Timeout {
return setInterval(() => {
this.monitorSystem();
}, this.checkInterval);
}
private checkRequiredFunctions(): void {
if (this.checkQueueSize === undefined)
throw new Error('Missing required function checkQueueSize in config');
if (this.checkSlaves === undefined)
throw new Error('Missing required function checkSlaves in config');
if (this.addSlave === undefined)
throw new Error('Missing required function addSlave in config');
if (this.removeSlave === undefined)
throw new Error('Missing required function removeSlave in config');
}
public exit(): void {
// end monitoring
clearInterval(this.interval);
}
}
export default ProcessBalancer;