Skip to content

Commit a5a05d6

Browse files
committed
Use ping package for latency checks; add initial + scheduled tests, update tests and deps
- Replace @network-utils/tcp-ping with the "ping" package and switch to ping.promise.probe API - Remove per-host port handling and adjust result parsing (use result.alive/result.time) - Reduce ping timeout (5000ms -> 500ms) for faster checks - Run an initial latency test on startup and schedule periodic tests every 15 minutes - Update tests to mock the new ping API and adjust expectations - Add "ping" to package.json dependencies
1 parent f48d8e3 commit a5a05d6

3 files changed

Lines changed: 116 additions & 141 deletions

File tree

radius-proxy/lib/radius-host-selector.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ping } from "@network-utils/tcp-ping"
1+
import * as ping from 'ping'
22
import { config } from "@/lib/config"
33
import { warn, info, debug } from "@/lib/log"
44

@@ -13,9 +13,23 @@ class RadiusHostSelector {
1313
private hostStatuses: Map<string, HostStatus> = new Map()
1414
private lastTestTime = 0
1515
private testInterval = 15 * 60 * 1000 // 15 minutes in milliseconds
16-
private testTimeout = 5000 // 5 seconds in milliseconds
16+
private testTimeout = 500 // 500 milliseconds
1717
private isTestingInProgress = false
1818

19+
constructor() {
20+
// Run initial latency test on startup to avoid waiting during first login
21+
this.runLatencyTests().catch(error => {
22+
warn(`[radius-host-selector] Initial latency test failed:`, error)
23+
})
24+
25+
// Run latency tests every 15 minutes automatically
26+
setInterval(() => {
27+
this.runLatencyTests().catch(error => {
28+
warn(`[radius-host-selector] Scheduled latency test failed:`, error)
29+
})
30+
}, this.testInterval)
31+
}
32+
1933
private getHostsFromConfig(): string[] {
2034
const radiusHost = config.RADIUS_HOST
2135
if (Array.isArray(radiusHost)) {
@@ -27,29 +41,26 @@ class RadiusHostSelector {
2741
return ['127.0.0.1'] // fallback
2842
}
2943

30-
private async testHostLatency(host: string, port: number): Promise<{ latency: number | null, isDown: boolean }> {
44+
private async testHostLatency(host: string): Promise<{ latency: number | null, isDown: boolean }> {
3145
try {
32-
debug(`[radius-host-selector] Testing latency for ${host}:${port}`)
46+
debug(`[radius-host-selector] Testing latency for ${host}`)
3347

34-
const result = await ping({
35-
address: host,
36-
port: port,
37-
timeout: this.testTimeout,
38-
attempts: 1
48+
const result = await ping.promise.probe(host, {
49+
timeout: this.testTimeout / 1000 // convert to seconds
3950
})
4051

4152
debug(`[radius-host-selector] Ping result for ${host}:`, result)
4253

43-
if (result.errors && result.errors.length > 0) {
44-
debug(`[radius-host-selector] Host ${host}:${port} is down - ${result.errors[0].error.message}`)
54+
if (!result.alive) {
55+
debug(`[radius-host-selector] Host ${host} is down`)
4556
return { latency: null, isDown: true }
4657
}
4758

48-
const latency = result.averageLatency
49-
debug(`[radius-host-selector] Host ${host}:${port} latency: ${latency}ms`)
59+
const latency = typeof result.time === 'number' ? result.time : null
60+
debug(`[radius-host-selector] Host ${host} latency: ${latency}ms`)
5061
return { latency, isDown: false }
5162
} catch (error) {
52-
warn(`[radius-host-selector] Error testing ${host}:${port}:`, error)
63+
warn(`[radius-host-selector] Error testing ${host}:`, error)
5364
return { latency: null, isDown: true }
5465
}
5566
}
@@ -68,7 +79,6 @@ class RadiusHostSelector {
6879
try {
6980
this.isTestingInProgress = true
7081
const hosts = this.getHostsFromConfig()
71-
const port = config.RADIUS_PORT || 1812
7282
const now = Date.now()
7383

7484
info(`[radius-host-selector] Testing latency for ${hosts.length} RADIUS hosts`)
@@ -77,7 +87,7 @@ class RadiusHostSelector {
7787
// Test all hosts in parallel
7888
const testPromises = hosts.map(async (host) => {
7989
debug(`[radius-host-selector] Starting test for ${host}`)
80-
const { latency, isDown } = await this.testHostLatency(host, port)
90+
const { latency, isDown } = await this.testHostLatency(host)
8191
debug(`[radius-host-selector] Test result for ${host}: latency=${latency}, isDown=${isDown}`)
8292

8393
const status: HostStatus = {

radius-proxy/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"lucide-react": "^0.544.0",
2121
"next": "15.5.4",
2222
"next-themes": "^0.4.6",
23+
"ping": "^1.0.0",
2324
"react": "19.1.0",
2425
"react-dom": "19.1.0",
2526
"sonner": "^2.0.7",

radius-proxy/tests/radius-host-selector.test.ts

Lines changed: 89 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,43 @@
11
import { expect, test, describe, beforeEach, mock } from "bun:test";
22
import { config, _invalidateConfigCache } from "@/lib/config";
33

4-
interface PingOptions {
5-
address: string;
6-
port?: number;
7-
timeout?: number;
8-
attempts?: number;
9-
}
10-
114
interface HostStatus {
12-
host: string;
13-
latency: number | null;
14-
isDown: boolean;
15-
lastChecked: number;
5+
host: string
6+
latency: number | null
7+
isDown: boolean
8+
lastChecked: number
169
}
1710

18-
// Mock the tcp-ping module to simulate different latency scenarios
19-
mock.module("@network-utils/tcp-ping", () => ({
20-
ping: async (options: PingOptions) => {
21-
const host = options.address;
22-
23-
// Simulate different latency scenarios
24-
if (host === "192.168.0.191") {
25-
return {
26-
averageLatency: 20,
27-
errors: [],
28-
maximumLatency: 25,
29-
minimumLatency: 15,
30-
options
31-
};
32-
} else if (host === "192.168.0.192") {
33-
return {
34-
averageLatency: 30,
35-
errors: [],
36-
maximumLatency: 35,
37-
minimumLatency: 25,
38-
options
39-
};
40-
} else if (host === "192.168.0.193") {
41-
// Simulate host down
42-
return {
43-
averageLatency: null,
44-
errors: [{ attempt: 1, error: new Error("Request timeout") }],
45-
maximumLatency: 0,
46-
minimumLatency: null,
47-
options
48-
};
49-
} else {
50-
// Default case for other hosts
51-
return {
52-
averageLatency: 50,
53-
errors: [],
54-
maximumLatency: 55,
55-
minimumLatency: 45,
56-
options
57-
};
11+
// Mock the ping module to simulate different latency scenarios
12+
mock.module('ping', () => ({
13+
default: {
14+
promise: {
15+
probe: async (host: string) => {
16+
// Simulate different latency scenarios
17+
if (host === "192.168.0.191") {
18+
return {
19+
alive: true,
20+
time: 20
21+
};
22+
} else if (host === "192.168.0.192") {
23+
return {
24+
alive: true,
25+
time: 30
26+
};
27+
} else if (host === "192.168.0.193") {
28+
// Simulate host down
29+
return {
30+
alive: false,
31+
time: 'unknown'
32+
};
33+
} else {
34+
// Default case for other hosts
35+
return {
36+
alive: true,
37+
time: 50
38+
};
39+
}
40+
}
5841
}
5942
}
6043
}));
@@ -130,33 +113,26 @@ describe("RADIUS Host Selector", () => {
130113
});
131114

132115
test("should demonstrate host selection logic with mocked responses", async () => {
133-
// Test the tcp-ping mock directly to verify it's working as expected
134-
const { ping } = await import("@network-utils/tcp-ping");
116+
// Test the ping mock directly to verify it's working as expected
117+
const ping = await import('ping');
135118

136119
// Test various scenarios to validate our mock logic
137120
const scenarios = [
138121
// Normal latency responses
139-
{ host: "192.168.0.191", expectedLatency: 20, expectedDown: false, description: "fastest host" },
140-
{ host: "192.168.0.192", expectedLatency: 30, expectedDown: false, description: "slower host" },
141-
{ host: "192.168.0.193", expectedLatency: null, expectedDown: true, description: "down host" },
142-
{ host: "10.0.0.100", expectedLatency: 50, expectedDown: false, description: "default case" }
122+
{ host: "192.168.0.191", expectedTime: 20, expectedAlive: true, description: "fastest host" },
123+
{ host: "192.168.0.192", expectedTime: 30, expectedAlive: true, description: "slower host" },
124+
{ host: "192.168.0.193", expectedTime: 'unknown', expectedAlive: false, description: "down host" },
125+
{ host: "10.0.0.100", expectedTime: 50, expectedAlive: true, description: "default case" }
143126
];
144127

145128
for (const scenario of scenarios) {
146-
const result = await ping({
147-
address: scenario.host,
148-
port: 1812,
149-
timeout: 5000,
150-
attempts: 1
129+
const result = await ping.default.promise.probe(scenario.host, {
130+
timeout: 5
151131
});
152132

153-
if (scenario.expectedDown) {
154-
expect(result.averageLatency).toBeNull();
155-
expect(result.errors.length).toBeGreaterThan(0);
156-
} else {
157-
expect(result.averageLatency).toBe(scenario.expectedLatency as number);
158-
expect(result.errors).toEqual([]);
159-
}
133+
expect(result.alive).toBe(scenario.expectedAlive);
134+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
135+
(expect(result.time) as any).toEqual(scenario.expectedTime);
160136
}
161137
});
162138

@@ -220,35 +196,29 @@ describe("RADIUS Host Selector", () => {
220196

221197
test("should select second host when it has lowest latency", async () => {
222198
// Mock where the SECOND host has the lowest latency, not the first
223-
mock.module("@network-utils/tcp-ping", () => ({
224-
ping: async (options: PingOptions) => {
225-
const host = options.address;
226-
if (host === "192.168.0.191") {
227-
return {
228-
averageLatency: 50, // First host is slowest
229-
errors: [],
230-
maximumLatency: 55,
231-
minimumLatency: 45,
232-
options
233-
};
234-
} else if (host === "192.168.0.192") {
235-
return {
236-
averageLatency: 15, // Second host is fastest
237-
errors: [],
238-
maximumLatency: 20,
239-
minimumLatency: 10,
240-
options
241-
};
242-
} else if (host === "192.168.0.193") {
243-
return {
244-
averageLatency: 30, // Third host is medium
245-
errors: [],
246-
maximumLatency: 35,
247-
minimumLatency: 25,
248-
options
249-
};
199+
mock.module('ping', () => ({
200+
default: {
201+
promise: {
202+
probe: async (host: string) => {
203+
if (host === "192.168.0.191") {
204+
return {
205+
alive: true,
206+
time: 50 // First host is slowest
207+
};
208+
} else if (host === "192.168.0.192") {
209+
return {
210+
alive: true,
211+
time: 15 // Second host is fastest
212+
};
213+
} else if (host === "192.168.0.193") {
214+
return {
215+
alive: true,
216+
time: 30 // Third host is medium
217+
};
218+
}
219+
return { alive: true, time: 100 };
220+
}
250221
}
251-
return { averageLatency: 100, errors: [], maximumLatency: 100, minimumLatency: 100, options };
252222
}
253223
}));
254224

@@ -274,35 +244,29 @@ describe("RADIUS Host Selector", () => {
274244

275245
test("should select third host when it has lowest latency", async () => {
276246
// Mock where the THIRD host has the lowest latency
277-
mock.module("@network-utils/tcp-ping", () => ({
278-
ping: async (options: PingOptions) => {
279-
const host = options.address;
280-
if (host === "192.168.0.191") {
281-
return {
282-
averageLatency: 45, // First host is medium
283-
errors: [],
284-
maximumLatency: 50,
285-
minimumLatency: 40,
286-
options
287-
};
288-
} else if (host === "192.168.0.192") {
289-
return {
290-
averageLatency: 60, // Second host is slowest
291-
errors: [],
292-
maximumLatency: 65,
293-
minimumLatency: 55,
294-
options
295-
};
296-
} else if (host === "192.168.0.193") {
297-
return {
298-
averageLatency: 12, // Third host is fastest
299-
errors: [],
300-
maximumLatency: 15,
301-
minimumLatency: 10,
302-
options
303-
};
247+
mock.module('ping', () => ({
248+
default: {
249+
promise: {
250+
probe: async (host: string) => {
251+
if (host === "192.168.0.191") {
252+
return {
253+
alive: true,
254+
time: 45 // First host is medium
255+
};
256+
} else if (host === "192.168.0.192") {
257+
return {
258+
alive: true,
259+
time: 60 // Second host is slowest
260+
};
261+
} else if (host === "192.168.0.193") {
262+
return {
263+
alive: true,
264+
time: 12 // Third host is fastest
265+
};
266+
}
267+
return { alive: true, time: 100 };
268+
}
304269
}
305-
return { averageLatency: 100, errors: [], maximumLatency: 100, minimumLatency: 100, options };
306270
}
307271
}));
308272

0 commit comments

Comments
 (0)