mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-26 18:35:05 +00:00
fix: allow bounded sustained Windows atomic replacement contention
This commit is contained in:
2 files changed
+45
-5
No files matched your search
@@ -26,7 +26,9 @@ function readFileWithSharingRetry(filePath, options) {
|
||||
}
|
||||
|
||||
function renameAtomic(tempPath, resolvedPath, options) {
|
||||
const deadline = performance.now() + 750;
|
||||
// Concurrent Windows installers can hold replacement sharing locks beyond
|
||||
// the reader's shorter retry budget. Keep publication bounded to two seconds.
|
||||
const deadline = performance.now() + 2000;
|
||||
let attempts = 0;
|
||||
let sleeper;
|
||||
while (true) {
|
||||
@@ -42,7 +44,7 @@ function renameAtomic(tempPath, resolvedPath, options) {
|
||||
const remaining = deadline - performance.now();
|
||||
if (process.platform !== 'win32'
|
||||
|| !['EPERM', 'EACCES', 'EBUSY'].includes(error.code)
|
||||
|| attempts >= 21 || remaining <= 0) throw error;
|
||||
|| attempts >= 81 || remaining <= 0) throw error;
|
||||
sleeper ||= new Int32Array(new SharedArrayBuffer(4));
|
||||
Atomics.wait(sleeper, 0, 0, Math.min(25, remaining));
|
||||
}
|
||||
|
||||
@@ -69,11 +69,34 @@ for (const code of ['EPERM', 'EACCES', 'EBUSY']) {
|
||||
throw expected;
|
||||
});
|
||||
assert.throws(() => helper.writeFileAtomic(destination, 'new'), error => error === expected);
|
||||
assert.strictEqual(attempts, 21, 'Hard attempt bound applies even before the deadline');
|
||||
assert.strictEqual(helper.waits(), 20);
|
||||
assert.strictEqual(attempts, 81, 'Hard attempt bound applies even before the deadline');
|
||||
assert.strictEqual(helper.waits(), 80);
|
||||
assert.strictEqual(fs.readFileSync(destination, 'utf8'), 'old');
|
||||
assert.deepStrictEqual(fs.readdirSync(root), ['state.json']);
|
||||
});
|
||||
|
||||
test(`Windows ${code} recovers sustained sharing contention with ownership rechecked`, ({ root, destination }) => {
|
||||
let attempts = 0;
|
||||
let parents = 0;
|
||||
let guards = 0;
|
||||
const helper = loadHelper('win32', (temporary, target) => {
|
||||
attempts++;
|
||||
assert.strictEqual(fs.readFileSync(target, 'utf8'), 'old');
|
||||
assert.strictEqual(fs.readFileSync(temporary, 'utf8'), 'new');
|
||||
if (attempts <= 50) throw sharingError(code);
|
||||
fs.renameSync(temporary, target);
|
||||
});
|
||||
helper.writeFileAtomic(destination, 'new', {
|
||||
validateParent: () => { parents++; },
|
||||
beforeRename: () => { guards++; },
|
||||
});
|
||||
assert.strictEqual(attempts, 51);
|
||||
assert.strictEqual(guards, attempts);
|
||||
assert.ok(parents >= attempts);
|
||||
assert.strictEqual(helper.waits(), 50);
|
||||
assert.strictEqual(fs.readFileSync(destination, 'utf8'), 'new');
|
||||
assert.deepStrictEqual(fs.readdirSync(root), ['state.json']);
|
||||
});
|
||||
}
|
||||
|
||||
test('Windows retries also stop at the elapsed deadline', ({ root, destination }) => {
|
||||
@@ -81,7 +104,7 @@ test('Windows retries also stop at the elapsed deadline', ({ root, destination }
|
||||
const expected = sharingError('EPERM');
|
||||
const helper = loadHelper('win32', () => { attempts++; throw expected; }, { clockStep: 250 });
|
||||
assert.throws(() => helper.writeFileAtomic(destination, 'new'), error => error === expected);
|
||||
assert.strictEqual(attempts, 4);
|
||||
assert.strictEqual(attempts, 9);
|
||||
assert.deepStrictEqual(fs.readdirSync(root), ['state.json']);
|
||||
});
|
||||
|
||||
@@ -124,6 +147,21 @@ test('ownership revocation between sharing failures prevents another rename', ({
|
||||
assert.deepStrictEqual(fs.readdirSync(root), ['state.json']);
|
||||
});
|
||||
|
||||
test('late ownership revocation stops sustained contention without publication', ({ root, destination }) => {
|
||||
let attempts = 0;
|
||||
let guards = 0;
|
||||
const revoked = new Error('Destination ownership revoked after sustained contention');
|
||||
const helper = loadHelper('win32', () => { attempts++; throw sharingError('EPERM'); });
|
||||
assert.throws(() => helper.writeFileAtomic(destination, 'new', {
|
||||
beforeRename() { if (++guards === 26) throw revoked; },
|
||||
}), error => error === revoked);
|
||||
assert.strictEqual(attempts, 25);
|
||||
assert.strictEqual(guards, 26);
|
||||
assert.strictEqual(helper.waits(), 25);
|
||||
assert.strictEqual(fs.readFileSync(destination, 'utf8'), 'old');
|
||||
assert.deepStrictEqual(fs.readdirSync(root), ['state.json']);
|
||||
});
|
||||
|
||||
test('parent replacement during retry preserves both destinations and original private staging', ({ root, destination }) => {
|
||||
const parent = path.join(root, 'parent');
|
||||
const original = path.join(root, 'original');
|
||||
|
||||
Reference in new issue
Block a user