Retry transient Windows pet-file replacement without overwriting changes

Windows recorder CI now passes 56 of 57 tests; the remaining live-reader fixture
hit intermittent rename EPERM after two successful rotations. Retry EPERM/EBUSY
for under two seconds, rechecking destination identity, size and modification
time before each attempt. Persistent denial keeps the old file and archive.

Validation: pet typecheck/build/test: 57 passed, 0 failed on macOS.
npm test: 66 package, 12 SDK, 446 web passed; 0 failed.
npm run check:web: 0 errors, 2 existing image warnings.
Native bundle unchanged. Corrected-head Windows CI remains required.

Signed-off-by: CodeWhale Bot <[email protected]>
This commit is contained in:
CodeWhale Bot committed 2026-09-12 13:28:43 -07:00
1 parent 369ec14441
commit 6d9605259d
2 files changed
+20 -2

No files matched your search

+4 -1
View File
@@ -163,7 +163,10 @@ open junctions; the same particle identities return to the whale at rest.
tests now use CRLF assets and fail against the previous controller. CLI tests
use a test-only IPC preload for Windows shutdown-handler coverage because
`child.kill('SIGINT')` forcibly terminates a Windows process; POSIX tests retain
the real signal. Current Windows CI is required to verify the platform fix.
the real signal. The next Windows run passed 56/57 tests but found intermittent
reader contention during replacement. Windows replacement now retries for
under two seconds and rechecks destination identity, size and modification
time before each attempt. Current Windows CI is required to verify that fix.
- Bounded Runtime input: a real local HTTP/SSE fixture supplies 260,001 records
spanning nearly 29 hours of event timestamps. The importer retains fewer than
512 events and 256 KiB of metadata, including a still-open human request. The
+16 -1
View File
@@ -2,6 +2,7 @@ import { open, link, rename, unlink, lstat } from 'node:fs/promises';
import { constants } from 'node:fs';
import { dirname, basename, resolve } from 'node:path';
import { randomUUID } from 'node:crypto';
import { setTimeout as delay } from 'node:timers/promises';
import { PET_BIN_MS, validatePetBucket, encodePetJSONL } from '../../dist/core/pet-telemetry.js';
async function syncDirectory(path) {
@@ -51,7 +52,21 @@ export async function createPetRecorder(path, { maxBuckets = 216_000, maxBytes =
// Windows can reject replacement while either writer handle is
// open. Both files are synced and the old file is archived first.
await output.close(); output = undefined;
await rename(temporary, path); installed = true;
for (let attempt = 0; ; attempt++) {
const destination = await lstat(path, { bigint: true });
if (!destination.isFile() || destination.dev !== held.dev || destination.ino !== held.ino
|| destination.size !== held.size || destination.mtimeNs !== held.mtimeNs)
throw new Error('The live pet recording changed while rotating; existing files were preserved.');
try { await rename(temporary, path); break; }
catch (error) {
// A reader or file scanner can briefly deny replacement on
// Windows. Retry for under two seconds, checking identity each
// time; persistent denial still stops without deleting history.
if (process.platform !== 'win32' || !['EPERM', 'EBUSY'].includes(error.code) || attempt >= 20) throw error;
await delay(Math.min(100, (attempt + 1) * 25));
}
}
installed = true;
// The first row is already published. Account for it before any
// fallible cleanup/report so a later append sees the actual file.
sequence = 1; bytes = size; segment++;