fix(proxy): retry raced dynamic port binds

This commit is contained in:
Wooseong Kim committed 2026-04-20 14:33:44 +09:00
1 parent 2a80f7b1c1
commit 7cf555356a
2 files changed
+61 -29

No files matched your search

+1 -1
View File
@@ -199,7 +199,7 @@ export async function handleProxyCommand(args: string[]): Promise<number> {
case 'start': case 'start':
return handleStart(args.slice(1)); return handleStart(args.slice(1));
case 'stop': { case 'stop': {
const profileName = args[1] && !args[1]?.startsWith('-') ? args[1] : undefined; const profileName = args[1] && !args[1].startsWith('-') ? args[1] : undefined;
const result = await stopOpenAICompatProxy(profileName); const result = await stopOpenAICompatProxy(profileName);
if (!result.success) { if (!result.success) {
console.error(fail(result.error || 'Failed to stop proxy')); console.error(fail(result.error || 'Failed to stop proxy'));
+60 -28
View File
@@ -93,12 +93,17 @@ async function isPortOccupied(port: number): Promise<boolean> {
}); });
} }
async function findOpenAICompatProxyPort(): Promise<number> { async function findOpenAICompatProxyPort(
excludedPorts: ReadonlySet<number> = new Set()
): Promise<number> {
for ( for (
let candidate = OPENAI_COMPAT_PROXY_DEFAULT_PORT; let candidate = OPENAI_COMPAT_PROXY_DEFAULT_PORT;
candidate <= OPENAI_COMPAT_PROXY_DEFAULT_PORT + 10; candidate <= OPENAI_COMPAT_PROXY_DEFAULT_PORT + 10;
candidate += 1 candidate += 1
) { ) {
if (excludedPorts.has(candidate)) {
continue;
}
if (!(await isPortOccupied(candidate))) { if (!(await isPortOccupied(candidate))) {
return candidate; return candidate;
} }
@@ -107,11 +112,14 @@ async function findOpenAICompatProxyPort(): Promise<number> {
return 0; return 0;
} }
async function findOpenAICompatProxyPortNear(preferredPort: number): Promise<number> { async function findOpenAICompatProxyPortNear(
if (!(await isPortOccupied(preferredPort))) { preferredPort: number,
excludedPorts: ReadonlySet<number> = new Set()
): Promise<number> {
if (!excludedPorts.has(preferredPort) && !(await isPortOccupied(preferredPort))) {
return preferredPort; return preferredPort;
} }
return findOpenAICompatProxyPort(); return findOpenAICompatProxyPort(excludedPorts);
} }
interface OpenAICompatProxyStateRecord { interface OpenAICompatProxyStateRecord {
@@ -350,36 +358,23 @@ export async function startOpenAICompatProxy(
? portPreference.port ? portPreference.port
: status.port || portPreference.port); : status.port || portPreference.port);
const requiresExactPort = explicitPort !== undefined || portPreference.source === 'profile'; const requiresExactPort = explicitPort !== undefined || portPreference.source === 'profile';
const port = if (status.running && status.port === preferredPort && (status.host || '127.0.0.1') === host) {
status.running && status.port && (status.host || '127.0.0.1') === host
? status.port
: requiresExactPort
? preferredPort
: await findOpenAICompatProxyPortNear(preferredPort);
if (port === 0) {
return {
success: false,
port: preferredPort,
error: `No free proxy port found in range ${OPENAI_COMPAT_PROXY_DEFAULT_PORT}-${OPENAI_COMPAT_PROXY_DEFAULT_PORT + 10}`,
};
}
if (!Number.isInteger(port) || port < 1 || port > 65535) {
return { success: false, port, error: `Invalid port: ${port}` };
}
if (status.running && status.port === port && (status.host || '127.0.0.1') === host) {
return { return {
success: true, success: true,
alreadyRunning: true, alreadyRunning: true,
pid: status.pid, pid: status.pid,
port, port: preferredPort,
authToken: status.authToken, authToken: status.authToken,
}; };
} }
if (requiresExactPort && (await isPortOccupied(port))) { if (!Number.isInteger(preferredPort) || preferredPort < 1 || preferredPort > 65535) {
return { success: false, port: preferredPort, error: `Invalid port: ${preferredPort}` };
}
if (requiresExactPort && (await isPortOccupied(preferredPort))) {
return { return {
success: false, success: false,
port, port: preferredPort,
error: `Requested proxy port ${port} is already in use`, error: `Requested proxy port ${preferredPort} is already in use`,
}; };
} }
if (status.running) { if (status.running) {
@@ -387,7 +382,7 @@ export async function startOpenAICompatProxy(
if (!stopped.success) { if (!stopped.success) {
return { return {
success: false, success: false,
port, port: preferredPort,
error: stopped.error || 'Failed to restart the running proxy', error: stopped.error || 'Failed to restart the running proxy',
}; };
} }
@@ -397,12 +392,13 @@ export async function startOpenAICompatProxy(
if (!daemonEntry) { if (!daemonEntry) {
return { return {
success: false, success: false,
port, port: preferredPort,
error: 'OpenAI proxy daemon entrypoint not found. Run `bun run build` and retry.', error: 'OpenAI proxy daemon entrypoint not found. Run `bun run build` and retry.',
}; };
} }
return new Promise((resolve) => { const startOnPort = (port: number): Promise<StartOpenAICompatProxyResult> =>
new Promise((resolve) => {
let resolved = false; let resolved = false;
let timeout: NodeJS.Timeout | null = null; let timeout: NodeJS.Timeout | null = null;
const authToken = generateProxyAuthToken(); const authToken = generateProxyAuthToken();
@@ -499,5 +495,41 @@ export async function startOpenAICompatProxy(
}); });
}); });
}); });
if (requiresExactPort) {
return startOnPort(preferredPort);
}
const attemptedPorts = new Set<number>();
let lastResult: StartOpenAICompatProxyResult | null = null;
for (let attempt = 0; attempt < 3; attempt += 1) {
const port = await findOpenAICompatProxyPortNear(preferredPort, attemptedPorts);
if (port === 0) {
return {
success: false,
port: preferredPort,
error: `No free proxy port found in range ${OPENAI_COMPAT_PROXY_DEFAULT_PORT}-${OPENAI_COMPAT_PROXY_DEFAULT_PORT + 10}`,
};
}
const result = await startOnPort(port);
if (result.success) {
return result;
}
lastResult = result;
attemptedPorts.add(port);
if (!(await isPortOccupied(port))) {
return result;
}
}
return (
lastResult ?? {
success: false,
port: preferredPort,
error: 'Failed to start proxy',
}
);
}); });
} }