fix: prevent EADDRINUSE double-bind on Holesail client startup
Add global activeClientKeys Set to track all active clients and prevent race conditions between domain init and cache restore paths. Add EADDRINUSE error handling in holesail.js, holesail_child.js
This commit is contained in:
@@ -18,6 +18,20 @@ async function startHolesailClient(domain, hash, ip, port, persistent = false) {
|
||||
logDebug('Holesail', `Holesail client already exists for ${key}`);
|
||||
return;
|
||||
}
|
||||
// Check global activeClientKeys to prevent double-bind (both forked and direct paths)
|
||||
if (state.activeClientKeys && state.activeClientKeys.has(key)) {
|
||||
logDebug('Holesail', `Holesail client already starting/active for ${key} (global check), waiting...`);
|
||||
// Wait for the existing promise to complete
|
||||
if (state.starting.has(key)) {
|
||||
await state.starting.get(key);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Mark as active before starting
|
||||
if (state.activeClientKeys) {
|
||||
state.activeClientKeys.add(key);
|
||||
}
|
||||
let startPromise = state.starting.get(key);
|
||||
if (!startPromise) {
|
||||
startPromise = (async () => {
|
||||
@@ -86,10 +100,24 @@ async function startHolesailClient(domain, hash, ip, port, persistent = false) {
|
||||
state.holesailClientTimeouts.set(key, timeout);
|
||||
}
|
||||
} catch (err) {
|
||||
logError('Holesail', `Error connecting Holesail client for ${key}: ${err.message}`);
|
||||
if (err.code === 'EADDRINUSE' || err.message.includes('EADDRINUSE')) {
|
||||
logWarn('Holesail', `Port ${port} on ${ip} already in use (EADDRINUSE) for ${key}, skipping duplicate bind`);
|
||||
if (state.activeClientKeys) {
|
||||
state.activeClientKeys.delete(key);
|
||||
}
|
||||
} else {
|
||||
logError('Holesail', `Error connecting Holesail client for ${key}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logError('Holesail', `Failed to start Holesail client for ${key}: ${err.message}`);
|
||||
if (err.code === 'EADDRINUSE' || err.message.includes('EADDRINUSE')) {
|
||||
logWarn('Holesail', `Port ${port} on ${ip} already in use (EADDRINUSE) for ${key}, skipping duplicate bind`);
|
||||
if (state.activeClientKeys) {
|
||||
state.activeClientKeys.delete(key);
|
||||
}
|
||||
} else {
|
||||
logError('Holesail', `Failed to start Holesail client for ${key}: ${err.message}`);
|
||||
}
|
||||
} finally {
|
||||
state.starting.delete(key);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,17 @@ async function loadHolesailClients() {
|
||||
const promises = (data.clients || []).map(async (s) => {
|
||||
const id = s.id;
|
||||
const opts = s.opts;
|
||||
// Check if already exists in state.holesails (from domain init path)
|
||||
const runtimeKey = `${opts.domain}:${opts.port}`;
|
||||
if (state.holesails.has(runtimeKey)) {
|
||||
logDebug('Holesail', `Skipping restore for ${id} - client already exists for ${runtimeKey}`);
|
||||
return;
|
||||
}
|
||||
// Check global activeClientKeys to prevent double-bind
|
||||
if (state.activeClientKeys && state.activeClientKeys.has(runtimeKey)) {
|
||||
logDebug('Holesail', `Skipping restore for ${id} - client already starting for ${runtimeKey}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!state.domainToIPMap.has(opts.domain)) {
|
||||
// Lazy load to avoid circular dependency
|
||||
@@ -35,10 +46,18 @@ async function loadHolesailClients() {
|
||||
if (!portFree) {
|
||||
throw new Error(`Unable to ensure port ${opts.port} free on ${ip}`);
|
||||
}
|
||||
// Mark as active before spawning to prevent race with other paths
|
||||
if (state.activeClientKeys) {
|
||||
state.activeClientKeys.add(runtimeKey);
|
||||
}
|
||||
await startForkedHolesailClient(id, opts);
|
||||
logInfo('Holesail', `Restored client ${id} for domain ${opts.domain} on port ${opts.port}`);
|
||||
} catch (err) {
|
||||
logError('Holesail', `Failed to restore client ${id} for ${opts.domain}:${opts.port}: ${err.message}`);
|
||||
// Clean up activeClientKeys on failure
|
||||
if (state.activeClientKeys) {
|
||||
state.activeClientKeys.delete(runtimeKey);
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
@@ -211,6 +230,11 @@ async function startForkedHolesailClient(id, opts) {
|
||||
});
|
||||
child.on('exit', (code) => {
|
||||
logInfo('Holesail', `Child exited for client ${id} with code ${code}`);
|
||||
// Clean up activeClientKeys on child exit
|
||||
const runtimeKey = `${opts.domain}:${opts.port}`;
|
||||
if (state.activeClientKeys) {
|
||||
state.activeClientKeys.delete(runtimeKey);
|
||||
}
|
||||
state.holesailClientChildren.delete(id);
|
||||
state.holesailClientInfos.delete(id);
|
||||
state.holesailChildStartTimes.delete(id);
|
||||
|
||||
Reference in New Issue
Block a user