{"record":{"id":"fa76c5d0d441f520","repo":"Hmbown/CodeWhale","slug":"another-pet-recorder-is-using-this-output","errorCode":null,"errorMessage":"Another pet recorder is using this output.","messagePattern":"Another pet recorder is using this output\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pet/scripts/lib/pet-recorder.mjs","lineNumber":43,"sourceCode":"  catch (error) { if (error.code !== 'EEXIST') throw error; }\n  const identity = await lstat(name, { bigint: true });\n  if (!identity.isFile() || identity.nlink !== 1n || identity.size !== 0n)\n    throw new Error('Invalid pet recorder lock; existing files were preserved.');\n  let database;\n  const check = async () => {\n    const current = await lstat(name, { bigint: true });\n    if (!current.isFile() || current.nlink !== 1n || current.size !== 0n\n      || current.dev !== identity.dev || current.ino !== identity.ino)\n      throw new Error('The pet recorder lock was replaced; existing files were preserved.');\n  };\n  try {\n    database = new DatabaseSync(name);\n    database.exec('PRAGMA busy_timeout = 0; BEGIN EXCLUSIVE');\n    await check();\n    return { check, close: () => { database.close(); } };\n  } catch (error) {\n    database?.close();\n    if (error.errcode === 5 || error.errcode === 6) throw new Error('Another pet recorder is using this output.');\n    throw error;\n  }\n}\n\n/** Replaces the CLI's unbounded append-only output. Each complete segment is\n * replayable on its own; the same live pathname always holds the newest one. */\nexport async function createPetRecorder(path, { maxBuckets = 216_000, maxBytes = 64 * 1024 * 1024, report = () => {}, resume = false } = {}) {\n  if (!Number.isSafeInteger(maxBuckets) || maxBuckets < 1 || maxBuckets > 216_000\n    || !Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 64 * 1024 * 1024)\n    throw new Error('Invalid pet recording segment limit.');\n  path = resolve(await realpath(dirname(resolve(path))), basename(path));\n  let lock = await lockRecorder(path), output, sequence = 0, bytes = 0, segment = 0, busy = false, restart = false, expectedMtime;\n  try {\n    try { output = await open(path, 'wx', 0o600); }\n    catch (error) {\n      if (!resume || error.code !== 'EEXIST') throw error;\n      const original = await lstat(path, { bigint: true });\n      if (!original.isFile() || original.size > 64n * 1024n * 1024n)","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/scripts/lib/pet-recorder.mjs#L25-L61","documentation":"Inside lockRecorder, the SQLite database is opened on the lock file and BEGIN EXCLUSIVE is issued with busy_timeout 0. If SQLite reports errcode 5 (SQLITE_BUSY) or 6 (SQLITE_LOCKED), another recorder already holds the database lock, so this friendly error is thrown instead of the raw SQLite code. It is the intended 'single writer' signal: a second recorder instance is pointing at the same output.","triggerScenarios":"Starting a second recorder against the same output path while the first still holds the lock: BEGIN EXCLUSIVE returns SQLITE_BUSY (5) or SQLITE_LOCKED (6).","commonSituations":"Running two recorder processes/terminals on the same output; a cron job overlapping a manual run; the previous recorder is a zombie still holding the DB; container restart racing a lingering old instance.","solutions":["Stop the other recorder process using this output path, then rerun.","Point the new recorder at a different output path if both instances are intentional.","Check for lingering processes (ps aux | grep recorder) and kill any stale one before retrying.","Add instance coordination (e.g. your scheduler's mutual exclusion) so overlapping runs never start."],"exampleFix":"// before: second overlapping run\nnode recorder.js --out same.db   # -> Another pet recorder is using this output.\n// after: guard with flock so only one instance ever starts\nflock -n /tmp/recorder.lock -c 'node recorder.js --out same.db' || echo 'already running'","handlingStrategy":"try-catch","validationCode":"// check a live lock exists before even attempting to start a second recorder\nimport { lstat } from 'node:fs/promises';\ntry { await lstat(path + '.writer-lock'); console.error('output appears locked; refusing to start'); process.exit(1); }\ncatch (e) { if (e.code !== 'ENOENT') throw e; }","typeGuard":null,"tryCatchPattern":"try {\n  const lock = await lockRecorder(path);\n} catch (e) {\n  if (e.message === 'Another pet recorder is using this output.') {\n    console.error('recorder already running for', path);\n    process.exit(0); // expected during overlapping runs\n  }\n  throw e;\n}","preventionTips":["Wrap recorder launches in flock or your scheduler's mutual-exclusion feature.","Use one output path per recorder instance.","Kill stale recorder processes after crashes before restarting on the same output."],"tags":["database","locking","concurrency","sqlite"],"backgroundTag":"address-already-in-use","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T01:17:13.364Z"}