Hmbown/CodeWhale · warning

telemetry privacy lock is held

Error message

telemetry privacy lock is held

What it means

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.

Source

Thrown at crates/telemetry/src/envelope.rs:84

        let path = buffer::install_id_path(root);
        let existing = std::fs::read_to_string(&path)
            .ok()
            .and_then(|body| serde_json::from_str::<InstallId>(&body).ok())
            .filter(|record| uuid::Uuid::parse_str(record.install_id.trim()).is_ok())
            .filter(|record| !is_expired(&record.rotated_at));
        if let Some(record) = existing {
            return Ok(record);
        }
        let record = InstallId {
            schema_version: 1,
            install_id: uuid::Uuid::new_v4().to_string(),
            rotated_at: now_rfc3339(),
        };
        codewhale_config::persistence::atomic_write_json(&path, &record)
            .with_context(|| format!("failed to write {}", path.display()))?;
        Ok(record)
    })?
    .ok_or_else(|| anyhow::anyhow!("telemetry privacy lock is held"))
}

fn is_expired(rotated_at: &str) -> bool {
    let Ok(parsed) = chrono::DateTime::parse_from_rfc3339(rotated_at) else {
        // An unreadable timestamp is treated as expired: minting a fresh random
        // id is always the safe direction.
        return true;
    };
    let age = chrono::Utc::now().signed_duration_since(parsed.with_timezone(&chrono::Utc));
    age.num_days() >= ROTATION_DAYS
}

/// Read `state.json`, or a default when it is missing or unreadable.
#[must_use]
pub fn read_state(root: &Path) -> TelemetryState {
    std::fs::read_to_string(buffer::state_path(root))
        .ok()
        .and_then(|body| serde_json::from_str::<TelemetryState>(&body).ok())

View on GitHub (pinned to 8880682c63)

Solutions

  1. Retry after a short delay: the lock is held only for the duration of one small critical section, so a second attempt usually wins
  2. Skip telemetry for this run if a bounded retry fails; telemetry is explicitly best-effort
  3. Serialize telemetry access in your own orchestration so only one process touches the home at a time
  4. In tests, give each test its own temp root to avoid cross-test lock contention

Example fix

// before
let id = envelope::read_or_create_install_id(&root)?;
// after
let mut attempts = 0;
let id = loop {
    match envelope::read_or_create_install_id(&root) {
        Ok(id) => break id,
        Err(err) if err.to_string() == "telemetry privacy lock is held" && attempts < 3 => {
            attempts += 1;
            std::thread::sleep(std::time::Duration::from_millis(50));
        }
        Err(err) => return Err(err),
    }
};
Defensive patterns

Strategy: retry

Try / catch

let mut attempts = 0;
let id = loop {
    match envelope::read_or_create_install_id(&root) {
        Ok(id) => break id,
        Err(err) if err.to_string() == "telemetry privacy lock is held" && attempts < 3 => {
            attempts += 1;
            std::thread::sleep(std::time::Duration::from_millis(50));
        }
        Err(err) => return Err(err),
    }
};

Prevention

When it happens

Trigger: 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'.

Common situations: 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.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/0145deb8da80a40f. Report an issue: GitHub.