discord-hybrid-sharding
Version:
The first package which combines sharding manager & internal sharding to save a lot of resources, which allows clustering!
164 lines (163 loc) • 8.34 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReClusterManager = void 0;
const Util_1 = require("../Util/Util");
class ReClusterManager {
options;
name;
onProgress;
manager;
constructor(options) {
if (!options)
this.options = {};
else
this.options = options;
this.name = 'recluster';
this.onProgress = false;
}
build(manager) {
manager[this.name] = this;
this.manager = manager;
return this;
}
/**
* Execute a Zero Downtime Restart on all Clusters with an updated totalShards (count) or a scheduled restart.
* @param options
* @param options.delay
* @param options.timeout
* @param options.totalShards
* @param options.totalClusters
* @param options.shardsPerClusters
* @param options.shardClusterList
* @param options.shardList
* @param options.restartMode
*/
async start(options) {
let { delay, timeout, totalClusters, totalShards, shardsPerClusters, shardClusterList, shardList = this.manager?.shardList, restartMode = 'gracefulSwitch', } = options || { restartMode: 'gracefulSwitch' };
if (this.onProgress)
throw new Error('Zero Downtime Reclustering is already in progress');
if (!this.manager)
throw new Error('Manager is missing on ReClusterManager');
if (totalShards) {
if (!this.manager?.token)
throw new Error('Token must be defined on manager, when totalShards is set on auto');
if (totalShards === 'auto' || totalShards === -1)
totalShards = await (0, Util_1.fetchRecommendedShards)(this.manager.token);
this.manager.totalShards = totalShards;
}
if (totalClusters)
this.manager.totalClusters = totalClusters;
if (shardsPerClusters) {
this.manager.shardsPerClusters = shardsPerClusters;
this.manager.totalClusters = Math.ceil(this.manager.totalShards / this.manager.shardsPerClusters);
}
if (shardList)
this.manager.shardList = shardList;
else
this.manager.shardList = Array.from(Array(this.manager.totalShards).keys());
if (shardClusterList)
this.manager.shardClusterList = shardClusterList;
else
this.manager.shardClusterList = (0, Util_1.chunkArray)(this.manager.shardList, Math.ceil(this.manager.shardList.length / this.manager.totalClusters));
if (this.manager.shardClusterList.length !== this.manager.totalClusters) {
this.manager.totalClusters = this.manager.shardClusterList.length;
}
this.manager._debug([
'[↻][ReClustering] Starting... Zerodowntime Reclustering',
`├── Mode: ${restartMode}`,
`├── Total Shards: ${this.manager.totalShards}`,
`├── Total Clusters: ${this.manager.totalClusters}`,
`├── Shards Per Cluster: ${this.manager.shardsPerClusters}`,
`├── Shard Cluster List: ${this.manager.shardClusterList.join(', ')}`,
`└── Shard List: ${this.manager.shardList.join(', ')}`,
].join('\n'));
return this._start({ restartMode, timeout, delay });
}
/**
* @param options
* @param options.delay The delay to wait between each cluster spawn
* @param options.timeout The readyTimeout to wait until the cluster spawn promise is rejected
* @param options.restartMode The restartMode of the clusterManager, gracefulSwitch = waits until all new clusters have spawned with maintenance mode, rolling = Once the Cluster is Ready, the old cluster will be killed
*/
async _start({ restartMode = 'gracefulSwitch', timeout = 30000 * 6, delay = 7000 }) {
if (!this.manager)
throw new Error('Manager is missing on ReClusterManager');
process.env.MAINTENANCE = 'recluster';
this.manager.triggerMaintenance('recluster');
this.manager._debug('[↻][ReClustering] Enabling Maintenance Mode on all clusters');
let switchClusterAfterReady = false;
// when no shard settings have been updated
switchClusterAfterReady = restartMode === 'rolling'; //gracefulSwitch, spawn all clusters and kill all old clusters, when new clusters are ready
const newClusters = new Map();
const oldClusters = new Map();
Array.from(this.manager.clusters.values()).forEach(cluster => {
oldClusters.set(cluster.id, cluster);
});
for (let i = 0; i < this.manager.totalClusters; i++) {
const length = this.manager.shardClusterList[i]?.length || this.manager.totalShards / this.manager.totalClusters;
const clusterId = this.manager.clusterList[i] || i;
const readyTimeout = timeout !== -1 ? timeout + delay * length : timeout;
const spawnDelay = delay * length;
this.manager.queue.add({
run: (...a) => {
if (!this.manager)
throw new Error('Manager is missing on ReClusterManager');
const cluster = this.manager.createCluster(clusterId, this.manager.shardClusterList[i], this.manager.totalShards, true);
newClusters.set(clusterId, cluster);
this.manager._debug(`[↻][ReClustering][${clusterId}] Spawning... Cluster`);
return cluster.spawn(...a).then(c => {
if (!this.manager)
throw new Error('Manager is missing on ReClusterManager');
this.manager._debug(`[↻][ReClustering][${clusterId}] Cluster Ready`);
if (switchClusterAfterReady) {
const oldCluster = this.manager.clusters.get(clusterId);
if (oldCluster) {
oldCluster.kill({ force: true, reason: 'reclustering' });
oldClusters.delete(clusterId);
}
this.manager.clusters.set(clusterId, cluster);
cluster.triggerMaintenance(undefined);
this.manager._debug(`[↻][ReClustering][${clusterId}] Switched OldCluster to NewCluster and exited Maintenance Mode`);
}
return c;
});
},
args: [readyTimeout],
timeout: spawnDelay,
});
}
await this.manager.queue.start();
if (oldClusters.size) {
this.manager._debug('[↻][ReClustering] Killing old clusters');
for (const [id, cluster] of Array.from(oldClusters)) {
cluster.kill({ force: true, reason: 'reclustering' });
this.manager._debug(`[↻][ReClustering][${id}] Killed OldCluster`);
this.manager.clusters.delete(id);
}
oldClusters.clear();
}
if (!switchClusterAfterReady) {
this.manager._debug('[↻][ReClustering] Starting exiting Maintenance Mode on all clusters and killing old clusters');
for (let i = 0; i < this.manager.totalClusters; i++) {
const clusterId = this.manager.clusterList[i] || i;
const cluster = newClusters.get(clusterId);
const oldCluster = this.manager.clusters.get(clusterId);
if (!cluster)
continue;
if (oldCluster) {
oldCluster.kill({ force: true, reason: 'reclustering' });
oldClusters.delete(clusterId);
}
this.manager.clusters.set(clusterId, cluster);
cluster.triggerMaintenance();
this.manager._debug(`[↻][ReClustering][${clusterId}] Switched OldCluster to NewCluster and exited Maintenance Mode`);
}
}
newClusters.clear();
this.onProgress = false;
process.env.MAINTENANCE = undefined;
this.manager._debug('[↻][ReClustering] Finished ReClustering');
return { success: true };
}
}
exports.ReClusterManager = ReClusterManager;