This commit is contained in:
Raven Scott
2026-05-28 18:19:32 -04:00
parent d046f6a8e2
commit 2fe3d9fba5
5 changed files with 161 additions and 78 deletions
+53 -3
View File
@@ -56,16 +56,31 @@ const pluginRegistry = new Map();
// Serialize HyperDB insert/delete/flush per plugin (HyperDB refuses flush while mutating)
const dbWriteQueues = new Map();
const dbWriteBatchDepth = new Map();
/**
* Run a write operation after prior writes for the same plugin complete.
* Nested calls (inside db.runWrite) run inline without re-queueing.
* @param {string} pluginDomain
* @param {Function} fn
* @returns {Promise<*>}
*/
function enqueueDbWrite(pluginDomain, fn) {
const depth = dbWriteBatchDepth.get(pluginDomain) || 0;
if (depth > 0) {
return fn();
}
const prev = dbWriteQueues.get(pluginDomain) || Promise.resolve();
const next = prev.then(() => fn(), () => fn());
const run = async () => {
dbWriteBatchDepth.set(pluginDomain, depth + 1);
try {
return await fn();
} finally {
dbWriteBatchDepth.set(pluginDomain, depth);
}
};
const next = prev.then(run, run);
dbWriteQueues.set(pluginDomain, next.catch(() => {}));
return next;
}
@@ -3955,6 +3970,19 @@ const sdk = {
* @param {Object} doc - Document to insert
* @returns {Promise<void>}
*/
/**
* Run multiple inserts/deletes/flushes as one serialized write batch.
* @param {Function} fn - Async function performing DB writes
* @returns {Promise<*>}
*/
async runWrite(fn) {
const pluginDomain = _getPluginDomain();
if (!pluginDomain) {
throw new Error('Plugin domain not available');
}
return enqueueDbWrite(pluginDomain, () => fn());
},
async insert(collection, doc) {
const pluginDomain = _getPluginDomain();
if (!pluginDomain) {
@@ -3994,11 +4022,24 @@ const sdk = {
throw new Error('Database is closed');
}
logDebug('PluginSDK', `[db.insert] Calling db.insert()...`);
await db.insert(collection, doc);
const INSERT_TIMEOUT = 120000;
const insertPromise = db.insert(collection, doc);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error(`db.insert() timeout after ${INSERT_TIMEOUT}ms`)), INSERT_TIMEOUT);
});
await Promise.race([insertPromise, timeoutPromise]);
logDebug('PluginSDK', `[db.insert] Insert completed in ${Date.now() - startTime}ms`);
return;
} catch (err) {
logError('PluginSDK', `[db.insert] Attempt ${attempt} failed: ${err.message}`);
const isWriteContention = err.message && (
err.message.includes('Insert/delete in progress') ||
err.message.includes('refusing to commit')
);
if (isWriteContention && attempt < MAX_ATTEMPTS) {
await new Promise((resolve) => setTimeout(resolve, 50));
continue;
}
const recovered = await this._recoverFromDbError(err, db, attempt, MAX_ATTEMPTS);
if (recovered) {
db = recovered;
@@ -4175,7 +4216,7 @@ const sdk = {
logDebug('PluginSDK', `[db.flush] Got database instance, closed=${db.closed}`);
// Flush with automatic database recreation on SESSION_CLOSED errors
const MAX_ATTEMPTS = 2;
const MAX_ATTEMPTS = 15;
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
try {
logDebug('PluginSDK', `[db.flush] Attempt ${attempt}: Calling db.ready()...`);
@@ -4199,6 +4240,15 @@ const sdk = {
return;
} catch (err) {
logError('PluginSDK', `[db.flush] Attempt ${attempt} failed: ${err.message}`);
const isWriteContention = err.message && (
err.message.includes('Insert/delete in progress') ||
err.message.includes('refusing to commit') ||
err.message.includes('Database has changed')
);
if (isWriteContention && attempt < MAX_ATTEMPTS) {
await new Promise((resolve) => setTimeout(resolve, 50));
continue;
}
const isSessionClosed = err.message && (
err.message.includes('SESSION_CLOSED') ||
err.message.includes('Database not available') ||