1+ import dgram from 'node:dgram' ;
2+ import crypto from 'node:crypto' ;
13import { Schema , model } from 'mongoose' ;
24import uniqueValidator from 'mongoose-unique-validator' ;
35import type { IServer , IServerConnectInfo , IServerMethods , ServerModel } from '@/types/mongoose/server' ;
46
7+ // * Kinda ugly to slap this in with the Mongoose stuff but it's fine for now
8+ // TODO - Maybe move this one day?
9+ const socket = dgram . createSocket ( 'udp4' ) ;
10+ const pendingHealthCheckRequests = new Map < string , ( ) => void > ( ) ;
11+
12+ socket . on ( 'message' , ( msg : Buffer , _rinfo : dgram . RemoteInfo ) => {
13+ const uuid = msg . toString ( ) ;
14+ const resolve = pendingHealthCheckRequests . get ( uuid ) ;
15+
16+ if ( resolve ) {
17+ resolve ( ) ;
18+ }
19+ } ) ;
20+
21+ socket . bind ( ) ;
22+
23+ function healthCheck ( target : { host : string ; port : number } ) : Promise < string > {
24+ return new Promise ( ( resolve , reject ) => {
25+ const uuid = crypto . randomUUID ( ) ;
26+
27+ const timeout = setTimeout ( ( ) => {
28+ pendingHealthCheckRequests . delete ( uuid ) ;
29+ reject ( new Error ( 'No valid response received' ) ) ;
30+ } , 5 * 1000 ) ; // TODO - Make this configurable? 5 seconds seems fine for now
31+
32+ pendingHealthCheckRequests . set ( uuid , ( ) => {
33+ clearTimeout ( timeout ) ;
34+ pendingHealthCheckRequests . delete ( uuid ) ;
35+ resolve ( target . host ) ;
36+ } ) ;
37+
38+ socket . send ( Buffer . from ( uuid ) , target . port , target . host , ( error ) => {
39+ if ( error ) {
40+ clearTimeout ( timeout ) ;
41+ pendingHealthCheckRequests . delete ( uuid ) ;
42+ reject ( error ) ;
43+ }
44+ } ) ;
45+ } ) ;
46+ }
47+
548const ServerSchema = new Schema < IServer , ServerModel , IServerMethods > ( {
649 client_id : String ,
750 ip : {
@@ -20,7 +63,8 @@ const ServerSchema = new Schema<IServer, ServerModel, IServerMethods>({
2063 access_mode : String ,
2164 maintenance_mode : Boolean ,
2265 device : Number ,
23- aes_key : String
66+ aes_key : String ,
67+ health_check_port : Number
2468} ) ;
2569
2670ServerSchema . plugin ( uniqueValidator , { message : '{PATH} already in use.' } ) ;
@@ -31,9 +75,26 @@ ServerSchema.method('getServerConnectInfo', async function (): Promise<IServerCo
3175 throw new Error ( `No IP configured for server ${ this . _id } ` ) ;
3276 }
3377
34- const randomIp = ipList [ Math . floor ( Math . random ( ) * ipList . length ) ] ;
78+ const healthCheckTargets = ipList . map ( ip => ( {
79+ host : ip ,
80+ port : this . health_check_port
81+ } ) ) ;
82+
83+ let target : string | undefined ;
84+
85+ try {
86+ // * Pick the first address that wins the health check. If no address responds in 5 seconds
87+ // * nothing is returned
88+ target = await Promise . race ( healthCheckTargets . map ( target => healthCheck ( target ) ) ) ;
89+ } catch {
90+ // * Eat error for now, this means that no address responded in time
91+ // TODO - Handle this
92+ }
93+
94+ const randomIP = ipList [ Math . floor ( Math . random ( ) * ipList . length ) ] ;
95+
3596 return {
36- ip : randomIp ,
97+ ip : target || randomIP , // * Just use a random IP if nothing responded in time and Hope For The Best:tm:
3798 port : this . port
3899 } ;
39100} ) ;
0 commit comments