This commit is contained in:
Raven Scott
2026-05-27 20:03:25 -04:00
parent dbc6997219
commit 3d35f148c5
5 changed files with 178 additions and 32 deletions
+2 -1
View File
@@ -63,7 +63,8 @@ function isDnsPassUsable(pass) {
if (pass.opened === false) return false;
const base = pass.base;
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;
}
+103 -18
View File
@@ -1,13 +1,86 @@
/**
* Serializes all Autopass / Autobase operations on the master corestore.
* Concurrent list(), createInvite(), add(), and remove() cause
* "Atomic state must flush to parent" on Hypercore 11.
* Concurrent list(), createInvite(), add(), and remove() — plus HyperDB autoUpdate
* 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();
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) {
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(
() => {},
() => {}
@@ -21,7 +94,9 @@ function whenDnsPassIdle() {
}
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) {
return enqueueDnsPass(() => pass.add(key, value, file));
return enqueueDnsPass(() =>
runWithDnsPassRetry(pass, () => pass.add(key, value, file))
);
}
async function dnsPassRemove(pass, key) {
return enqueueDnsPass(() => pass.remove(key));
return enqueueDnsPass(() =>
runWithDnsPassRetry(pass, () => pass.remove(key))
);
}
async function dnsPassGet(pass, key) {
return enqueueDnsPass(() => pass.get(key));
return enqueueDnsPass(() =>
runWithDnsPassRetry(pass, () => pass.get(key))
);
}
/**
@@ -58,22 +139,26 @@ async function dnsPassGet(pass, key) {
* @returns {Promise<Array<{key: string, value: string}>>}
*/
async function listAllEntries(pass) {
return enqueueDnsPass(async () => {
const entries = [];
const stream = pass.list();
for await (const entry of stream) {
entries.push({
key: entry.key.toString('utf8'),
value: entry.value.toString('utf8')
});
}
return entries;
});
return enqueueDnsPass(async () =>
runWithDnsPassRetry(pass, async () => {
const entries = [];
const stream = pass.list();
for await (const entry of stream) {
entries.push({
key: entry.key.toString('utf8'),
value: entry.value.toString('utf8')
});
}
return entries;
})
);
}
module.exports = {
enqueueDnsPass,
whenDnsPassIdle,
syncDnsPassView,
isAtomicDnsPassError,
createInvite,
inviteToWire,
invitePreview,
+2
View File
@@ -42,6 +42,8 @@ const currentSubnetIndex = 0; // Start with first subnet for round-robin
module.exports = {
dnsPass: null,
/** Incremented while dns-pass-queue runs a serialized Autopass op (defer update handlers) */
dnsPassWriteInProgress: 0,
holesails: new Map(),
starting: new Map(),
tlsServers: new Map(),