{"record":{"id":"0145deb8da80a40f","repo":"Hmbown/CodeWhale","slug":"telemetry-privacy-lock-is-held","errorCode":null,"errorMessage":"telemetry privacy lock is held","messagePattern":"telemetry privacy lock is held","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/telemetry/src/envelope.rs","lineNumber":84,"sourceCode":"        let path = buffer::install_id_path(root);\n        let existing = std::fs::read_to_string(&path)\n            .ok()\n            .and_then(|body| serde_json::from_str::<InstallId>(&body).ok())\n            .filter(|record| uuid::Uuid::parse_str(record.install_id.trim()).is_ok())\n            .filter(|record| !is_expired(&record.rotated_at));\n        if let Some(record) = existing {\n            return Ok(record);\n        }\n        let record = InstallId {\n            schema_version: 1,\n            install_id: uuid::Uuid::new_v4().to_string(),\n            rotated_at: now_rfc3339(),\n        };\n        codewhale_config::persistence::atomic_write_json(&path, &record)\n            .with_context(|| format!(\"failed to write {}\", path.display()))?;\n        Ok(record)\n    })?\n    .ok_or_else(|| anyhow::anyhow!(\"telemetry privacy lock is held\"))\n}\n\nfn is_expired(rotated_at: &str) -> bool {\n    let Ok(parsed) = chrono::DateTime::parse_from_rfc3339(rotated_at) else {\n        // An unreadable timestamp is treated as expired: minting a fresh random\n        // id is always the safe direction.\n        return true;\n    };\n    let age = chrono::Utc::now().signed_duration_since(parsed.with_timezone(&chrono::Utc));\n    age.num_days() >= ROTATION_DAYS\n}\n\n/// Read `state.json`, or a default when it is missing or unreadable.\n#[must_use]\npub fn read_state(root: &Path) -> TelemetryState {\n    std::fs::read_to_string(buffer::state_path(root))\n        .ok()\n        .and_then(|body| serde_json::from_str::<TelemetryState>(&body).ok())","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/telemetry/src/envelope.rs#L66-L102","documentation":"read_or_create_install_id serializes its read/mint through the telemetry compaction lock via try_with_lock, which never blocks: when another process already holds the write lock it returns Ok(None), and the caller converts that into this anyhow error. It means concurrent codewhale processes raced on the same telemetry home and this one lost.","triggerScenarios":"Two or more processes (a running TUI plus a CLI invocation, parallel scheduled drains, or concurrent tests sharing one CODEWHALE_HOME) call telemetry functions at the same moment; the loser's fd-lock try_write fails and surfaces as 'telemetry privacy lock is held'.","commonSituations":"Parallel test suites sharing a single home directory; a long-running TUI holding the lock during compaction while a script calls telemetry APIs; overlapping cron-style drains on the same profile.","solutions":["Retry after a short delay: the lock is held only for the duration of one small critical section, so a second attempt usually wins","Skip telemetry for this run if a bounded retry fails; telemetry is explicitly best-effort","Serialize telemetry access in your own orchestration so only one process touches the home at a time","In tests, give each test its own temp root to avoid cross-test lock contention"],"exampleFix":"// before\nlet id = envelope::read_or_create_install_id(&root)?;\n// after\nlet mut attempts = 0;\nlet id = loop {\n    match envelope::read_or_create_install_id(&root) {\n        Ok(id) => break id,\n        Err(err) if err.to_string() == \"telemetry privacy lock is held\" && attempts < 3 => {\n            attempts += 1;\n            std::thread::sleep(std::time::Duration::from_millis(50));\n        }\n        Err(err) => return Err(err),\n    }\n};","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"let mut attempts = 0;\nlet id = loop {\n    match envelope::read_or_create_install_id(&root) {\n        Ok(id) => break id,\n        Err(err) if err.to_string() == \"telemetry privacy lock is held\" && attempts < 3 => {\n            attempts += 1;\n            std::thread::sleep(std::time::Duration::from_millis(50));\n        }\n        Err(err) => return Err(err),\n    }\n};","preventionTips":["Keep exactly one process per telemetry home doing envelope work at a time","Give each test its own temp root so suites never contend on the fd-lock","Treat telemetry as best-effort: losing one read/mint to lock contention is acceptable","Retry small bounded delays — the critical section is tiny, so contention windows are short"],"tags":["telemetry","concurrency","file-lock","contention"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}