Let auth profile locks reach stale reclaim

## Summary

- Extends the auth profile lock wait horizon so a fresh leaked lock can cross the stale-lock threshold and be reclaimed.
- Keeps the existing 30s stale threshold unchanged; only the caller-facing timeout moves to `STALE_LOCK_AGE_MS + 5s`.
- Adds a regression test that locks the timeout/stale-age relationship so the recovery path cannot become unreachable again.

## Problem

- Sentry issue TAURI-RUST-B1 reports `Timed out waiting for auth profile lock`.
- The stale-lock reclaim threshold is 30s, but the lock wait timeout was 10s.
- That meant a just-orphaned lock with a live pid could never age into the existing age-based recovery before callers gave up.

## Solution

- Derive `LOCK_TIMEOUT_MS` from `STALE_LOCK_AGE_MS + 5_000`.
- Update stale-lock comments to avoid the stale 10s wording.
- Add a focused unit-level guard for the timeout relation.

## Submission Checklist

- [x] Tests added or updated (happy path + at least one failure / edge case) per [Testing Strategy](../gitbooks/developing/testing-strategy.md#failure-path-requirement)
- [x] **Diff coverage ≥ 80%** — changed lines (Vitest + cargo-llvm-cov merged via `diff-cover`) meet the gate enforced by [`.github/workflows/coverage.yml`](../.github/workflows/coverage.yml). Run `pnpm test:coverage` and `pnpm test:rust` locally; PRs below 80% on changed lines will not merge.
- [x] Coverage matrix updated — N/A: internal auth-profile lock recovery constant, no matrix feature row.
- [x] All affected feature IDs from the matrix are listed in the PR description under `## Related`
- [x] No new external network dependencies introduced (mock backend used per [Testing Strategy](../gitbooks/developing/testing-strategy.md#mock-policy))
- [x] Manual smoke checklist updated if this touches release-cut surfaces — N/A: no release-cut surface touched.
- [x] Linked issue closed via `Closes #NNN` in the `## Related` section

## Impact

- Auth profile operations now wait long enough for existing stale-lock recovery to handle fresh leaked locks.
- Worst-case wait before reporting a truly live lock increases from 10s to 35s.
- No storage schema or API contract changes.

## Related

- Closes: #2318
- Follow-up PR(s)/TODOs: N/A
- Coverage matrix feature IDs: N/A: credentials lock recovery internals.

---

## AI Authored PR Metadata (required for Codex/Linear PRs)

### Linear Issue
- Key: N/A
- URL: N/A

### Commit & Branch
- Branch: codex/2318-auth-profile-lock-timeout
- Commit SHA: c5267484

### Validation Run
- [x] `pnpm --filter openhuman-app format:check` — N/A: no frontend files changed.
- [x] `pnpm typecheck` — N/A: no TypeScript files changed.
- [x] Focused tests: attempted `cargo test -p openhuman credentials::profiles --lib`.
- [x] Rust fmt/check (if changed): `cargo fmt --all --check`; `git diff --check`.
- [x] Tauri fmt/check (if changed): N/A, no `app/src-tauri` files changed.

### Validation Blocked
- `command:` `cargo test -p openhuman credentials::profiles --lib`
- `error:` `whisper-rs-sys` build could not find `clang.dll` / `libclang.dll`; `LIBCLANG_PATH` is not set in this local Windows environment.
- `impact:` The targeted Rust test binary did not run locally; CI has the Linux Rust toolchain/image and should execute it.

### Behavior Changes
- Intended behavior change: auth profile lock acquisition can wait past the stale-lock threshold and reclaim a fresh leaked lock instead of timing out first.
- User-visible effect: fewer startup/session failures from transient orphaned `auth-profiles.lock` files.

### Parity Contract
- Legacy behavior preserved: stale lock detection logic, pid checks, malformed-lock rules, and guard cleanup are unchanged.
- Guard/fallback/dispatch parity checks: existing stale-lock tests remain in place; new constant guard ensures recovery remains reachable.

### Duplicate / Superseded PR Handling
- Duplicate PR(s): N/A
- Canonical PR: N/A
- Resolution (closed/superseded/updated): N/A

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Refactor**
  * Improved lock timeout configuration for enhanced maintainability.

* **Tests**
  * Added test to validate lock timeout behavior and timing constraints.

<!-- review_stack_entry_start -->

[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/tinyhumansai/openhuman/pull/2321?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: aqilaziz <gonzes7@gmail.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
Aqil Aziz
2026-05-20 16:59:23 -07:00
committed by GitHub
co-authored by Steven Enamakel
parent ca31ed1c55
commit 7bc053a8b1
2 changed files with 17 additions and 3 deletions
+5 -3
View File
@@ -13,7 +13,6 @@ const CURRENT_SCHEMA_VERSION: u32 = 1;
const PROFILES_FILENAME: &str = "auth-profiles.json";
const LOCK_FILENAME: &str = "auth-profiles.lock";
const LOCK_WAIT_MS: u64 = 50;
const LOCK_TIMEOUT_MS: u64 = 10_000;
/// A lock file that has existed for longer than this is treated as leaked
/// (its owner crashed without unlinking it, or `fs::remove_file` in the
/// guard's `Drop` was rejected by Windows AV/indexer and the file got
@@ -23,6 +22,9 @@ const LOCK_TIMEOUT_MS: u64 = 10_000;
/// threshold is intentionally well above any realistic operation time
/// so we never reclaim under a slow-but-legitimate holder.
const STALE_LOCK_AGE_MS: u64 = 30_000;
/// Wait long enough for a fresh leaked lock to cross the stale threshold
/// and be reclaimed before surfacing a lock timeout to the caller.
const LOCK_TIMEOUT_MS: u64 = STALE_LOCK_AGE_MS + 5_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
@@ -595,7 +597,7 @@ impl AuthProfilesStore {
// Issue #1612 — a previous openhuman crash can leave a
// stale auth-profiles.lock behind, after which every RPC
// path that touches the auth profile store fails for the
// 10s LOCK_TIMEOUT_MS window and the user gets stuck in a
// `LOCK_TIMEOUT_MS` window and the user gets stuck in a
// retry storm. Before falling back to the busy-wait, try
// once to peek at the writer's recorded PID and remove
// the lock if that process is no longer alive. Flag is
@@ -654,7 +656,7 @@ impl AuthProfilesStore {
/// `AuthProfileLockGuard::drop` could not unlink the file (AV /
/// indexer briefly held a handle) and orphaned the lock with its
/// still-alive pid inside — every subsequent acquirer would
/// otherwise spin the full 10s `LOCK_TIMEOUT_MS` and bail. No
/// otherwise spin the full `LOCK_TIMEOUT_MS` and bail. No
/// legitimate auth-profile op holds the lock long enough to be
/// affected, so a too-old lock is unambiguously a leak.
///
@@ -495,6 +495,18 @@ fn clear_lock_if_stale_reclaims_aged_malformed_lock() {
assert!(!lock_path.exists());
}
#[test]
fn lock_timeout_allows_fresh_leaked_locks_to_age_into_stale_reclaim() {
assert!(
LOCK_TIMEOUT_MS > STALE_LOCK_AGE_MS,
"lock timeout must outlive stale-lock age so a fresh leaked lock can be reclaimed"
);
assert!(
LOCK_TIMEOUT_MS - STALE_LOCK_AGE_MS >= 1_000,
"timeout should leave at least one periodic stale recheck after the threshold"
);
}
/// Sentry OPENHUMAN-TAURI-H8: when `OpenOptions::create_new` fails with
/// anything other than `AlreadyExists`, the error surfaced to Sentry
/// must embed the underlying `io::ErrorKind` and `raw_os_error()` so we