Files
Lumotia/docs/issues/c1-live-session-race.md
Jake 9b0067b4c0
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Land release blocker fixes and workspace cleanup
2026-04-23 00:16:09 +01:00

2.6 KiB

RB-01 CRITICAL: racy single-session guard in live.rs

Severity: CRITICAL Path: src-tauri/src/commands/live.rs:193-338 Source: 2026-04-22 code review Labels: release-blocker, critical, concurrency Status: RESOLVED (2026-04-22)

Resolution

LiveTranscriptionState now includes a dedicated tokio::sync::Mutex<()> lifecycle gate. Both start_live_transcription_session and stop_live_transcription_session acquire that async mutex before touching running, and they keep it held across the awaited setup / join work that previously exposed the race windows.

That changes the two failing interleavings from the review:

  • Two overlapping starts no longer race through the empty-slot check. The second call waits for the first to finish setup, then observes running.is_some() and returns the existing "A live transcription session is already running" error.
  • A start launched during stop can no longer sneak in after running.take() but before the previous worker has fully joined. It blocks on the lifecycle mutex until the join completes.

Regression tests in commands::live::tests:

  • concurrent_starts_allow_only_one_session_to_claim_the_slot
  • start_waits_for_stop_to_finish_joining_before_reusing_slot

Problem

start_live_transcription_session checks running is None before multiple awaits and only stores the handle at the end. stop_live_transcription_session removes running before awaiting the worker join. Two overlapping IPC calls can:

  • Admit a second live session (start sees running == None, awaits, another start fires in the gap, both proceed)
  • Expose an empty slot while the first session is still shutting down (stop removes the handle, awaits, a fresh start runs against the incoherent state)

This breaks the file's core invariant that only one microphone/live session exists at a time.

Acceptance

  • Hold the session-slot lock (or a semaphore) across the async boundary so no two start/stop IPC calls can interleave.
  • Regression test: fire two start_live_transcription_session IPC calls concurrently; exactly one must succeed and the other must error cleanly.
  • Regression test: during an in-flight stop, a concurrent start must block until the previous session's worker has fully joined.

Fix scope

Large. Will likely require the run_live_session monolith refactor (RB-04) to land first so the state machine is small enough to reason about under the lock discipline.

Dependencies

  • Landed after RB-04 (run_live_session refactor) made the worker lifecycle explicit enough to guard safely.