slavery-js
Version:
A simple clustering app that allows you to scale an application on multiple thread, containers or machines
470 lines (446 loc) • 22.1 kB
text/typescript
import Network, { Listener, Connection } from '../network/index.js';
import Node, { NodeManager } from '../nodes/index.js';
import Cluster from '../cluster/index.js';
import { PeerDiscoveryClient } from '../app/peerDiscovery/index.js';
import RequestQueue from './RequestQueue.js';
import ProcessBalancer from './ProcessBalancer.js';
import ServiceClient from './ServiceClient.js';
import Stash from './Stash.js';
import { toListeners, log, getPort, isServerActive, execAsyncCode, await_interval } from '../utils/index.js';
import type { ServiceAddress, SlaveMethods, Request, Options } from './types/index.js';
import { serializeError } from 'serialize-error';
// the paramer the service will take
type Parameters = {
// the name of the service
service_name: string,
// the address of the service will take
peerServicesAddresses?: ServiceAddress[],
// the adderess of the peer discovery service used to find the other services
peerDiscoveryAddress?: { host: string, port: number },
// the master callback that will be called by the master process
mastercallback?: (...args: any[]) => any,
// the slave callbacks that will be called by the slaves
slaveMethods?: SlaveMethods,
// the options that will be passed to the service
options?: Options
};
class Service {
/* This will be the based class for the service which salvery will call to create proceses */
public name: string;
public host: string;
public port: number;
private nodes?: NodeManager;
private stash: Stash = new Stash();
private processBalancer?: ProcessBalancer | null = null;
private requestQueue: RequestQueue | null = null;
public nm_host: string;
public nm_port: number;
public number_of_nodes: number;
private masterCallback?: (...args: any[]) => any;
private slaveMethods: SlaveMethods;
private peerAddresses: ServiceAddress[];
private peerDiscoveryAddress?: { host: string, port: number };
private peerDiscovery?: PeerDiscoveryClient;
private cluster?: Cluster;
private network?: Network;
private options: Options;
private servicesConnected: boolean = false;
constructor(params: Parameters) {
this.name = params.service_name;
// reserved names are injected into the master callback and must not be used as service names
const reservedNames = ['service_master', 'service_slave', 'service_slaves', 'self'];
if (reservedNames.includes(this.name)) {
throw new Error(`Service name '${this.name}' is reserved and cannot be used`);
}
// the address of the service will take
this.host = params.options?.host || 'localhost';
this.port = params?.options?.port || 0;
// the host of the node manager
this.nm_host = params.options?.nm_host || 'localhost';
this.nm_port = params.options?.nm_port || 0;
// the call that will run the master process
this.masterCallback = params.mastercallback || undefined;
// the method that we will use ont he slave
this.slaveMethods = params.slaveMethods || {};
// other sevices that we conenct to
this.peerAddresses = params.peerServicesAddresses || [];
// the peer discovery service
this.peerDiscoveryAddress = params.peerDiscoveryAddress || undefined;
this.peerDiscovery = undefined;
// if both the peerAddresses and the peerDiscoveryServiceAddress are not defined,
// we will throw an error
if(this.peerAddresses === undefined && this.peerDiscoveryAddress === undefined)
throw new Error('Peer Addresses or Peer Discovery Service Address must be defined');
// the options that will be passed to the service
this.options = params.options || {};
if (this.options.onError === undefined) this.options.onError = 'throw';
// smallest number of processes need to run
// the master and a slave
if(this.options.number_of_nodes === undefined){
this.number_of_nodes = 1;
if(this.options.auto_scale === undefined)
this.options.auto_scale = true;
}else{
this.number_of_nodes = this.options.number_of_nodes;
if(this.options.auto_scale === undefined)
this.options.auto_scale = false
}
}
public async start() { // this will start the service
// let initlize the cluster so that we can start the service
this.cluster = new Cluster(this.options);
// create a new process for the master process
this.cluster.spawn('master_' + this.name, {
allowedToSpawn: true, // give the ability to spawn new processes
spawnOnlyFromPrimary: true // make sure that only one master process is created
});
// run the code for the master process
if(this.cluster.is('master_' + this.name)) {
// initialize the master process
await this.initialize_master();
}
// if the cluster is a slave we initialize the process
if(this.cluster.is('slave_' + this.name)) {
await this.initialize_slaves();
}
}
private async initialize_master() {
// initialize the master and all the services
// get the port for the service
if(this.port === 0) this.port = await getPort({host: this.host});
// if we have a peer discovery service we will try to connect to it
if(this.peerDiscoveryAddress !== undefined) await this.handle_peer_discovery();
log('peer addresses', this.peerAddresses);
//console.log(`[${this.name}] > initialize_master peer addresses`, this.peerAddresses);
// initialize the node manager
//console.log(`[${this.name}] > initialize_master node manager`);
await this.initlize_node_manager();
//console.log(`[${this.name}] > initialize_master node manager done`);
// initialize the request queue
//console.log(`[${this.name}] > initialize_master request queue`);
this.initialize_request_queue();
//console.log(`[${this.name}] > initialize_master request queue done`);
// initlieze the network and create a service
//console.log(`[${this.name}] > initialize_master network`);
this.network = new Network({
name: this.name + '_service_network',
options: {
timeout: this.options.timeout,
}
});
//console.log(`[${this.name}] > initialize_master network done`);
// list the listeners we have for the other services to request
let listeners = toListeners(this.slaveMethods).map(
// add out handle request function to the listener
l => ({ ...l, callback: this.handle_request(l, 'run') })
);
// add the _exec listner, we have to add it here as the listners in
// this.getServiceListeners strips the selector
let exec_listener : Listener = { event: '_exec', callback: ()=>{} };
listeners.push({ ...exec_listener, callback: this.handle_request(exec_listener, 'exec') });
// add the local service listener
listeners = listeners.concat(this.getServiceListeners());
// create the server
this.network.createServer(this.name, this.host, this.port, listeners);
// initilze the process balancer
this.initialize_process_balancer();
// remover self address from the peer addresses by name
this.peerAddresses = this.peerAddresses.filter((p: ServiceAddress) => p.name !== this.name);
// connect to the services
let connections = await this.network.connectAll(this.peerAddresses);
// create a service client for the services
let services = connections.map( (c: Connection) => {
let name = c.getTargetName();
if(name === undefined) throw new Error('Service name is undefined');
return new ServiceClient(name, this.network as Network, this.options);
}).reduce((acc: any, s: ServiceClient) => {
acc[s.name] = s;
return acc;
}, {})
// set service as connected
this.servicesConnected = true;
// run the callback for the master process
if(this.masterCallback !== undefined)
this.masterCallback({ ...services, service_slaves: this.nodes, service_master: this, self: this });
}
private async initialize_slaves() {
// connect to the master process
let metadata = process.env.metadata;
if(metadata === undefined)
throw new Error('could not get post and host of the node manager, metadata is undefined');
let { host, port } = JSON.parse(metadata)['metadata'];
// creater the node
let node = new Node({
mode: 'client',
master_host: host,
master_port: port,
methods: this.slaveMethods,
options: {
timeout: this.options.timeout,
}
});
// connect with the master process
await node.start();
}
private async initlize_node_manager() {
/* the node manage will be used to conenct to and manage the nodes */
// if the slave methods is an empty object we will not make any nodes
if(Object.keys(this.slaveMethods).length === 0) return null;
// get the port for the node manager
if(this.nm_port === 0)
this.nm_port = await getPort({host: this.nm_host});
// make a node manager
this.nodes = new NodeManager({
name: this.name,
host: this.nm_host,
port: this.nm_port,
options: {
timeout: this.options.timeout,
stash: this.stash, // set the stash
}
})
// spawn the nodes from the node Manager
await this.nodes.spawnNodes('slave_' + this.name, this.number_of_nodes, {
metadata: { host: this.nm_host, port: this.nm_port }
});
// register the services in the nodes
await this.nodes.setServices(this.peerAddresses);
// get the nodes
return this.nodes;
}
private initialize_request_queue() {
/* this function will give the request queue all the values an callback it need to work */
// if there are no nodes to make don't create a request queue
if(Object.keys(this.slaveMethods).length === 0) return null
// if node manager is not defined throw an error
if(this.nodes === undefined) throw new Error('Node Manager is not defined');
// create a new request queue
this.requestQueue = new RequestQueue({
// we pass the functions that the request queue will use,
// such as getting the next node, and the function to process the request
get_slave: this.nodes.getIdle.bind(this.nodes),
process_request:
async (node: Node, request: Request) => await node[request.type](request.method, request.parameters),
options: {
heartbeat: 100,
requestTimeout: this.options.timeout,
onError: this.options.onError,
}
});
}
private initialize_process_balancer() {
/* this function will initialize the process balancer */
if(Object.keys(this.slaveMethods).length === 0) return null;
// if the auto scale is true we will create a process balancer
if(this.options.auto_scale === true){
this.processBalancer = new ProcessBalancer({
// pass the functions need for the balancer to know the hwo to balance
checkQueueSize: this.requestQueue?.queueSize.bind(this.requestQueue),
checkSlaves: () => ({ idleCount: this.nodes?.getIdleCount(), workingCount: this.nodes?.getBusyCount() }),
addSlave: () => this.nodes?.spawnNodes( 'slave_' + this.name, 1, { metadata: { host: this.nm_host, port: this.nm_port } }),
removeSlave: () => this.nodes?.killNode()
});
}
}
private getServiceListeners() {
// let add the listeners which we this service will respond
// lets ignore the selector field
let listeners = [{
// get number of nodes
event: '_get_nodes_count',
callback: () => ({ result: this.nodes?.getNodeCount() })
},{
event: '_get_nodes',
callback: () => this.nodes?.getNodes().map((n: Node) => ({ status: n.status, id: n.id }))
},{
event: '_get_idle_nodes', // wee need to filter this array of objects
callback: () => ({ result: this.nodes?.getIdleNodes() })
},{
event: '_get_busy_nodes', // this one too
callback: () =>({ result: this.nodes?.getBusyNodes() })
},{
event: '_number_of_nodes_connected',
params: ['node_num'],
callback: async (node_num: number) => await this.nodes?.numberOfNodesConnected(node_num)
},{ // select individual nodes, or groups of nodes
event: '_select',
params: ['node_num'],
callback: async (node_num: number) => {
if(this.nodes === undefined) throw new Error('Nodes are undefined');
// get the idle nodes
let count = this.nodes?.getNodeCount();
if(count === undefined) return { isError: true, error: serializeError(new Error('Nodes are undefined')) }
// if the number of nodes is greater than the number of nodes we have
if(node_num > count) return { isError: true, error: serializeError(new Error('Not enough nodes')) }
if(node_num === 0) node_num = count;
// select the nodes
let selected_nodes = [];
for(let i = 0; i < node_num; i++){
let node = this.nodes?.nextNode();
if(node === null) throw new Error('could not get node');
selected_nodes.push(node.id);
}
// return the selected nodes
return { result: selected_nodes }
}
},{ // spawn or kill a node
event: '_add_node',
params: ['number_of_nodes'],
callback: (number_of_nodes: number) =>
({ result: this.nodes?.spawnNodes(
'slave_' + this.name,
number_of_nodes,
{ metadata: { host: this.nm_host, port: this.nm_port } }
) })
},{ // kill a node
event: '_kill_node',
params: ['node_id'],
callback: async(node_ids: string[] | string | undefined | number) => {
let res;
if(node_ids === undefined){
res = await this.nodes?.killNode();
}else if(typeof node_ids === 'string'){
res = await this.nodes?.killNode(node_ids);
}else if(typeof node_ids === 'number'){
for(let i = 0; i < node_ids; i++) await this.nodes?.killNode();
}else if(node_ids.length === 0){
res = await this.nodes?.killNode();
}else if(node_ids.length >= 1){
res = await this.nodes?.killNodes(node_ids);
}else {
return { isError: true, error: serializeError(new Error('Invalid node id')) }
}
return ({result: res});
}
},{ // exit the service
event: '_queue_size',
callback: () => ({ result: this.requestQueue?.queueSize() })
},{
event: '_turn_over_ratio',
callback: () => ({ result: this.requestQueue?.getTurnoverRatio() })
},{
event: '_exec_master',
params: ['code_string'],
callback: async (code_string: any) => {
// check if the code_string is a string
if(typeof code_string !== 'string')
return { isError: true, error: serializeError(new Error('Code string is not a string')) }
// await until service is connected
await await_interval({
condition: () => this.servicesConnected, timeout: 1000
}).catch((error) => {
return { isError: true, error: serializeError(error || new Error(`[Service] Could not connect to the services`)) };
});
let service;
try {
service = this.getServices();
} catch (e) {
return { isError: true, error: serializeError(e) };
}
let parameter = { ...service, service_master: this, self: this };
try {
// run the albitrary code
let result = await execAsyncCode(code_string, parameter);
return { result: result };
} catch(e) {
return { isError: true, error: serializeError(e) };
}
}
},{
event: 'new_service',
params: ['service_address'],
callback: async (service_address: ServiceAddress) => {
if(this.network === undefined) return { isError: true, error: serializeError(new Error('Network is not defined')) };
try {
await this.network?.connect(service_address);
return { result: true };
} catch (error) {
return { isError: true, error: serializeError(error) };
}
}
},{
event: 'exit',
callback: () => ({ result: this.exit() })
}];
// get only the paramters, diregar the rest
return listeners.map(l => {
return { ...l, callback: ({ parameters }: any) => l.callback(parameters) }
});
}
private handle_request(l: Listener, type: 'run' | 'exec'): Function {
/* this function will take a listener triggered by another a request
* it will set the request in the request queue, and return a promise
* which resolves once the request is processed.
* the queue will processs the request when it finds an idle node
* and the node returns the result. */
return async (data: any) => {
if(this.slaveMethods === undefined) throw new Error('Slave Methods are not defined');
if(this.requestQueue === null)
throw new Error('Request Queue is not defined');
let promise = this.requestQueue.addRequest({
method: l.event,
type: type,
parameters: data.parameters,
selector: data.selection,
});
// wait until the request is processed
let result = await promise;
if(result.isError) // if there is an error serialize it
result.error = serializeError(result.error);
return result;
}
}
private async handle_peer_discovery() {
if(this.peerDiscoveryAddress === undefined) throw new Error('Peer Discovery Address is not defined');
if(this.cluster === undefined) throw new Error('Cluster is not defined');
// check if the peer discovery service is active
log(`[${this.name}] > Service > Checking if Peer Discovery Service is active`);
log(`[${this.name}] > Service > Peer Discovery Address: ${this.peerDiscoveryAddress.host}:${this.peerDiscoveryAddress.port}`);
let serverIsActive = await isServerActive(this.peerDiscoveryAddress);
if(serverIsActive === false)
throw new Error('Peer Discovery Service is not active');
// if it is active we will register to it
this.peerDiscovery = new PeerDiscoveryClient(this.peerDiscoveryAddress);
await this.peerDiscovery.connect();
// register the service to the peer discovery service
this.peerDiscovery.register({ name: this.name, host: this.host, port: this.port });
// get the services that we will connect to
this.peerAddresses = await this.peerDiscovery.getServices();
}
private getServices(): { [serviceName: string]: ServiceClient } {
// get the service from the network
if(this.network === undefined) throw new Error('Network is not defined');
let services = this.network.getServices()
return services.map( (c: Connection) => {
let name = c.getTargetName();
if(name === undefined) throw new Error('Service name is undefined');
return new ServiceClient(name, this.network as Network, this.options);
}).reduce((acc: any, s: ServiceClient) => {
acc[s.name] = s;
return acc;
}, {})
}
public exit(){
//log(`[${this.name}] will exit in 1 seconds`);
setTimeout(() => {
// first we close the ProcessBalancer if we have one
if(this.processBalancer) this.processBalancer.exit();
// request queue will be closed
if(this.requestQueue) this.requestQueue.exit();
// if peer discovery is defined we will close it
if(this.peerDiscovery) this.peerDiscovery.exit();
// then we close the nodes Manager
if(this.nodes) this.nodes.exit();
// then we close the connections we have,
if(this.network) this.network.close();
// lastly we close ourselves, how sad
process.exit(0);
}, 1000);
return true
}
public set = async (key: any, value: any = null) =>
await this.stash.set(key, value);
public get = async (key: string = '') =>
await this.stash.get(key);
}
export default Service;