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) {
|
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 attempts = 0;
|
||||||
let sleeper;
|
let sleeper;
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -42,7 +44,7 @@ function renameAtomic(tempPath, resolvedPath, options) {
|
|||||||
const remaining = deadline - performance.now();
|
const remaining = deadline - performance.now();
|
||||||
if (process.platform !== 'win32'
|
if (process.platform !== 'win32'
|
||||||
|| !['EPERM', 'EACCES', 'EBUSY'].includes(error.code)
|
|| !['EPERM', 'EACCES', 'EBUSY'].includes(error.code)
|
||||||
|| attempts >= 21 || remaining <= 0) throw error;
|
|| attempts >= 81 || remaining <= 0) throw error;
|
||||||
sleeper ||= new Int32Array(new SharedArrayBuffer(4));
|
sleeper ||= new Int32Array(new SharedArrayBuffer(4));
|
||||||
Atomics.wait(sleeper, 0, 0, Math.min(25, remaining));
|
Atomics.wait(sleeper, 0, 0, Math.min(25, remaining));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,11 +69,34 @@ for (const code of ['EPERM', 'EACCES', 'EBUSY']) {
|
|||||||
throw expected;
|
throw expected;
|
||||||
});
|
});
|
||||||
assert.throws(() => helper.writeFileAtomic(destination, 'new'), error => error === expected);
|
assert.throws(() => helper.writeFileAtomic(destination, 'new'), error => error === expected);
|
||||||
assert.strictEqual(attempts, 21, 'Hard attempt bound applies even before the deadline');
|
assert.strictEqual(attempts, 81, 'Hard attempt bound applies even before the deadline');
|
||||||
assert.strictEqual(helper.waits(), 20);
|
assert.strictEqual(helper.waits(), 80);
|
||||||
assert.strictEqual(fs.readFileSync(destination, 'utf8'), 'old');
|
assert.strictEqual(fs.readFileSync(destination, 'utf8'), 'old');
|
||||||
assert.deepStrictEqual(fs.readdirSync(root), ['state.json']);
|
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 }) => {
|
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 expected = sharingError('EPERM');
|
||||||
const helper = loadHelper('win32', () => { attempts++; throw expected; }, { clockStep: 250 });
|
const helper = loadHelper('win32', () => { attempts++; throw expected; }, { clockStep: 250 });
|
||||||
assert.throws(() => helper.writeFileAtomic(destination, 'new'), error => error === expected);
|
assert.throws(() => helper.writeFileAtomic(destination, 'new'), error => error === expected);
|
||||||
assert.strictEqual(attempts, 4);
|
assert.strictEqual(attempts, 9);
|
||||||
assert.deepStrictEqual(fs.readdirSync(root), ['state.json']);
|
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']);
|
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 }) => {
|
test('parent replacement during retry preserves both destinations and original private staging', ({ root, destination }) => {
|
||||||
const parent = path.join(root, 'parent');
|
const parent = path.join(root, 'parent');
|
||||||
const original = path.join(root, 'original');
|
const original = path.join(root, 'original');
|
||||||
|
|||||||
Reference in new issue
Block a user