2022-11-06 18:35:53 +01:00
const ChildProcess = require ( 'node:child_process' ) ;
const { EventEmitter } = require ( 'node:events' ) ;
const { Util } = require ( '../util' ) ;
2023-02-10 22:56:26 +01:00
// Give subprocess 90s to shut down before being forcibly killed
const KillTO = 90 * 1000 ;
2022-11-06 18:35:53 +01:00
class Shard extends EventEmitter {
constructor ( controller , id , options = { } ) {
super ( ) ;
this . controller = controller ;
2023-02-06 14:12:22 +01:00
if ( typeof id !== 'number' || isNaN ( id ) )
throw new Error ( 'Missing ID' ) ;
2022-11-06 18:35:53 +01:00
this . id = id ;
2023-02-06 14:12:22 +01:00
if ( ! options . path )
throw new Error ( 'Missing path to file to fork' ) ;
2022-11-06 18:35:53 +01:00
this . filePath = options . path ;
this . args = options . args || [ ] ;
this . execArgv = options . execArgv || [ ] ;
this . env = options . env || { } ;
this . _respawn = options . respawn || false ;
this . serverOptions = options . serverOptions || { } ;
this . ready = false ;
this . process = null ;
2022-11-06 19:31:41 +01:00
this . fatal = false ;
2022-11-06 18:35:53 +01:00
2022-11-25 14:43:26 +01:00
// Keep track of crashes for preventing crash loops
this . crashes = [ ] ;
// Set in the spawn method
this . spawnedAt = null ;
2023-02-10 22:56:26 +01:00
this . _awaitingShutdown = null ;
2022-11-06 18:35:53 +01:00
}
async spawn ( waitForReady = false ) {
2023-02-06 14:12:22 +01:00
if ( this . fatal )
throw new Error ( ` [shard- ${ this . id } ] Process died fatally and cannot be restarted. Fix the issue before trying again. ` ) ;
if ( this . process )
throw new Error ( ` [shard- ${ this . id } ] A process for this shard already exists! ` ) ;
2022-11-06 18:35:53 +01:00
this . process = ChildProcess . fork ( this . filePath , this . args , { env : { ... this . env , SHARD _ID : this . id } , execArgv : this . execArgv } )
. on ( 'message' , this . _handleMessage . bind ( this ) )
2023-02-06 13:33:27 +01:00
. on ( 'exit' , this . _handleExit . bind ( this ) )
2022-11-06 18:35:53 +01:00
. on ( 'disconnect' , this . _handleDisconnect . bind ( this ) ) ; // Don't know if this is going to help, but monitoring whether this gets called whenever a process on its own closes the IPC channel
this . process . once ( 'spawn' , ( ) => {
this . emit ( 'spawn' ) ;
this . process . send ( { _start : this . serverOptions } ) ;
2022-11-25 14:43:26 +01:00
this . spawnedAt = Date . now ( ) ;
2022-11-06 18:35:53 +01:00
} ) ;
2023-02-06 14:12:22 +01:00
if ( ! waitForReady )
return ;
2022-11-06 18:35:53 +01:00
return new Promise ( ( resolve , reject ) => {
this . once ( 'ready' , resolve ) ;
this . once ( 'disconnect' , ( ) => reject ( new Error ( ` [shard- ${ this . id } ] Shard disconnected while starting up ` ) ) ) ;
this . once ( 'death' , ( ) => reject ( new Error ( ` [shard- ${ this . id } ] Shard died while starting ` ) ) ) ;
setTimeout ( ( ) => reject ( new Error ( ` [shard- ${ this . id } ] Shard timed out while starting ` ) ) , 30_000 ) ;
} ) ;
}
async respawn ( delay = 500 ) {
await this . kill ( ) ;
2023-02-06 14:12:22 +01:00
if ( delay )
await Util . wait ( delay ) ;
2022-11-06 18:35:53 +01:00
return this . spawn ( ) ;
}
/ * *
* Sends a shutdown command to the shard , if it doesn ' t respond within 5 seconds it gets killed
* TODO : Add a check to see if the process actually ends and print out a warning if it hasn ' t
*
* @ return { * }
* @ memberof Shard
* /
kill ( ) {
if ( this . process ) {
return new Promise ( ( resolve ) => {
// Clear out all other exit listeners so they don't accidentally start the process up again
this . process . removeAllListeners ( 'exit' ) ;
// Set timeout for force kill
const to = setTimeout ( ( ) => {
this . process . kill ( ) ;
resolve ( ) ;
2023-02-10 22:56:26 +01:00
} , KillTO ) ;
2022-11-06 18:35:53 +01:00
// Gracefully handle exit
2023-02-06 13:08:41 +01:00
this . process . once ( 'exit' , ( code , signal ) => {
2022-11-06 18:35:53 +01:00
clearTimeout ( to ) ;
2023-02-06 13:08:41 +01:00
this . _handleExit ( code , signal , false ) ;
2022-11-06 18:35:53 +01:00
resolve ( ) ;
} ) ;
// Clear the force kill timeout if the process responds with a shutdown echo, allowing it time to gracefully close all connections
this . once ( 'shutdown' , ( ) => {
clearTimeout ( to ) ;
} ) ;
this . process . send ( { _shutdown : true } ) ;
} ) ;
}
2023-02-06 13:08:41 +01:00
this . _handleExit ( null , null , false ) ;
2022-11-06 18:35:53 +01:00
}
send ( message ) {
return new Promise ( ( resolve , reject ) => {
if ( this . ready && this . process ) {
this . process . send ( message , err => {
2023-02-06 14:12:22 +01:00
if ( err )
reject ( err ) ;
else
resolve ( ) ;
2022-11-06 18:35:53 +01:00
} ) ;
2023-02-06 14:12:22 +01:00
} else {
reject ( new Error ( ` [shard- ${ this . id } ] Cannot send message to dead shard. ` ) ) ;
}
2022-11-06 18:35:53 +01:00
} ) ;
}
2023-02-10 22:56:26 +01:00
awaitShutdown ( ) {
this . _respawn = false ;
return new Promise ( ( resolve ) => {
this . _awaitingShutdown = resolve ;
} ) ;
}
2022-11-06 18:35:53 +01:00
_handleMessage ( message ) {
if ( message ) {
if ( message . _ready ) {
this . ready = true ;
this . emit ( 'ready' ) ;
return ;
} else if ( message . _shutdown ) {
2023-02-10 22:56:26 +01:00
setTimeout ( ( ) => {
if ( this . process )
this . process . kill ( 'SIGKILL' ) ;
} , KillTO ) ;
2022-11-06 18:35:53 +01:00
this . ready = false ;
this . emit ( 'shutdown' ) ;
return ;
2022-11-06 19:31:41 +01:00
} else if ( message . _fatal ) {
this . process . removeAllListeners ( ) ;
this . ready = false ;
this . fatal = true ;
2023-02-06 13:08:41 +01:00
this . _handleExit ( null , null , false ) ;
2022-12-22 09:49:42 +01:00
this . emit ( 'fatal' , message ) ;
2022-11-06 18:35:53 +01:00
}
}
2022-11-09 10:20:06 +01:00
this . emit ( 'message' , message ) ;
2022-11-06 18:35:53 +01:00
}
_handleDisconnect ( ) {
this . emit ( 'disconnect' ) ;
}
2023-02-06 13:08:41 +01:00
_handleExit ( code , signal , respawn = this . _respawn ) {
2023-02-06 14:12:22 +01:00
if ( this . process )
this . process . removeAllListeners ( ) ;
2022-11-06 18:35:53 +01:00
this . emit ( 'death' ) ;
2023-02-10 22:56:26 +01:00
if ( this . _awaitingShutdown )
this . _awaitingShutdown ( ) ;
2022-11-06 18:35:53 +01:00
2023-02-06 14:12:22 +01:00
if ( code !== 0 )
this . crashes . push ( Date . now ( ) - this . spawnedAt ) ;
2023-02-06 13:08:41 +01:00
2022-11-06 18:35:53 +01:00
this . ready = false ;
this . process = null ;
2023-02-06 13:08:41 +01:00
const len = this . crashes . length ;
if ( len > 2 ) {
const last3 = this . crashes . slice ( len - 3 ) ;
const sum = last3 . reduce ( ( s , val ) => {
s += val ;
return s ;
} , 0 ) ;
const avg = sum / 3 ;
// If average run duration is below 60 mins send a notification about detected crash loop and stop the respawn
if ( avg < 60 * 60 * 1000 ) {
this . emit ( 'warn' , ` Crash loop detected, average run time for the last 3 spawns: ${ avg } ` ) ;
2022-11-25 14:43:26 +01:00
}
2023-02-06 13:08:41 +01:00
respawn = false ;
2022-11-25 14:43:26 +01:00
}
2023-02-06 13:08:41 +01:00
2023-02-06 14:12:22 +01:00
if ( respawn )
this . spawn ( ) . catch ( err => this . emit ( 'error' , err ) ) ;
2023-02-06 13:08:41 +01:00
2022-11-06 18:35:53 +01:00
}
}
module . exports = Shard ;