Skip to content

Commit 2623551

Browse files
committed
Speed up isLocalPortActive with caching & server reuse
This only covers localhost, but that's quite a common case for testing scenarios. In practice this speeds up some proxying benchmarks about 30%.
1 parent 1589ca9 commit 2623551

File tree

1 file changed

+54
-15
lines changed

1 file changed

+54
-15
lines changed

src/util/socket-util.ts

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,64 @@ import {
2222
import { getSocketMetadataTags } from './socket-metadata';
2323
import { normalizeIP } from './ip-utils';
2424

25-
// Test if a local port for a given interface (IPv4/6) is currently in use
25+
// Test if a local port for a given interface (IPv4/6) is currently in use, by attempting
26+
// to bind to it. We reuse a single server instance and cache results to avoid creating
27+
// a new server on every request. Concurrent checks for the same port share one probe.
28+
const probeServer = net.createServer();
29+
const portActiveCache = new Map<string, { result: boolean, expires: number }>();
30+
const portActiveInFlight = new Map<string, Promise<boolean>>();
31+
const PORT_ACTIVE_CACHE_TTL = 5000; // 5 seconds
32+
2633
export async function isLocalPortActive(interfaceIp: '::1' | '127.0.0.1', port: number) {
2734
if (interfaceIp === '::1' && !isLocalIPv6Available) return false;
2835

29-
return new Promise((resolve) => {
30-
const server = net.createServer();
31-
server.listen({
32-
host: interfaceIp,
33-
port,
34-
ipv6Only: interfaceIp === '::1'
35-
});
36-
server.once('listening', () => {
37-
resolve(false);
38-
server.close(() => {});
39-
});
40-
server.once('error', (e) => {
41-
resolve(true);
36+
const cacheKey = `${interfaceIp}:${port}`;
37+
38+
const cached = portActiveCache.get(cacheKey);
39+
if (cached && cached.expires > Date.now()) {
40+
return cached.result;
41+
}
42+
43+
// Deduplicate concurrent checks for the same address:port
44+
let probe = portActiveInFlight.get(cacheKey);
45+
if (!probe) {
46+
probe = probePort(interfaceIp, port);
47+
portActiveInFlight.set(cacheKey, probe);
48+
probe.then((result) => {
49+
portActiveCache.set(cacheKey, { result, expires: Date.now() + PORT_ACTIVE_CACHE_TTL });
50+
portActiveInFlight.delete(cacheKey);
4251
});
43-
});
52+
}
53+
54+
return probe;
55+
}
56+
57+
// Serialized via the in-flight map above — only one probe runs at a time per key,
58+
// and different keys won't collide because they share the single probeServer sequentially
59+
// via the listen/close cycle.
60+
let probeServerReady = Promise.resolve();
61+
62+
function probePort(interfaceIp: string, port: number): Promise<boolean> {
63+
const result = probeServerReady.then(() =>
64+
new Promise<boolean>((resolve) => {
65+
probeServer.listen({
66+
host: interfaceIp,
67+
port,
68+
ipv6Only: interfaceIp === '::1'
69+
});
70+
probeServer.once('listening', () => {
71+
probeServer.close(() => resolve(false));
72+
});
73+
probeServer.once('error', () => {
74+
resolve(true);
75+
});
76+
})
77+
);
78+
79+
// Chain so the next probe waits for this one to fully complete
80+
probeServerReady = result.then(() => {});
81+
82+
return result;
4483
}
4584

4685
// This file imported in browsers etc as it's used in handlers, but none of these methods are used

0 commit comments

Comments
 (0)