Updates
This commit is contained in:
+4
-2
@@ -111,7 +111,9 @@ TOPIC_SEED=p2ns-dns
|
|||||||
MASTER_RECONNECT_INTERVAL=5
|
MASTER_RECONNECT_INTERVAL=5
|
||||||
# Maximum number of reconnection attempts per peer before giving up (default: 10)
|
# Maximum number of reconnection attempts per peer before giving up (default: 10)
|
||||||
MASTER_MAX_RECONNECT_ATTEMPTS=10
|
MASTER_MAX_RECONNECT_ATTEMPTS=10
|
||||||
# Delay in milliseconds before sending proactive invite to new peers (default: 500)
|
# Delay in milliseconds before sending proactive invite to new peers (default: 4000)
|
||||||
# Master nodes automatically send invites to peers when they connect
|
# Master nodes automatically send invites to peers when they connect
|
||||||
MASTER_PROACTIVE_INVITE_DELAY=500
|
MASTER_PROACTIVE_INVITE_DELAY=4000
|
||||||
|
# Gap between serialized Autopass ops in ms (default: 75) — reduces atomic flush races
|
||||||
|
# DNS_PASS_OP_GAP_MS=75
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ function isDnsPassUsable(pass) {
|
|||||||
if (pass.opened === false) return false;
|
if (pass.opened === false) return false;
|
||||||
const base = pass.base;
|
const base = pass.base;
|
||||||
if (!base) return false;
|
if (!base) return false;
|
||||||
if (base.closed || base.closing) return false;
|
if (base.closed) return false;
|
||||||
|
// base.closing is transient during replication; only treat as unusable when fully closed
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,86 @@
|
|||||||
/**
|
/**
|
||||||
* Serializes all Autopass / Autobase operations on the master corestore.
|
* Serializes all Autopass / Autobase operations on the master corestore.
|
||||||
* Concurrent list(), createInvite(), add(), and remove() cause
|
* Concurrent list(), createInvite(), add(), and remove() — plus HyperDB autoUpdate
|
||||||
* "Atomic state must flush to parent" on Hypercore 11.
|
* during replication — cause "Atomic state must flush to parent" on Hypercore 11.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const state = require('../infrastructure/state');
|
||||||
|
const { logDebug, logWarn } = require('../infrastructure/logger');
|
||||||
|
|
||||||
let chain = Promise.resolve();
|
let chain = Promise.resolve();
|
||||||
|
|
||||||
|
const ATOMIC_ERR_RE = /Atomic state must flush|SESSION_CLOSED|closing core/i;
|
||||||
|
const OP_GAP_MS = parseInt(process.env.DNS_PASS_OP_GAP_MS || '75', 10);
|
||||||
|
const SYNC_RETRIES = parseInt(process.env.DNS_PASS_SYNC_RETRIES || '4', 10);
|
||||||
|
|
||||||
|
function isAtomicDnsPassError(err) {
|
||||||
|
const msg = err && err.message ? err.message : String(err);
|
||||||
|
return ATOMIC_ERR_RE.test(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bring Autobase + HyperDB view in sync before a read/write (reduces atomic flush races).
|
||||||
|
* @param {object} pass - Autopass instance
|
||||||
|
* @returns {Promise<boolean>} false if sync could not complete (busy)
|
||||||
|
*/
|
||||||
|
async function syncDnsPassView(pass) {
|
||||||
|
if (!pass || pass.closed) return false;
|
||||||
|
try {
|
||||||
|
await pass.ready();
|
||||||
|
const base = pass.base;
|
||||||
|
if (!base || base.closed || base.closing) return false;
|
||||||
|
await base.update();
|
||||||
|
const view = base.view;
|
||||||
|
if (view && !view.closed && typeof view.update === 'function') {
|
||||||
|
view.update();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
if (isAtomicDnsPassError(err)) {
|
||||||
|
logDebug('DnsPassQueue', `View sync deferred (core busy): ${err.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runWithDnsPassRetry(pass, operation) {
|
||||||
|
let lastErr;
|
||||||
|
for (let attempt = 0; attempt < SYNC_RETRIES; attempt++) {
|
||||||
|
if (attempt > 0) {
|
||||||
|
await sleep(100 * attempt);
|
||||||
|
}
|
||||||
|
await syncDnsPassView(pass);
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (err) {
|
||||||
|
lastErr = err;
|
||||||
|
if (!isAtomicDnsPassError(err) || attempt === SYNC_RETRIES - 1) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
logWarn('DnsPassQueue', `dnsPass op retry ${attempt + 1}/${SYNC_RETRIES}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastErr;
|
||||||
|
}
|
||||||
|
|
||||||
function enqueueDnsPass(operation) {
|
function enqueueDnsPass(operation) {
|
||||||
const run = chain.then(() => Promise.resolve().then(operation));
|
const run = chain.then(async () => {
|
||||||
|
state.dnsPassWriteInProgress = (state.dnsPassWriteInProgress || 0) + 1;
|
||||||
|
try {
|
||||||
|
const result = await Promise.resolve().then(operation);
|
||||||
|
if (OP_GAP_MS > 0) {
|
||||||
|
await sleep(OP_GAP_MS);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
state.dnsPassWriteInProgress = Math.max(0, (state.dnsPassWriteInProgress || 1) - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
chain = run.then(
|
chain = run.then(
|
||||||
() => {},
|
() => {},
|
||||||
() => {}
|
() => {}
|
||||||
@@ -21,7 +94,9 @@ function whenDnsPassIdle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createInvite(pass, opts) {
|
async function createInvite(pass, opts) {
|
||||||
return enqueueDnsPass(() => pass.createInvite(opts));
|
return enqueueDnsPass(() =>
|
||||||
|
runWithDnsPassRetry(pass, () => pass.createInvite(opts))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,15 +116,21 @@ function invitePreview(inv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function dnsPassAdd(pass, key, value, file) {
|
async function dnsPassAdd(pass, key, value, file) {
|
||||||
return enqueueDnsPass(() => pass.add(key, value, file));
|
return enqueueDnsPass(() =>
|
||||||
|
runWithDnsPassRetry(pass, () => pass.add(key, value, file))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dnsPassRemove(pass, key) {
|
async function dnsPassRemove(pass, key) {
|
||||||
return enqueueDnsPass(() => pass.remove(key));
|
return enqueueDnsPass(() =>
|
||||||
|
runWithDnsPassRetry(pass, () => pass.remove(key))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dnsPassGet(pass, key) {
|
async function dnsPassGet(pass, key) {
|
||||||
return enqueueDnsPass(() => pass.get(key));
|
return enqueueDnsPass(() =>
|
||||||
|
runWithDnsPassRetry(pass, () => pass.get(key))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,7 +139,8 @@ async function dnsPassGet(pass, key) {
|
|||||||
* @returns {Promise<Array<{key: string, value: string}>>}
|
* @returns {Promise<Array<{key: string, value: string}>>}
|
||||||
*/
|
*/
|
||||||
async function listAllEntries(pass) {
|
async function listAllEntries(pass) {
|
||||||
return enqueueDnsPass(async () => {
|
return enqueueDnsPass(async () =>
|
||||||
|
runWithDnsPassRetry(pass, async () => {
|
||||||
const entries = [];
|
const entries = [];
|
||||||
const stream = pass.list();
|
const stream = pass.list();
|
||||||
for await (const entry of stream) {
|
for await (const entry of stream) {
|
||||||
@@ -68,12 +150,15 @@ async function listAllEntries(pass) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
enqueueDnsPass,
|
enqueueDnsPass,
|
||||||
whenDnsPassIdle,
|
whenDnsPassIdle,
|
||||||
|
syncDnsPassView,
|
||||||
|
isAtomicDnsPassError,
|
||||||
createInvite,
|
createInvite,
|
||||||
inviteToWire,
|
inviteToWire,
|
||||||
invitePreview,
|
invitePreview,
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ const currentSubnetIndex = 0; // Start with first subnet for round-robin
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
dnsPass: null,
|
dnsPass: null,
|
||||||
|
/** Incremented while dns-pass-queue runs a serialized Autopass op (defer update handlers) */
|
||||||
|
dnsPassWriteInProgress: 0,
|
||||||
holesails: new Map(),
|
holesails: new Map(),
|
||||||
starting: new Map(),
|
starting: new Map(),
|
||||||
tlsServers: new Map(),
|
tlsServers: new Map(),
|
||||||
|
|||||||
@@ -410,9 +410,17 @@ async function main() {
|
|||||||
// Function to process pending invite requests when master becomes ready
|
// Function to process pending invite requests when master becomes ready
|
||||||
async function processPendingInviteRequests() {
|
async function processPendingInviteRequests() {
|
||||||
const { isDnsPassUsable } = require('./includes/core/core');
|
const { isDnsPassUsable } = require('./includes/core/core');
|
||||||
if (!isMaster || !isDnsPassUsable(state.dnsPass)) {
|
const { whenDnsPassIdle, syncDnsPassView } = require('./includes/core/dns-pass-queue');
|
||||||
|
if (!isMaster || !state.dnsPass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isDnsPassUsable(state.dnsPass)) {
|
||||||
|
await syncDnsPassView(state.dnsPass);
|
||||||
|
}
|
||||||
|
if (!isDnsPassUsable(state.dnsPass)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await whenDnsPassIdle();
|
||||||
|
|
||||||
const pendingRequests = Array.from(state.pendingInviteRequests.entries());
|
const pendingRequests = Array.from(state.pendingInviteRequests.entries());
|
||||||
state.pendingInviteRequests.clear(); // Clear queue immediately to avoid duplicate processing
|
state.pendingInviteRequests.clear(); // Clear queue immediately to avoid duplicate processing
|
||||||
@@ -838,11 +846,16 @@ async function main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { isDnsPassUsable } = require('./includes/core/core');
|
const { isDnsPassUsable } = require('./includes/core/core');
|
||||||
|
const { whenDnsPassIdle, syncDnsPassView } = require('./includes/core/dns-pass-queue');
|
||||||
|
if (!isDnsPassUsable(pass)) {
|
||||||
|
await syncDnsPassView(pass);
|
||||||
|
}
|
||||||
if (!isDnsPassUsable(pass)) {
|
if (!isDnsPassUsable(pass)) {
|
||||||
logWarn('Swarm', `dnsPass not usable, cannot create invite for ${peerId}`);
|
logWarn('Swarm', `dnsPass not usable, cannot create invite for ${peerId}`);
|
||||||
channelManager.sendToPeer(CORE_DOMAIN, 'request', peerId, 'invite_unavailable:creation_failed');
|
channelManager.sendToPeer(CORE_DOMAIN, 'request', peerId, 'invite_unavailable:creation_failed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await whenDnsPassIdle();
|
||||||
logDebug('Swarm', `Creating invite for peer ${peerId}...`);
|
logDebug('Swarm', `Creating invite for peer ${peerId}...`);
|
||||||
const { createInvite, inviteToWire, invitePreview } = require('./includes/core/dns-pass-queue');
|
const { createInvite, inviteToWire, invitePreview } = require('./includes/core/dns-pass-queue');
|
||||||
const inviteChannelInfo = channelManager.getChannelInfo(CORE_DOMAIN, 'invite');
|
const inviteChannelInfo = channelManager.getChannelInfo(CORE_DOMAIN, 'invite');
|
||||||
@@ -873,15 +886,29 @@ async function main() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err.message || '';
|
const msg = err.message || '';
|
||||||
if (msg.includes('Atomic state must flush') || msg.includes('SESSION_CLOSED')) {
|
if (msg.includes('Atomic state must flush') || msg.includes('SESSION_CLOSED')) {
|
||||||
logWarn('Swarm', `dnsPass busy, invite creation failed for ${peerId}: ${msg}`);
|
logWarn('Swarm', `dnsPass busy, scheduling invite retry for ${peerId}: ${msg}`);
|
||||||
|
setTimeout(async () => {
|
||||||
|
const retryPass = getDnsPass();
|
||||||
|
const { isDnsPassUsable } = require('./includes/core/core');
|
||||||
|
if (!connectedPeers.has(peerId) || !retryPass || !isDnsPassUsable(retryPass)) return;
|
||||||
|
try {
|
||||||
|
const { createInvite, inviteToWire, whenDnsPassIdle } = require('./includes/core/dns-pass-queue');
|
||||||
|
await whenDnsPassIdle();
|
||||||
|
const inv = await createInvite(retryPass);
|
||||||
|
channelManager.sendToPeer(CORE_DOMAIN, 'invite', peerId, inviteToWire(inv));
|
||||||
|
logInfo('Swarm', `Sent invite to ${peerId} after dnsPass busy retry`);
|
||||||
|
} catch (retryErr) {
|
||||||
|
logWarn('Swarm', `Invite retry failed for ${peerId}: ${retryErr.message}`);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
} else {
|
} else {
|
||||||
logError('Swarm', `Error creating invite for request from ${peerId}: ${msg}`);
|
logError('Swarm', `Error creating invite for request from ${peerId}: ${msg}`);
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
channelManager.sendToPeer(CORE_DOMAIN, 'request', peerId, 'invite_unavailable:creation_failed');
|
channelManager.sendToPeer(CORE_DOMAIN, 'request', peerId, 'invite_unavailable:creation_failed');
|
||||||
} catch (sendErr) {
|
} catch (sendErr) {
|
||||||
// Channel might be closed, ignore
|
// Channel might be closed, ignore
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
pendingInviteRequestHandlers.delete(peerId);
|
pendingInviteRequestHandlers.delete(peerId);
|
||||||
}
|
}
|
||||||
@@ -904,7 +931,8 @@ async function main() {
|
|||||||
if (pass && isDnsPassUsable(pass) && (isMaster || process.env.ALLOW_ANY_WRITER_INVITES === 'true')) {
|
if (pass && isDnsPassUsable(pass) && (isMaster || process.env.ALLOW_ANY_WRITER_INVITES === 'true')) {
|
||||||
// We can create an invite - send it back through the relay chain
|
// We can create an invite - send it back through the relay chain
|
||||||
try {
|
try {
|
||||||
const { createInvite, inviteToWire } = require('./includes/core/dns-pass-queue');
|
const { createInvite, inviteToWire, whenDnsPassIdle } = require('./includes/core/dns-pass-queue');
|
||||||
|
await whenDnsPassIdle();
|
||||||
const inv = await createInvite(pass);
|
const inv = await createInvite(pass);
|
||||||
const invWire = inviteToWire(inv);
|
const invWire = inviteToWire(inv);
|
||||||
logInfo('Swarm', `Created relay invite for origin ${originPeerId.substring(0, 16)}...`);
|
logInfo('Swarm', `Created relay invite for origin ${originPeerId.substring(0, 16)}...`);
|
||||||
@@ -1239,11 +1267,16 @@ async function main() {
|
|||||||
retryCount++;
|
retryCount++;
|
||||||
try {
|
try {
|
||||||
const { isDnsPassUsable } = require('./includes/core/core');
|
const { isDnsPassUsable } = require('./includes/core/core');
|
||||||
|
const { whenDnsPassIdle, syncDnsPassView } = require('./includes/core/dns-pass-queue');
|
||||||
|
if (!isDnsPassUsable(pass)) {
|
||||||
|
await syncDnsPassView(pass);
|
||||||
|
}
|
||||||
if (!isDnsPassUsable(pass)) {
|
if (!isDnsPassUsable(pass)) {
|
||||||
logWarn('Swarm', `dnsPass not usable, skipping proactive invite for ${peerId}`);
|
logWarn('Swarm', `dnsPass not usable, skipping proactive invite for ${peerId}`);
|
||||||
pendingInviteAcks.delete(peerId);
|
pendingInviteAcks.delete(peerId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await whenDnsPassIdle();
|
||||||
const { createInvite, inviteToWire, invitePreview } = require('./includes/core/dns-pass-queue');
|
const { createInvite, inviteToWire, invitePreview } = require('./includes/core/dns-pass-queue');
|
||||||
const inv = await createInvite(pass);
|
const inv = await createInvite(pass);
|
||||||
const invWire = inviteToWire(inv);
|
const invWire = inviteToWire(inv);
|
||||||
@@ -1518,6 +1551,19 @@ async function main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.dnsPassWriteInProgress > 0) {
|
||||||
|
logDebug('Swarm', 'Deferring dnsPass update handler — Autopass write in progress');
|
||||||
|
if (listenerRefs.autoVoteDebounce) {
|
||||||
|
clearTimeout(listenerRefs.autoVoteDebounce);
|
||||||
|
}
|
||||||
|
listenerRefs.autoVoteDebounce = setTimeout(() => {
|
||||||
|
listenerRefs.dnsPassUpdate().catch((err) => {
|
||||||
|
logWarn('Swarm', `Deferred dnsPass update handler failed: ${err.message}`);
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logDebug('Swarm', 'Pass update event triggered - showing updated list');
|
logDebug('Swarm', 'Pass update event triggered - showing updated list');
|
||||||
invalidateEntriesCache(); // Invalidate cache on update
|
invalidateEntriesCache(); // Invalidate cache on update
|
||||||
@@ -1909,7 +1955,7 @@ async function main() {
|
|||||||
if (config.CONSENSUS_IMMEDIATE_UPDATE !== false && state.dnsPass) {
|
if (config.CONSENSUS_IMMEDIATE_UPDATE !== false && state.dnsPass) {
|
||||||
// Defer auto-votes on master so invite creation is not competing with pass.list()
|
// Defer auto-votes on master so invite creation is not competing with pass.list()
|
||||||
const autoVoteDelay = isMaster
|
const autoVoteDelay = isMaster
|
||||||
? parseInt(process.env.MASTER_AUTO_VOTE_DELAY || '3000', 10)
|
? parseInt(process.env.MASTER_AUTO_VOTE_DELAY || '5000', 10)
|
||||||
: 0;
|
: 0;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (state.isShuttingDown || !state.dnsPass) return;
|
if (state.isShuttingDown || !state.dnsPass) return;
|
||||||
@@ -2052,7 +2098,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send proactive invite after waiting for bidirectional channel
|
// Send proactive invite after waiting for bidirectional channel
|
||||||
const proactiveInviteDelay = parseInt(process.env.MASTER_PROACTIVE_INVITE_DELAY || '1500', 10);
|
const proactiveInviteDelay = parseInt(process.env.MASTER_PROACTIVE_INVITE_DELAY || '4000', 10);
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
// Double-check connection is still stable
|
// Double-check connection is still stable
|
||||||
if (!isConnectionStable(peerId, conn)) {
|
if (!isConnectionStable(peerId, conn)) {
|
||||||
@@ -3356,11 +3402,21 @@ async function main() {
|
|||||||
process.on('SIGQUIT', cleanup);
|
process.on('SIGQUIT', cleanup);
|
||||||
// Handle uncaught exceptions
|
// Handle uncaught exceptions
|
||||||
process.on('uncaughtException', (err) => {
|
process.on('uncaughtException', (err) => {
|
||||||
logError('Main', `Uncaught exception: ${err.message}`);
|
const msg = err && err.message ? err.message : String(err);
|
||||||
|
if (/Atomic state must flush|SESSION_CLOSED/i.test(msg)) {
|
||||||
|
logWarn('Main', `Autopass/Hypercore busy (non-fatal): ${msg}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logError('Main', `Uncaught exception: ${msg}`);
|
||||||
// Do not call cleanup() to prevent automatic shutdown
|
// Do not call cleanup() to prevent automatic shutdown
|
||||||
});
|
});
|
||||||
// Handle unhandled promise rejections
|
// Handle unhandled promise rejections
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
const msg = reason && reason.message ? reason.message : String(reason);
|
||||||
|
if (/Atomic state must flush|SESSION_CLOSED/i.test(msg)) {
|
||||||
|
logWarn('Main', `Autopass/Hypercore busy (unhandled rejection, non-fatal): ${msg}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
logError('Main', `Unhandled rejection at: ${promise} reason: ${reason}`);
|
logError('Main', `Unhandled rejection at: ${promise} reason: ${reason}`);
|
||||||
// Do not call cleanup() to prevent automatic shutdown
|
// Do not call cleanup() to prevent automatic shutdown
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user