slavery-js
Version:
A simple clustering app that allows you to scale an application on multiple thread, containers or machines
323 lines • 13.4 kB
JavaScript
import {
__publicField
} from "../chunk-V6TY7KAL.js";
import Network 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 { serializeError } from "serialize-error";
class Service {
constructor(params) {
/* This will be the based class for the service which salvery will call to create proceses */
__publicField(this, "name");
__publicField(this, "host");
__publicField(this, "port");
__publicField(this, "nodes");
__publicField(this, "stash", new Stash());
__publicField(this, "processBalancer", null);
__publicField(this, "requestQueue", null);
__publicField(this, "nm_host");
__publicField(this, "nm_port");
__publicField(this, "number_of_nodes");
__publicField(this, "masterCallback");
__publicField(this, "slaveMethods");
__publicField(this, "peerAddresses");
__publicField(this, "peerDiscoveryAddress");
__publicField(this, "peerDiscovery");
__publicField(this, "cluster");
__publicField(this, "network");
__publicField(this, "options");
__publicField(this, "servicesConnected", false);
__publicField(this, "set", async (key, value = null) => await this.stash.set(key, value));
__publicField(this, "get", async (key = "") => await this.stash.get(key));
this.name = params.service_name;
this.host = params.options?.host || "localhost";
this.port = params?.options?.port || 0;
this.nm_host = params.options?.nm_host || "localhost";
this.nm_port = params.options?.nm_port || 0;
this.masterCallback = params.mastercallback || void 0;
this.slaveMethods = params.slaveMethods || {};
this.peerAddresses = params.peerServicesAddresses || [];
this.peerDiscoveryAddress = params.peerDiscoveryAddress || void 0;
this.peerDiscovery = void 0;
if (this.peerAddresses === void 0 && this.peerDiscoveryAddress === void 0)
throw new Error("Peer Addresses or Peer Discovery Service Address must be defined");
this.options = params.options || {};
if (this.options.number_of_nodes === void 0) {
this.number_of_nodes = 1;
if (this.options.auto_scale === void 0)
this.options.auto_scale = true;
} else {
this.number_of_nodes = this.options.number_of_nodes;
if (this.options.auto_scale === void 0)
this.options.auto_scale = false;
}
}
async start() {
this.cluster = new Cluster(this.options);
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
});
if (this.cluster.is("master_" + this.name)) {
await this.initialize_master();
}
if (this.cluster.is("slave_" + this.name)) {
await this.initialize_slaves();
}
}
async initialize_master() {
if (this.port === 0) this.port = await getPort({ host: this.host });
if (this.peerDiscoveryAddress !== void 0) await this.handle_peer_discovery();
log("peer addresses", this.peerAddresses);
await this.initlize_node_manager();
this.initialize_request_queue();
this.network = new Network({ name: this.name + "_service_network" });
let listeners = toListeners(this.slaveMethods).map(
// add out handle request function to the listener
(l) => ({ ...l, callback: this.handle_request(l, "run") })
);
let exec_listener = { event: "_exec", callback: () => {
} };
listeners.push({ ...exec_listener, callback: this.handle_request(exec_listener, "exec") });
listeners = listeners.concat(this.getServiceListeners());
this.network.createServer(this.name, this.host, this.port, listeners);
this.initialize_process_balancer();
this.peerAddresses = this.peerAddresses.filter((p) => p.name !== this.name);
let connections = await this.network.connectAll(this.peerAddresses);
let services = connections.map((c) => {
let name = c.getTargetName();
if (name === void 0) throw new Error("Service name is undefined");
return new ServiceClient(name, this.network, this.options);
}).reduce((acc, s) => {
acc[s.name] = s;
return acc;
}, {});
this.servicesConnected = true;
if (this.masterCallback !== void 0)
this.masterCallback({ ...services, slaves: this.nodes, master: this, self: this });
}
async initialize_slaves() {
let node = new Node();
let metadata = process.env.metadata;
if (metadata === void 0)
throw new Error("could not get post and host of the node manager, metadata is undefined");
let { host, port } = JSON.parse(metadata)["metadata"];
await node.connectToMaster(host, port);
await node.setServices(this.peerAddresses);
node.addMethods(this.slaveMethods);
await node._startup();
}
async initlize_node_manager() {
if (Object.keys(this.slaveMethods).length === 0) return null;
if (this.nm_port === 0)
this.nm_port = await getPort({ host: this.nm_host });
this.nodes = new NodeManager({
name: this.name,
host: this.nm_host,
port: this.nm_port,
stash: this.stash
// set the stash
});
await this.nodes.spawnNodes("slave_" + this.name, this.number_of_nodes, {
metadata: { host: this.nm_host, port: this.nm_port }
});
await this.nodes.registerServices(this.peerAddresses);
return this.nodes;
}
initialize_request_queue() {
if (Object.keys(this.slaveMethods).length === 0) return null;
if (this.nodes === void 0) throw new Error("Node Manager is not defined");
this.requestQueue = new RequestQueue({
// we pass the functions that the request queue will use
get_slave: this.nodes.getIdle.bind(this.nodes),
process_request: async (node, request) => await node[request.type](request.method, request.parameters)
});
}
initialize_process_balancer() {
if (Object.keys(this.slaveMethods).length === 0) return null;
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()
});
}
}
getServiceListeners() {
let listeners = [{
// get number of nodes
event: "_get_nodes_count",
callback: () => ({ result: this.nodes?.getNodeCount() })
}, {
event: "_get_nodes",
callback: () => this.nodes?.getNodes().map((n) => ({ 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) => await this.nodes?.numberOfNodesConnected(node_num)
}, {
// select individual nodes, or groups of nodes
event: "_select",
params: ["node_num"],
callback: async (node_num) => {
if (this.nodes === void 0) throw new Error("Nodes are undefined");
let count = this.nodes?.getNodeCount();
if (count === void 0) return { isError: true, error: serializeError(new Error("Nodes are undefined")) };
if (node_num > count) return { isError: true, error: serializeError(new Error("Not enough nodes")) };
if (node_num === 0) node_num = count;
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 { result: selected_nodes };
}
}, {
// spawn or kill a node
event: "_add_node",
params: ["number_of_nodes"],
callback: (number_of_nodes) => ({ 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) => {
let res;
if (node_ids === void 0) {
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) => {
if (typeof code_string !== "string")
return { isError: true, error: serializeError(new Error("Code string is not a string")) };
await await_interval(() => this.servicesConnected, 1e4).catch(() => {
throw new Error(`[Service] Could not connect to the services`);
});
let service = this.getServices();
let parameter = { ...service, master: this, self: this };
try {
let result = await execAsyncCode(code_string, parameter);
return { result };
} catch (e) {
return { isError: true, error: serializeError(e) };
}
}
}, {
event: "new_service",
params: ["service_address"],
callback: async (service_address) => {
if (this.network === void 0) throw new Error("Network is not defined");
await this.network?.connect(service_address);
}
}, {
event: "exit",
callback: () => ({ result: this.exit() })
}];
return listeners.map((l) => {
return { ...l, callback: ({ parameters }) => l.callback(parameters) };
});
}
handle_request(l, type) {
return async (data) => {
if (this.slaveMethods === void 0) 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,
parameters: data.parameters,
selector: data.selection,
completed: false,
result: null
});
let result = await promise;
if (result.isError === true)
result.error = serializeError(result.error);
return result;
};
}
async handle_peer_discovery() {
if (this.peerDiscoveryAddress === void 0) throw new Error("Peer Discovery Address is not defined");
if (this.cluster === void 0) throw new Error("Cluster is not defined");
log(`[${this.name}] > Service > Checking if Peer Discovery Service is active`);
log(`[${this.name}] > Service > Peer Discovery Address: ${this.peerDiscoveryAddress.host}:${this.peerDiscoveryAddress.port}`);
if (await isServerActive(this.peerDiscoveryAddress) === false)
throw new Error("Peer Discovery Service is not active");
this.peerDiscovery = new PeerDiscoveryClient(this.peerDiscoveryAddress);
await this.peerDiscovery.connect();
this.peerDiscovery.register({ name: this.name, host: this.host, port: this.port });
this.peerAddresses = await this.peerDiscovery.getServices();
}
getServices() {
if (this.network === void 0) throw new Error("Network is not defined");
let services = this.network.getServices();
return services.map((c) => {
let name = c.getTargetName();
if (name === void 0) throw new Error("Service name is undefined");
return new ServiceClient(name, this.network, this.options);
}).reduce((acc, s) => {
acc[s.name] = s;
return acc;
}, {});
}
exit() {
setTimeout(() => {
if (this.processBalancer) this.processBalancer.exit();
if (this.requestQueue) this.requestQueue.exit();
if (this.peerDiscovery) this.peerDiscovery.exit();
if (this.nodes) this.nodes.exit();
if (this.network) this.network.close();
process.exit(0);
}, 1e3);
return true;
}
}
var Service_default = Service;
export {
Service_default as default
};
//# sourceMappingURL=Service.js.map