mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-14 12:15:29 +02:00
Preserve the original contributor histories and apply the exact independently reviewed repair.
Source-Parent: 5785183108
Review-Manifest-SHA256: e223d98c24e357715527b786257901d2ef0bebadb75ac61e5c000fe60da10f9a
131 lines
4.7 KiB
JavaScript
131 lines
4.7 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const { performance } = require('perf_hooks');
|
|
|
|
const WAIT_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
const DEFAULT_TIMEOUT_MS = 5000;
|
|
|
|
// Cleanup must never replace an arbitrary primary throw (including frozen errors,
|
|
// primitives, or objects with throwing property traps). Diagnostics are best effort.
|
|
function attachCleanupError(primary, name, secondary) {
|
|
if (primary === null || (typeof primary !== 'object' && typeof primary !== 'function')) return;
|
|
try { Object.defineProperty(primary, name, { value: secondary, configurable: true }); }
|
|
catch (_error) { /* Preserve the primary when it cannot accept diagnostics. */ }
|
|
}
|
|
|
|
function hasCode(error, code) {
|
|
try { return error !== null && error !== undefined && error.code === code; }
|
|
catch (_error) { return false; }
|
|
}
|
|
|
|
function sameIdentity(left, right) {
|
|
return left.ino === right.ino && left.dev === right.dev;
|
|
}
|
|
|
|
function lostLock(lockPath) {
|
|
return Object.assign(new Error(`State-store lock changed or lost; refusing to remove it: ${lockPath}`),
|
|
{ code: 'STATE_STORE_LOCK_LOST' });
|
|
}
|
|
|
|
function releaseOwnedLock(lockPath, descriptor, identity) {
|
|
let failed = false;
|
|
let primary;
|
|
let pendingDelete = false;
|
|
let inspected = false;
|
|
let closeFailed = false;
|
|
try {
|
|
const current = fs.lstatSync(lockPath, { bigint: true });
|
|
inspected = true;
|
|
if (!current.isFile() || current.isSymbolicLink() || !sameIdentity(identity, current)) {
|
|
throw lostLock(lockPath);
|
|
}
|
|
// Keep the owned descriptor through validation and unlink. This cooperative
|
|
// lock is not an atomic compare-and-unlink against a directory mutator.
|
|
fs.unlinkSync(lockPath);
|
|
} catch (error) {
|
|
failed = true;
|
|
primary = hasCode(error, 'ENOENT') ? lostLock(lockPath) : error;
|
|
pendingDelete = !inspected && hasCode(error, 'EPERM');
|
|
}
|
|
try {
|
|
fs.closeSync(descriptor); // Exactly one attempt, even after a release failure.
|
|
} catch (error) {
|
|
closeFailed = true;
|
|
if (failed) attachCleanupError(primary, 'closeError', error);
|
|
else { failed = true; primary = error; }
|
|
}
|
|
if (pendingDelete && !closeFailed) {
|
|
// Windows can defer deletion until the handle closes. Only a subsequent
|
|
// ENOENT proves loss; preserve genuine EPERM and never unlink after close.
|
|
try { fs.lstatSync(lockPath, { bigint: true }); }
|
|
catch (error) { if (hasCode(error, 'ENOENT')) primary = lostLock(lockPath); }
|
|
}
|
|
if (failed) throw primary;
|
|
}
|
|
|
|
function acquireLock(dbPath, timeoutMs) {
|
|
const lockPath = `${dbPath}.ecc-state.lock`;
|
|
const deadline = performance.now() + timeoutMs;
|
|
for (;;) {
|
|
let descriptor;
|
|
try {
|
|
descriptor = fs.openSync(lockPath, 'wx', 0o600);
|
|
} catch (error) {
|
|
if (!hasCode(error, 'EEXIST')) throw error;
|
|
const remaining = deadline - performance.now();
|
|
if (remaining <= 0) {
|
|
const busy = new Error(`State store is busy: ${dbPath}. Retry after the other ECC operation finishes. `
|
|
+ `If an operation terminated unexpectedly, stop all ECC processes using this database, `
|
|
+ `then inspect and remove the leftover lock: ${lockPath}`);
|
|
busy.code = 'STATE_STORE_BUSY';
|
|
throw busy;
|
|
}
|
|
Atomics.wait(WAIT_BUFFER, 0, 0, Math.min(20, remaining));
|
|
continue;
|
|
}
|
|
let identity;
|
|
try {
|
|
identity = fs.fstatSync(descriptor, { bigint: true });
|
|
fs.writeFileSync(descriptor, `${JSON.stringify({ pid: process.pid, hostname: os.hostname() })}\n`);
|
|
} catch (error) {
|
|
try {
|
|
if (identity) releaseOwnedLock(lockPath, descriptor, identity);
|
|
else fs.closeSync(descriptor);
|
|
} catch (releaseError) {
|
|
attachCleanupError(error, 'releaseError', releaseError);
|
|
}
|
|
throw error;
|
|
}
|
|
return () => releaseOwnedLock(lockPath, descriptor, identity);
|
|
}
|
|
}
|
|
|
|
/** Serialize one synchronous snapshot operation, never a handle's lifetime.
|
|
* Locks are not expired or stolen: a paused writer must not lose exclusivity.
|
|
* An abnormal process exit can leave a lock requiring the explicit recovery
|
|
* described by STATE_STORE_BUSY. No database bytes are changed on timeout. */
|
|
function withStateStoreLock(dbPath, callback, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
const release = acquireLock(dbPath, timeoutMs);
|
|
let result;
|
|
let primaryError;
|
|
let failed = false;
|
|
try {
|
|
result = callback();
|
|
} catch (error) {
|
|
failed = true;
|
|
primaryError = error;
|
|
}
|
|
try {
|
|
release();
|
|
} catch (releaseError) {
|
|
if (!failed) throw releaseError;
|
|
attachCleanupError(primaryError, 'releaseError', releaseError);
|
|
}
|
|
if (failed) throw primaryError;
|
|
return result;
|
|
}
|
|
|
|
module.exports = { withStateStoreLock, attachCleanupError };
|