Skip to content

Commit 84094ed

Browse files
committed
feat: add basic health check before returning server address in getServerConnectInfo
1 parent da534ac commit 84094ed

File tree

2 files changed

+65
-3
lines changed

2 files changed

+65
-3
lines changed

src/models/server.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,50 @@
1+
import dgram from 'node:dgram';
2+
import crypto from 'node:crypto';
13
import { Schema, model } from 'mongoose';
24
import uniqueValidator from 'mongoose-unique-validator';
35
import 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+
548
const 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

2670
ServerSchema.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
});

src/types/mongoose/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface IServer {
1313
maintenance_mode: boolean;
1414
device: number;
1515
aes_key: string;
16+
health_check_port: number;
1617
}
1718

1819
export interface IServerConnectInfo {

0 commit comments

Comments
 (0)