fix: eliminate upload queue stalls under automation load
Move atomic configuration writes off the Electron main event loop and preserve concurrent queue and folder monitor state. Make finish-and-pause interrupt queued admission waits without aborting active uploads, then resume only persistently marked automation jobs across active, idle, restart, and finalization races. Reuse bounded automation evidence snapshots, refresh them during long drains, and cover high-load persistence, pause, resume, and hidden Electron behavior with regression tests.
This commit is contained in:
+43
-32
@@ -589,6 +589,26 @@ class ConfigStore {
|
||||
}, options);
|
||||
}
|
||||
|
||||
saveFolderMonitorRuntimeState(folderMonitor, options = {}) {
|
||||
const snapshot = {
|
||||
paused: folderMonitor?.paused === true,
|
||||
pausedAt: folderMonitor?.paused === true ? (folderMonitor?.pausedAt ?? null) : null
|
||||
};
|
||||
return this._enqueueWrite(() => {
|
||||
const current = this.load();
|
||||
const currentGlobalSettings = current.globalSettings || {};
|
||||
current.globalSettings = {
|
||||
...currentGlobalSettings,
|
||||
folderMonitor: {
|
||||
...(currentGlobalSettings.folderMonitor || {}),
|
||||
...snapshot
|
||||
}
|
||||
};
|
||||
this._guardHosters(current, false);
|
||||
return this._commit(current);
|
||||
}, options);
|
||||
}
|
||||
|
||||
saveUploadRecovery(uploadRecovery, options = {}) {
|
||||
const snapshot = uploadRecovery === null || uploadRecovery === undefined ? null : this._clone(uploadRecovery);
|
||||
return this._enqueueWrite(() => {
|
||||
@@ -686,38 +706,29 @@ class ConfigStore {
|
||||
return config.history || [];
|
||||
}
|
||||
|
||||
_atomicWrite(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tmpPath = this.filePath + '.tmp';
|
||||
const backupPath = this.filePath + '.bak';
|
||||
let fd;
|
||||
try {
|
||||
fd = fs.openSync(tmpPath, 'w');
|
||||
fs.writeSync(fd, data);
|
||||
fs.fsyncSync(fd);
|
||||
} catch (e) {
|
||||
try { if (fd !== undefined) fs.closeSync(fd); } catch {}
|
||||
return reject(e);
|
||||
}
|
||||
try { fs.closeSync(fd); } catch {}
|
||||
Promise.resolve().then(() => {
|
||||
try {
|
||||
try {
|
||||
if (fs.existsSync(this.filePath)) {
|
||||
const cur = fs.readFileSync(this.filePath, 'utf-8');
|
||||
if (cur && cur.trim().length > 2) fs.writeFileSync(backupPath, cur, 'utf-8');
|
||||
}
|
||||
} catch {}
|
||||
fs.renameSync(tmpPath, this.filePath);
|
||||
} catch (e) { return reject(e); }
|
||||
// Invalidate the read cache: the next load() re-reads + re-merges the
|
||||
// freshly-written file (the on-disk format is sparse — load() fills
|
||||
// defaults — so we must NOT serve a pre-merge in-memory object).
|
||||
this._cache = null;
|
||||
this._cacheKey = '';
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
async _atomicWrite(data) {
|
||||
const tmpPath = this.filePath + '.tmp';
|
||||
const backupPath = this.filePath + '.bak';
|
||||
const fileHandle = await fs.promises.open(tmpPath, 'w');
|
||||
let writeError = null;
|
||||
try {
|
||||
await fileHandle.writeFile(data);
|
||||
await fileHandle.sync();
|
||||
} catch (error) {
|
||||
writeError = error;
|
||||
}
|
||||
try { await fileHandle.close(); } catch {}
|
||||
if (writeError) throw writeError;
|
||||
try {
|
||||
const current = await fs.promises.readFile(this.filePath, 'utf-8');
|
||||
if (current && current.trim().length > 2) await fs.promises.writeFile(backupPath, current, 'utf-8');
|
||||
} catch {}
|
||||
await fs.promises.rename(tmpPath, this.filePath);
|
||||
// Invalidate the read cache: the next load() re-reads + re-merges the
|
||||
// freshly-written file (the on-disk format is sparse — load() fills
|
||||
// defaults — so we must NOT serve a pre-merge in-memory object).
|
||||
this._cache = null;
|
||||
this._cacheKey = '';
|
||||
}
|
||||
|
||||
appendHistory(entry) {
|
||||
|
||||
Reference in New Issue
Block a user