UNPKG

status-sharding

Version:

Welcome to Status Sharding! This package is designed to provide an efficient and flexible solution for sharding Discord bots, allowing you to scale your bot across multiple processes or workers.

337 lines (336 loc) 19.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClusterManager = void 0; const reCluster_1 = require("../plugins/reCluster"); const heartbeat_1 = require("../plugins/heartbeat"); const shardingUtils_1 = require("../other/shardingUtils"); const broker_1 = require("../handlers/broker"); const promise_1 = require("../handlers/promise"); const cluster_1 = require("./cluster"); const queue_1 = require("../handlers/queue"); const map_1 = __importDefault(require("../other/map")); const events_1 = __importDefault(require("events")); const path_1 = __importDefault(require("path")); const os_1 = __importDefault(require("os")); const fs_1 = __importDefault(require("fs")); /** Manager for the Clusters. */ class ClusterManager extends events_1.default { file; /** Check if all clusters are ready. */ ready; /** IPC Broker for the ClusterManager. */ broker; /** Options for the ClusterManager */ options; /** Promise Handler for the ClusterManager */ promise; /** A collection of all clusters the manager spawned. */ clusters; /** ReCluster Manager for the ClusterManager */ reCluster; /** Heartbeat Manager for the ClusterManager */ heartbeat; /** Queue for the ClusterManager */ clusterQueue; /** Creates an instance of ClusterManager. */ constructor(file, options) { super(); this.file = file; if (!file) throw new Error('CLIENT_INVALID_OPTION | No File specified.'); this.file = path_1.default.isAbsolute(file) ? file : path_1.default.resolve(process.cwd(), file); if (!fs_1.default.statSync(this.file)?.isFile()) throw new Error('CLIENT_INVALID_OPTION | Provided file is not an actual file.'); else if (options.mode && options.mode !== 'worker' && options.mode !== 'process') throw new RangeError('CLIENT_INVALID_OPTION | Cluster mode must be "worker" or "process".'); this.options = { ...options, totalShards: options.totalShards === undefined ? -1 : options.totalShards, totalClusters: options.totalClusters === undefined ? -1 : options.totalClusters, shardsPerClusters: options.shardsPerClusters === undefined ? -1 : options.shardsPerClusters, respawn: options.respawn === undefined ? true : options.respawn, heartbeat: shardingUtils_1.ShardingUtils.mergeObjects(options.heartbeat || {}, { enabled: true, timeout: 8000, interval: 2000, maxRestarts: -1, maxMissedHeartbeats: 2, }), packageType: null, mode: options.mode || 'worker', shardList: [], clusterList: [], spawnOptions: { timeout: options.spawnOptions?.timeout ?? -1, delay: options.spawnOptions?.delay ?? 7000, }, }; process.env.TOTAL_SHARDS = String(options.totalShards); process.env.CLUSTER_COUNT = String(options.totalClusters); process.env.CLUSTER_MANAGER_MODE = options.mode; process.env.DISCORD_TOKEN = String(options.token) || undefined; process.env.CLUSTER_QUEUE_MODE = options.queueOptions?.mode ?? 'auto'; this.ready = false; this.clusters = new map_1.default(); this.promise = new promise_1.PromiseHandler(this); this.broker = new broker_1.IPCBrokerManager(this); this.reCluster = new reCluster_1.ReClusterManager(this); this.heartbeat = this.options.heartbeat.enabled ? new heartbeat_1.HeartbeatManager(this) : null; this.clusterQueue = new queue_1.Queue(this.options.queueOptions || { mode: 'auto', timeout: this.options.spawnOptions.timeout || -1, }); this._debug('[ClusterManager] Initialized successfully.'); } /** Spawns multiple internal clusters. */ async spawn() { if (this.options.spawnOptions.delay < 6000) process.emitWarning('Spawn Delay is smaller than 6s, this can cause global rate limits on /gateway/bot', { code: 'SHARDING_DELAY', }); if (this.options.token) { if (this.options.token?.includes('Bot ') || this.options.token?.includes('Bearer ')) this.options.token = this.options.token.slice(this.options.token.indexOf(' ') + 1); else this.options.token = this.options.token.trim(); } const cpuCores = os_1.default.cpus().length; this.options.totalShards = this.options.totalShards !== -1 ? this.options.totalShards : this.options.token ? await shardingUtils_1.ShardingUtils.getRecommendedShards(this.options.token) || 1 : 1; if (this.options.totalClusters === -1) { if (this.options.totalShards < 1) this.options.totalClusters = cpuCores; else { const idealClusters = Math.max(Math.ceil(this.options.totalShards / 7), Math.min(cpuCores, this.options.totalShards)); this.options.totalClusters = idealClusters; } } this.options.shardsPerClusters = (this.options.shardsPerClusters === -1) ? Math.ceil(this.options.totalShards / this.options.totalClusters) : this.options.shardsPerClusters; if (this.options.totalShards < 1) this.options.totalShards = 1; if (this.options.totalClusters < 1) this.options.totalClusters = 1; if (this.options.totalClusters > this.options.totalShards) { this._debug(`[ClusterManager] Warning: More clusters (${this.options.totalClusters}) than shards (${this.options.totalShards}). Adjusting clusters to match shards.`); this.options.totalClusters = this.options.totalShards; } if (this.options.shardsPerClusters > this.options.totalShards) { console.warn('[ClusterManager] Shards Per Cluster is bigger than Total Shards, setting it to Total Shards.'); this.options.shardsPerClusters = this.options.totalShards; } else if (this.options.shardsPerClusters > (this.options.totalShards / this.options.totalClusters)) { console.warn('[ClusterManager] Shards Per Cluster is bigger than optimal distribution, adjusting to optimal value.'); this.options.shardsPerClusters = Math.ceil(this.options.totalShards / this.options.totalClusters); } if (this.options.totalClusters > cpuCores * 2) { console.warn(`[ClusterManager] Warning: Running ${this.options.totalClusters} clusters on ${cpuCores} CPU cores. This may impact performance. Consider reducing clusters or upgrading hardware.`); } if (this.options.mode === 'worker' && this.options.totalClusters > cpuCores * 4) { console.warn(`[ClusterManager] Warning: ${this.options.totalClusters} worker threads may cause thread pool exhaustion. Consider using 'process' mode or reducing clusters.`); } if (!this.options.shardList?.length) this.options.shardList = new Array(this.options.totalShards).fill(0).map((_, i) => i); if (this.options.shardsPerClusters) this.options.totalClusters = Math.ceil((this.options.shardList?.length || 0) / this.options.shardsPerClusters) || 1; if (!this.options.clusterList?.length) this.options.clusterList = new Array(this.options.totalClusters).fill(0).map((_, i) => i); if (this.options.clusterList.length !== this.options.totalClusters) this.options.totalClusters = this.options.clusterList.length; if (this.options.shardsPerClusters < 1) throw new Error('CLIENT_INVALID_OPTION | Shards Per Cluster must be at least 1.'); if (this.options.totalShards < this.options.shardList.length) throw new Error('CLIENT_INVALID_OPTION | Shard List is bigger than Total Shards.'); if (this.options.totalClusters < this.options.clusterList.length) throw new Error('CLIENT_INVALID_OPTION | Cluster List is bigger than Total Clusters.'); if (this.options.shardList.some((shard) => shard < 0)) throw new Error('CLIENT_INVALID_OPTION | Shard List has invalid shards.'); if (this.options.clusterList.some((cluster) => cluster < 0)) throw new Error('CLIENT_INVALID_OPTION | Cluster List has invalid clusters.'); this._debug(`[ClusterManager] Spawning ${this.options.totalClusters} clusters with ${this.options.totalShards} shards in total (${this.options.shardsPerClusters} shards per cluster)`); const listOfShardsForCluster = shardingUtils_1.ShardingUtils.chunkArray(this.options.shardList || [], this.options.shardsPerClusters || this.options.totalShards); if (listOfShardsForCluster.length !== this.options.totalClusters) this.options.totalClusters = listOfShardsForCluster.length; this.options.totalShards = listOfShardsForCluster.reduce((acc, curr) => acc + curr.length, 0); this.options.totalClusters = listOfShardsForCluster.length; this.options.shardsPerClusters = Math.ceil(this.options.totalShards / this.options.totalClusters); for (let i = 0; i < this.options.totalClusters; i++) { if (listOfShardsForCluster[i]) { this._debug(`[ClusterManager] Added Cluster ${this.options.clusterList?.[i] || i} to the queue with ${listOfShardsForCluster[i]} shards.`); this.clusterQueue.add({ timeout: this.options.spawnOptions.delay * (listOfShardsForCluster[i]?.length ?? 0), args: [this.options.spawnOptions.timeout !== -1 ? this.options.spawnOptions.timeout + this.options.spawnOptions.delay * (listOfShardsForCluster[i]?.length ?? 0) : this.options.spawnOptions.timeout], run: (...timeout) => this.createCluster(this.options.clusterList?.[i] || i, listOfShardsForCluster[i] || []).spawn(...timeout), }); } } return this.clusterQueue.start(); } /** Sends a message to all clusters. */ async broadcast(message, ignoreClusters) { const clusters = Array.from(this.clusters.values()).filter((c) => !ignoreClusters?.includes(c.id)); const promises = Array.from(clusters).map((cluster) => cluster.send(message)); await Promise.allSettled(promises); return; } /** Kills all running clusters and respawns them. */ async respawnAll(clusterDelay = 8000, respawnDelay = 5500, timeout = -1, except = []) { this.promise.nonces.clear(); this._debug('[ClusterManager] Respawning all clusters.'); const listOfShardsForCluster = shardingUtils_1.ShardingUtils.chunkArray(this.options.shardList || [], this.options.shardsPerClusters || this.options.totalShards); const clustersToRestart = Array.from(this.clusters.values()).filter((c) => !except.includes(c.id)); if (clustersToRestart.length === 0) return this.clusters; let i = 0; while (clustersToRestart.length > 0) { const cluster = clustersToRestart.shift(); if (!cluster) continue; this._debug(`Respawning cluster ${cluster.id}`); await cluster.respawn(respawnDelay, timeout); const length = listOfShardsForCluster[i]?.length || this.options.totalShards / this.options.totalClusters; if (clusterDelay > 0) { this._debug(`Delaying ${length * clusterDelay}ms for cluster ${cluster.id}`); await shardingUtils_1.ShardingUtils.delayFor(length * clusterDelay); } i++; } return this.clusters; } /** Kills specific clusters and respawns them. */ async respawnClusters(clusters, clusterDelay = 8000, respawnDelay = 5500, timeout = -1) { this.promise.nonces.clear(); this._debug('[ClusterManager] Respawning specific clusters.'); const listOfShardsForCluster = shardingUtils_1.ShardingUtils.chunkArray(this.options.shardList || [], this.options.shardsPerClusters || this.options.totalShards); const clustersToRestart = Array.from(this.clusters.values()).filter((c) => clusters.includes(c.id)); if (clustersToRestart.length === 0) return this.clusters; let i = 0; for (const cluster of clustersToRestart) { this._debug(`Respawning cluster ${cluster.id}`); await cluster.respawn(respawnDelay, timeout); const length = listOfShardsForCluster[i]?.length || this.options.totalShards / this.options.totalClusters; if (clusterDelay > 0 && clustersToRestart.length > 1) { this._debug(`Delaying ${length * clusterDelay}ms for cluster ${cluster.id}`); await shardingUtils_1.ShardingUtils.delayFor(length * clusterDelay); } i++; } return this.clusters; } /** Evaluates a script on the Manager. */ async eval(script, options) { let result; let error; // Manager is not allowed to crash. try { result = await eval(typeof script === 'string' ? script : `(${script})(this,${options?.context ? JSON.stringify(options.context) : undefined})`); } catch (err) { error = err; } return { result: result, error: error }; } /** Evaluates a script on all clusters, or a given cluster, in the context of the Client. */ async broadcastEval(script, options) { if (this.clusters.size === 0) return Promise.reject(new Error('CLUSTERING_NO_CLUSTERS | No clusters have been spawned (#1).')); else if ((options?.cluster !== undefined || options?.shard !== undefined) && options?.guildId !== undefined) return Promise.reject(new Error('CLUSTERING_INVALID_OPTION | Cannot use both guildId and cluster/shard options.')); if (options?.cluster !== undefined) { const clusterIds = Array.isArray(options.cluster) ? options.cluster : [options.cluster]; if (clusterIds.some((c) => c < 0)) return Promise.reject(new RangeError('CLUSTER_ID_OUT_OF_RANGE | Cluster Ids must be greater than or equal to 0.')); } if (options?.guildId) options.cluster = await new Promise((resolve) => resolve(shardingUtils_1.ShardingUtils.clusterIdForGuildId(options.guildId, this.options.totalShards, this.options.totalClusters))).catch(() => undefined); if (options?.shard !== undefined) { const shardIds = Array.isArray(options.shard) ? options.shard : [options.shard]; if (shardIds.some((s) => s < 0)) return Promise.reject(new RangeError('SHARD_ID_OUT_OF_RANGE | Shard Ids must be greater than or equal to 0.')); const clusterIds = new Set(); for (const cluster of this.clusters.values()) { if (cluster.shardList.some((shard) => shardIds.includes(shard))) clusterIds.add(cluster.id); } if (clusterIds.size === 0) return Promise.reject(new Error('CLUSTERING_CLUSTER_NOT_FOUND | No clusters found for the given shard Ids.')); if (clusterIds.size === 1) options.cluster = clusterIds.values().next().value; else options.cluster = Array.from(clusterIds); } const promises = []; if (typeof options?.cluster === 'number') { const cluster = this.clusters.get(options.cluster); if (!cluster) return Promise.reject(new Error('CLUSTERING_CLUSTER_NOT_FOUND | No cluster was found with the given Id.')); promises.push(cluster.evalOnClient(script, options)); } else if (Array.isArray(options?.cluster)) { const clusters = Array.from(this.clusters.values()).filter((c) => options?.cluster?.includes(c.id)); if (clusters.length === 0) return Promise.reject(new Error('CLUSTERING_CLUSTER_NOT_FOUND | No clusters were found with the given Ids.')); for (const cluster of clusters) { promises.push(cluster.evalOnClient(script, options)); } } else { for (const cluster of this.clusters.values()) { promises.push(cluster.evalOnClient(script, options)); } } if (options?.useAllSettled || this.options.advanced?.proceedBroadcastIfClusterDead) { const results = (await Promise.allSettled(promises)).filter((r) => r.status === 'fulfilled'); return results.map((r) => r.value); } else { return Promise.all(promises); } } /** Evaluates a script on a given Cluster's Client. */ async evalOnClusterClient(cluster, script, options) { if (this.clusters.size === 0) return Promise.reject(new Error('CLUSTERING_NO_CLUSTERS | No clusters have been spawned (#2).')); else if (typeof cluster !== 'number' || cluster < 0) return Promise.reject(new RangeError('CLUSTER_ID_OUT_OF_RANGE | Cluster Ids must be greater than or equal to 0.')); const cl = this.clusters.get(cluster); if (!cl) return Promise.reject(new Error('CLUSTERING_CLUSTER_NOT_FOUND | Cluster with id ' + cluster + ' was not found (#1).')); return cl.evalOnClient(script, options); } /** Evaluates a script on a given Cluster. */ async evalOnCluster(cluster, script, options) { if (this.clusters.size === 0) return Promise.reject(new Error('CLUSTERING_NO_CLUSTERS | No clusters have been spawned (#3).')); else if (typeof cluster !== 'number' || cluster < 0) return Promise.reject(new RangeError('CLUSTER_ID_OUT_OF_RANGE | Cluster Ids must be greater than or equal to 0.')); const cl = this.clusters.get(cluster); if (!cl) return Promise.reject(new Error('CLUSTERING_CLUSTER_NOT_FOUND | Cluster with id ' + cluster + ' was not found (#2).')); return cl.eval(script, options); } /** Evaluates a script on specific guild. */ async evalOnGuild(guildId, script, options) { if (this.clusters.size === 0) return Promise.reject(new Error('CLUSTERING_NO_CLUSTERS | No clusters have been spawned (#4).')); else if (typeof guildId !== 'string') return Promise.reject(new TypeError('CLUSTERING_GUILD_ID_INVALID | Guild Id must be a string.')); else if (this.options.packageType !== 'discord.js') return Promise.reject(new Error('CLUSTERING_EVAL_GUILD_UNSUPPORTED | evalOnGuild is only supported in discord.js package type.')); return this.broadcastEval(shardingUtils_1.ShardingUtils.parseInput(script, options?.context, this.options.packageType, `this?.guilds?.cache?.get('${guildId}')`), { ...options, guildId }).then((e) => e?.find((r) => r !== undefined)); } /** Creates a new cluster. (Using this method is usually not necessary if you use the spawn method.) */ createCluster(id, shardsToSpawn, recluster = false) { const cluster = new cluster_1.Cluster(this, id, shardsToSpawn); if (!recluster) this.clusters.set(id, cluster); this.emit('clusterCreate', cluster); this.heartbeat?.getClusterStats(id); return cluster; } /** Logs out the Debug Messages. */ _debug(message) { this.emit('debug', message); } } exports.ClusterManager = ClusterManager;