{"record":{"id":"ccea83f88b11eacc","repo":"Hmbown/CodeWhale","slug":"the-live-pet-recording-changed-while-rotating-existing-files","errorCode":null,"errorMessage":"The live pet recording changed while rotating; existing files were preserved.","messagePattern":"The live pet recording changed while rotating; existing files were preserved\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/scripts/lib/pet-recorder.mjs","lineNumber":132,"sourceCode":"          let next, installed = false, created = false;\n          try {\n            next = await open(temporary, 'wx', 0o600); created = true;\n            await next.writeFile(row); await next.sync();\n            const nextIdentity = await next.stat({ bigint: true });\n            await next.close(); next = undefined;\n            await output.sync();\n            // link is exclusive: a collision never replaces someone else's\n            // archive. Persist this name before replacing the live pathname.\n            await link(path, archive); await syncDirectory(dirname(path));\n            // Windows can reject replacement while either writer handle is\n            // open. Both files are synced and the old file is archived first.\n            await output.close(); output = undefined;\n            for (let attempt = 0; ; attempt++) {\n              await lock.check();\n              const destination = await lstat(path, { bigint: true });\n              if (!destination.isFile() || destination.dev !== held.dev || destination.ino !== held.ino\n                || destination.size !== held.size || destination.mtimeNs !== held.mtimeNs)\n                throw new Error('The live pet recording changed while rotating; existing files were preserved.');\n              try { await rename(temporary, path); break; }\n              catch (error) {\n                // A reader or file scanner can briefly deny replacement on\n                // Windows. Retry for under two seconds, checking identity each\n                // time; persistent denial still stops without deleting history.\n                if (process.platform !== 'win32' || !['EPERM', 'EBUSY'].includes(error.code) || attempt >= 20) throw error;\n                await delay(Math.min(100, (attempt + 1) * 25));\n              }\n            }\n            installed = true;\n            // The first row is already published. Account for it before any\n            // fallible cleanup/report so a later append sees the actual file.\n            sequence = 1; bytes = size; segment++; restart = false;\n            output = await open(path, constants.O_WRONLY | constants.O_APPEND);\n            const reopened = await output.stat({ bigint: true });\n            if (reopened.dev !== nextIdentity.dev || reopened.ino !== nextIdentity.ino)\n              throw new Error('The live pet recording was replaced externally after rotation.');\n            expectedMtime = reopened.mtimeNs;","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/scripts/lib/pet-recorder.mjs#L114-L150","documentation":"During rotation, after staging the new segment file, the recorder re-stats the destination path and requires it to still be the exact same file (dev, inode, size, mtime) it held before performing the atomic rename. If the live recording changed meanwhile, it refuses to replace it and preserves the on-disk state.","triggerScenarios":"Another writer appended to or replaced the recording between the append-time identity check and the rename; a reader/scanner tool touched the file; the file was deleted and recreated during the rotation window.","commonSituations":"Log scanners or antivirus briefly holding/altering the file during rotation; a second recorder instance racing for the same path; monitoring jobs that copy-then-truncate the live file.","solutions":["Ensure no other process writes or replaces the recording while the recorder runs.","Retry the append after the competing writer stops; nothing was destroyed.","On Windows persistent EPERM/EBUSY, close tools (indexers, editors) holding the file and retry.","If a job must read the file, have it copy without modifying mtime or use read-only access."],"exampleFix":"// before: a cron job rewrites the live file during rotation\n* * * * * cp pet-recording.jsonl backup.jsonl && : > pet-recording.jsonl\n// after: copy only, never modify\n* * * * * cp pet-recording.jsonl backup.jsonl","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await recorder.append(bucket);\n} catch (e) {\n  if (e.message.includes('changed while rotating')) {\n    await sleep(500); // competing writer/AV finished; files were preserved\n    await recorder.append(bucket); // retry once\n  } else throw e;\n}","preventionTips":["Keep AV/indexers away from the recording directory","Do not run competing writers during rotation-prone appends","On Windows, close tools that hold file handles (EPERM/EBUSY)","Treat this as transient: state on disk is intact, retry"],"tags":["concurrency","file-integrity","rotation"],"backgroundTag":"file-write-failed","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}