mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 08:04:25 -10:00
Keep Windows storage revalidation within atomic-save retries
Destination validation and rename share the bounded Windows sharing retry budget. Preserve complete records, corruption/quota/revocation guards and own-temp cleanup; add a controlled transient validation regression and a nonsharing error preservation case. The original same-key cross-process case is unchanged. Local gate under Node22: npm test 1291 passed, 0 failed, 7 skipped (wrappers101, SDK19, source-host401, web770); check:web build passed. Storage Node20/0 and Bun20/0; extension-host typecheck passed. Identical new regression against saved committed source failed0/1 at550ms virtual delay with old bytes preserved; repaired source succeeded at750ms. Exact source snapshot e2c801a846eaf1aa562c22ce7b6d42015f8c897df89489433999ca449abf10eb unchanged across gate. Independent review f968d8c6b0dbaf2b0608cb2ab939b4823b4fd386a60b6e1fdfc30c76dcc9038b. Evidence: CW/artifacts/setup-design-refinement-20261006/windows-source-host-storage-repair and windows-storage-root-gate. Original hosted EPERM syscall remains unknown; actual Windows Node/Bun/compiled-host qualification is pending. No Rust build, provider/model/account call, deployment or publication. Signed-off-by: CodeWhale Bot <[email protected]>
This commit is contained in:
1 parent
31ba5aec97
commit
be15abff36
2 files changed
+80
-5
No files matched your search
@@ -65,7 +65,7 @@ async function retryWindowsSharing<T>(operation: () => Promise<T>, beforeRetry?:
|
||||
if (retry) await beforeRetry?.()
|
||||
try { return await operation() } catch (error) {
|
||||
if (process.platform !== 'win32' || retry >= 10 || !['EACCES', 'EBUSY', 'EPERM'].includes(fsCode(error) ?? '')) throw error
|
||||
await delay(50)
|
||||
await delay((retry + 1) * 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,12 +191,12 @@ export function createStorage({ dataDir, isActive, onWarning }: StorageOptions):
|
||||
await file.close()
|
||||
file = undefined
|
||||
await retryWindowsSharing(async () => {
|
||||
// Revalidation and publication share one bounded retry budget. A
|
||||
// sharing error while checking the destination must not escape it.
|
||||
active()
|
||||
await readRecordOnce(directory, name)
|
||||
active()
|
||||
await rename(temporary, join(directory, name))
|
||||
}, async () => {
|
||||
// A retry never bypasses a newly corrupt/linked destination or revocation.
|
||||
active()
|
||||
await readRecord(directory, name)
|
||||
})
|
||||
published = true
|
||||
await syncDirectory(directory)
|
||||
|
||||
@@ -268,6 +268,81 @@ test('Windows sharing retries keep complete records and retain atomic replacemen
|
||||
assert.match(report.names[0], /^[a-f0-9]{64}\.json$/u)
|
||||
})
|
||||
|
||||
test('Windows destination revalidation recovers after a sharing interval beyond 500ms', async (t) => {
|
||||
const { a, store } = await fixture(t)
|
||||
await store.set('shared', 'original')
|
||||
const report = await sharingWorker(t, a, `
|
||||
const timers = require('node:timers/promises');
|
||||
const realOpen = fs.promises.open, realRename = fs.promises.rename, realRead = fs.promises.readFile;
|
||||
const path = require('node:path').join(process.argv[1], 'storage-v1', require('node:crypto').createHash('sha256').update('shared').digest('hex') + '.json');
|
||||
const original = await realRead(path, 'utf8');
|
||||
let elapsed = 0, publications = 0, validationDenials = 0;
|
||||
const observed = [], waits = [];
|
||||
// A virtual sharing interval avoids tying this regression to runner load.
|
||||
// Filesystem reads, writes, fsync and the eventual rename remain real.
|
||||
timers.setTimeout = async (ms) => { elapsed += ms; waits.push(ms); };
|
||||
fs.promises.open = async (...args) => {
|
||||
if (String(args[0]).endsWith(require('node:path').basename(path)) && publications && elapsed < 600) {
|
||||
validationDenials++;
|
||||
observed.push(await realRead(path, 'utf8'));
|
||||
throw Object.assign(new Error('destination sharing'), { code: 'EPERM' });
|
||||
}
|
||||
return realOpen(...args);
|
||||
};
|
||||
fs.promises.rename = async (...args) => {
|
||||
if (publications++ === 0) {
|
||||
observed.push(await realRead(path, 'utf8'));
|
||||
throw Object.assign(new Error('publication sharing'), { code: 'EACCES' });
|
||||
}
|
||||
return realRename(...args);
|
||||
};
|
||||
syncBuiltinESMExports();
|
||||
const { createStorage } = await import(${JSON.stringify(moduleUrl)});
|
||||
const store = createStorage({ dataDir: process.argv[1], isActive: () => true });
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
let code;
|
||||
try { await store.set('shared', 'complete replacement'); } catch (error) { code = error.code; }
|
||||
process.send({ code, original, observed, waits, elapsed, publications, validationDenials,
|
||||
value: await store.get('shared'), bytes: await realRead(path, 'utf8'),
|
||||
names: await fs.promises.readdir(require('node:path').dirname(path)) });
|
||||
`)
|
||||
t.diagnostic(JSON.stringify(report))
|
||||
assert.equal(report.code, undefined, JSON.stringify(report))
|
||||
assert.ok(report.elapsed >= 600 && report.elapsed <= 2750, 'the finite sharing interval must settle within the bounded retry budget')
|
||||
assert.ok(report.validationDenials > 0)
|
||||
assert.equal(report.publications, 2, 'one refused rename followed by one real atomic publication')
|
||||
assert.equal(report.original, JSON.stringify({ version: 1, key: 'shared', value: 'original' }) + '\n')
|
||||
assert.ok(report.observed.length > 1 && report.observed.every((bytes) => bytes === report.original), 'old complete bytes remain visible throughout refusal')
|
||||
assert.equal(report.value, 'complete replacement')
|
||||
assert.equal(report.bytes, JSON.stringify({ version: 1, key: 'shared', value: 'complete replacement' }) + '\n')
|
||||
assert.equal(report.names.length, 1, 'this invocation cleans its temporary file without deleting the destination')
|
||||
assert.match(report.names[0], /^[a-f0-9]{64}\.json$/u)
|
||||
})
|
||||
|
||||
test('Windows nonsharing publication errors preserve old state without retry', async (t) => {
|
||||
const { a, store } = await fixture(t)
|
||||
await store.set('shared', 'original')
|
||||
const report = await sharingWorker(t, a, `
|
||||
const path = require('node:path').join(process.argv[1], 'storage-v1', require('node:crypto').createHash('sha256').update('shared').digest('hex') + '.json');
|
||||
const original = await fs.promises.readFile(path, 'utf8');
|
||||
let publications = 0;
|
||||
fs.promises.rename = async () => { publications++; throw Object.assign(new Error('io'), { code: 'EIO' }); };
|
||||
syncBuiltinESMExports();
|
||||
const { createStorage } = await import(${JSON.stringify(moduleUrl)});
|
||||
const store = createStorage({ dataDir: process.argv[1], isActive: () => true });
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
let code;
|
||||
try { await store.set('shared', 'must not publish'); } catch (error) { code = error.code; }
|
||||
process.send({ code, publications, original, bytes: await fs.promises.readFile(path, 'utf8'),
|
||||
names: await fs.promises.readdir(require('node:path').dirname(path)) });
|
||||
`)
|
||||
assert.equal(report.code, 'io')
|
||||
assert.equal(report.publications, 1)
|
||||
assert.equal(report.bytes, report.original)
|
||||
assert.equal(report.names.length, 1)
|
||||
assert.match(report.names[0], /^[a-f0-9]{64}\.json$/u)
|
||||
})
|
||||
|
||||
test('Windows sharing retries refuse revocation, corrupt replacements and permanent denial', async (t) => {
|
||||
const { a } = await fixture(t)
|
||||
for (const mode of ['revoked', 'corrupt', 'denied']) {
|
||||
|
||||
Reference in new issue
Block a user