How tuicr Handles Concurrent Session Writes and External Comment Merging
tuicr prevents data races by using exclusive file locks and atomic rename operations, ensuring that multiple processes can safely write to the same review session while automatically merging comments added by external CLI commands.
The agavra/tuicr repository implements a robust persistence layer that allows the TUI and external processes to collaborate on the same code review session without corruption. Understanding how tuicr handles concurrent session writes reveals a battle-tested pattern for atomic file operations in Rust applications.
Lock-Based Storage Architecture
The core mechanism resides in src/persistence/storage.rs, where every write operation follows a strict read-modify-write cycle protected by filesystem locks.
Exclusive File Locking with fs2
Before mutating a session, the process creates a side-car lock file (<slug>.lock) adjacent to the target JSON. It attempts an exclusive lock using fs2::FileExt::try_lock_exclusive. If another process holds the lock, the writer backs off and retries, preventing simultaneous writes to the same session file.
This lock file acts as a mutex across process boundaries. The slug—which encodes the session identity as local:<repo_path> or gh:<owner>/<repo>/pr/<num>—ensures that different review contexts never interfere with one another.
Atomic Write Operations
To eliminate corruption risks during crashes, tuicr never writes directly to the target file. Instead, save_session_by_identity in src/persistence/storage.rs executes a three-phase commit:
- Serialize to temporary file: The updated
ReviewSessionis written to<slug>.tmp. - Atomic rename:
std::fs::renamemoves the temporary file over the original session file. On POSIX systems, this replacement is atomic, meaning readers always see either the old complete file or the new complete file—never a partial write. - Cleanup: The lock file is removed only after the rename succeeds, signaling that the transaction is complete.
Stale Lock Recovery
If a process crashes while holding the lock, the lock file persists. Subsequent writers detect this stale lock via a timeout mechanism (timed out waiting for review storage lock), remove the orphaned lock file, and proceed with their own write. This guarantees forward progress even after hard failures.
Merging External Comments in Real Time
External processes—such as tuicr review add—use the ReviewStore facade in src/review_store.rs to modify sessions independently of the TUI. The storage layer automatically reconciles these changes.
The Slug-Addressed Session Identity
Sessions are globally identified by a slug string. Both the TUI and CLI calculate this slug deterministically from the repository context, ensuring they address the same physical file in the user-wide review directory (default: ~/.local/share/tuicr/reviews/).
When ReviewStore::add_comment invokes storage::save_session_by_identity, the closure receives the latest persisted session as its argument. By loading the current state before applying mutations, the function naturally merges external comments with existing data.
Polling and Hot-Reload Logic
The TUI monitors the session file for changes using a configurable polling interval (review_watch_interval_ms, default 1000ms). When App::reload_session in src/app/session.rs detects a newer file modification timestamp, it:
- Reloads the JSON into memory via
load_latest_session_for_context. - Merges new comments into the
App.sessionstate. - Updates the
CommentNavigatorindex so new annotations appear instantly in the UI.
This polling mechanism ensures that comments added via the CLI appear in the running TUI within one second, without requiring inter-process communication channels.
Implementation Walkthrough
The following patterns demonstrate the concurrent-safe APIs used throughout the codebase:
// External process: Adding a comment via the public API
use tuicr::review_store::ReviewStore;
use std::path::PathBuf;
fn add_external_comment(slug: &str) -> Result<(), Box<dyn std::error::Error>> {
ReviewStore::default().add_comment(
slug,
Comment::new_line(
PathBuf::from("src/main.rs"),
42,
"Consider extracting this into a helper function".to_string(),
),
)?;
Ok(())
}
// TUI internal: Persisting pending edits with automatic merge
use crate::persistence::storage::{save_session_by_identity, slug_for_session};
fn save_current_comment(app: &mut App) -> anyhow::Result<()> {
let slug = slug_for_session(&app.session)?;
save_session_by_identity(&slug, |persisted_session| {
// persisted_session contains the latest data from disk,
// including any external comments added since last load
persisted_session.add_comment(app.pending_comment.clone());
Ok(persisted_session.clone())
})?;
app.pending_comment.clear();
Ok(())
}
The save_session_by_identity function signature enforces the read-modify-write pattern by requiring a closure that receives the current session and returns the modified version, preventing accidental blind writes.
Summary
- Exclusive file locks (
<slug>.lock) prevent concurrent writes usingfs2::FileExt::try_lock_exclusiveas implemented insrc/persistence/storage.rs. - Atomic commits via temporary files and
std::fs::renameensure crash-resistant persistence. - Stale lock detection recovers automatically from crashed processes using timeout-based lock validation.
- Slug-based addressing guarantees that sessions are uniquely identified across TUI and CLI contexts.
- Polling-based reloading (
review_watch_interval_ms) merges external comments into the running TUI viaApp::reload_sessioninsrc/app/session.rs.
Frequently Asked Questions
What happens if two tuicr instances try to save simultaneously?
The first process acquires the exclusive lock on <slug>.lock and proceeds with the read-modify-write cycle. The second process blocks briefly, then retries. Because save_session_by_identity reloads the session state inside the locked section, the second write automatically incorporates any changes committed by the first process, preventing lost updates.
How does tuicr avoid corrupting session files if the process crashes mid-write?
All writes go to a temporary file (<slug>.tmp) first. Only after the entire JSON payload is successfully serialized does the code call std::fs::rename to move the temp file over the target. Since POSIX renames are atomic, a crash at any point leaves either the original valid file or the new valid file—never a truncated or garbled intermediate state.
Can I run the TUI and CLI commands in parallel on the same review?
Yes. The TUI polls the storage directory every second (configurable via review_watch_interval_ms) and calls load_latest_session_for_context to detect changes. When the CLI writes a new comment through ReviewStore, the TUI picks up the updated file timestamp on its next poll cycle and reloads the session, displaying the new comment immediately without restart.
Where are the unit tests for concurrent write handling?
The test suite in src/app/tests/persistence_merge_tests.rs simulates simultaneous writes from multiple processes and asserts that the final session contains the union of all comments. Additionally, src/app/tests/submit_flow_tests.rs exercises the full comment submission lifecycle, validating lock acquisition and atomic rename behavior under various failure scenarios.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →