-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathhost-service-utils.ts
More file actions
97 lines (88 loc) · 2.97 KB
/
Copy pathhost-service-utils.ts
File metadata and controls
97 lines (88 loc) · 2.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import { createServer } from "node:net";
export {
MAX_HOST_LOG_BYTES,
openRotatingLogFd,
} from "@superset/shared/rotating-log";
// Before the server becomes reachable, startup must still clear DB migrate and
// the daemon bootstrap (the shell-env snapshot now runs in the background, off
// the critical path). At boot every known org starts at once, and multiple app
// instances sharing one $SUPERSET_HOME_DIR compound the contention, so a
// healthy-but-slow child can need well over 10s. Give it generous headroom; a
// genuinely dead child is detected early via the poll's abort hook rather than
// by this deadline.
export const HEALTH_POLL_TIMEOUT_MS = 30_000;
const HEALTH_POLL_INTERVAL_MS = 200;
export async function findFreePort(
preferredPorts: Iterable<number> = [],
): Promise<number> {
const triedPorts = new Set<number>();
for (const port of preferredPorts) {
const normalizedPort = normalizePort(port);
if (!normalizedPort || triedPorts.has(normalizedPort)) continue;
triedPorts.add(normalizedPort);
if (await canBindPort(normalizedPort)) return normalizedPort;
}
return new Promise((resolve, reject) => {
const server = createServer();
server.listen(0, "127.0.0.1", () => {
const addr = server.address();
if (addr && typeof addr === "object") {
const { port } = addr;
server.close(() => resolve(port));
} else {
server.close(() => reject(new Error("Could not get port")));
}
});
server.on("error", reject);
});
}
function normalizePort(port: number): number | null {
if (!Number.isInteger(port) || port <= 0 || port > 65_535) return null;
return port;
}
export function canBindPort(port: number): Promise<boolean> {
return new Promise((resolve) => {
const server = createServer();
const finish = (available: boolean) => {
server.removeAllListeners("error");
server.removeAllListeners("listening");
if (!available) {
resolve(false);
return;
}
server.close(() => resolve(true));
};
server.once("error", () => finish(false));
server.once("listening", () => finish(true));
server.listen(port, "127.0.0.1");
});
}
export async function pollHealthCheck(
endpoint: string,
secret: string,
timeoutMs = HEALTH_POLL_TIMEOUT_MS,
// Bail out before the deadline once the child is known dead — otherwise a
// crash-on-startup would stall the caller for the full (now generous)
// timeout instead of failing fast.
shouldAbort?: () => boolean,
): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (shouldAbort?.()) return false;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 2_000);
try {
const res = await fetch(`${endpoint}/trpc/health.check`, {
signal: controller.signal,
headers: { Authorization: `Bearer ${secret}` },
});
if (res.ok) return true;
} catch {
// Not ready yet
} finally {
clearTimeout(timeout);
}
await new Promise((r) => setTimeout(r, HEALTH_POLL_INTERVAL_MS));
}
return false;
}