Accepted
2025-11-11
The Torrust Tracker Deployer application was experiencing reentrancy deadlocks with Arc<std::sync::Mutex<UserOutput>> when the same thread attempted to acquire the UserOutput lock multiple times. This occurred in scenarios where:
In CreateTemplateCommandController, we encountered a critical deadlock where:
- Controller Level:
display_success_and_guidance()acquired theUserOutputmutex for error handling - Progress Reporting: While holding the lock, it called
self.progress.complete() - Same Thread Deadlock:
ProgressReporter.complete()tried to acquire the same mutex on the same thread - Result: Tests hung indefinitely requiring timeout mechanisms
// Deadlock scenario with std::sync::Mutex
let user_output = Arc::new(Mutex::new(UserOutput::new(VerbosityLevel::Info)));
// First acquisition in controller
let lock1 = user_output.lock().unwrap();
// ... controller work that calls ProgressReporter ...
// Second acquisition in ProgressReporter (same thread)
let lock2 = user_output.lock().unwrap(); // ❌ DEADLOCK!This pattern was particularly problematic because:
- Error handling and progress reporting are legitimate concurrent concerns
- Both need access to UserOutput for displaying messages to users
- The call stack naturally leads from controllers → progress reporters → user output
- Standard Rust mutexes are not reentrant
This decision addresses GitHub Issue #164: "Eliminate potential deadlock with Arc<Mutex> reentrancy"
Replace Arc<std::sync::Mutex<UserOutput>> with Arc<parking_lot::ReentrantMutex<RefCell<UserOutput>>> throughout the codebase.
parking_lot::ReentrantMutex: Allows the same thread to acquire the lock multiple times safelyRefCell: Provides interior mutability sinceReentrantMutex<T>only gives&Tbut UserOutput methods need&mut selfArc: Maintains shared ownership across components
use std::cell::RefCell;
use std::sync::Arc;
use parking_lot::ReentrantMutex;
// New pattern that eliminates deadlocks
let user_output = Arc::new(ReentrantMutex::new(RefCell::new(
UserOutput::new(VerbosityLevel::Info)
)));
// First acquisition in controller
let lock1 = user_output.lock();
{
let mut output1 = lock1.borrow_mut();
output1.info("Controller processing...");
}
// Second acquisition in ProgressReporter (same thread) - NO DEADLOCK!
let lock2 = user_output.lock(); // ✅ Works correctly
{
let mut output2 = lock2.borrow_mut();
output2.progress("50% complete");
}// Standard usage in components
let lock = user_output.lock();
{
let mut output = lock.borrow_mut();
output.success("Operation completed");
output.data(&format!("Details: {}", details));
} // RefCell borrow dropped here
// ReentrantMutex lock can be held longer if needed- Eliminates Deadlocks: Same-thread reentrancy is now safe and supported
- Maintains Thread Safety: Still protects UserOutput from concurrent access between different threads
- Preserves API: UserOutput methods continue to work with
&mut selfthrough RefCell - Clear Intent: The type signature documents that reentrancy is expected and supported
- Performance:
parking_lot::ReentrantMutexhas similar performance characteristics tostd::sync::Mutex - Minimal Changes: UserOutput API remains unchanged, only synchronization mechanism updated
- Additional Dependency: Introduces dependency on
parking_lotcrate - Runtime Borrow Checking: RefCell uses runtime borrow checking instead of compile-time (though panics are highly unlikely in our usage pattern)
- Slightly More Complex: The type signature is more complex:
Arc<ReentrantMutex<RefCell<UserOutput>>>vsArc<Mutex<UserOutput>> - Learning Curve: Developers need to understand the ReentrantMutex + RefCell pattern
- Components Updated: Container, ExecutionContext, all controllers, ProgressReporter
- API Compatibility: No changes to UserOutput public API - all methods continue to work identically
- Test Verification: Integration tests verify the deadlock scenario is resolved
The current solution using RefCell has important thread safety limitations:
- Single-Thread Only:
RefCelldoes not implementSync, making the pattern unsuitable for multi-threaded access - Same-Thread Reentrancy: The solution only addresses reentrancy within a single thread, not concurrent access from multiple threads
- Current Use Case: This limitation is acceptable for the current codebase, which accesses
UserOutputfrom a single thread during command execution
Multi-Threading Alternative: If future requirements need multi-threaded access to UserOutput, consider:
Arc<ReentrantMutex<Arc<Mutex<UserOutput>>>>However, this would reintroduce the original deadlock problem and require a different architectural solution (e.g., message passing or separate UserOutput instances per thread).
Pros:
- Standard library solution
- Multiple readers possible
Cons:
- Still not reentrant - would deadlock on write → write from same thread
- UserOutput methods need
&mut self, so read access isn't useful - More complex than needed for single-writer use case
Pros:
- Completely eliminates synchronization complexity
- Simple ownership patterns
Cons:
- Loses thread safety guarantees
- Requires extensive architectural changes
- Breaks shared UserOutput patterns needed for progress reporting
- Would require passing UserOutput explicitly through all layers
Status: This approach was documented in a previous ADR but ultimately rejected in favor of the ReentrantMutex solution.
Pros:
- Completely eliminates shared mutable state
- Natural async boundaries
Cons:
- Massive architectural change requiring redesign of entire presentation layer
- Overkill for this specific reentrancy issue
- Would require rewriting all UserOutput usage patterns
- Significantly more complex than the focused ReentrantMutex solution
RefCell will panic if:
- Attempting to borrow mutably while already borrowed mutably (same thread)
- Attempting to borrow mutably while borrowed immutably
In our usage pattern, this is extremely unlikely because:
- We only use mutable borrows (never immutable)
- Borrows are immediately dropped after UserOutput method calls
- No long-lived borrows across function calls
The solution includes integration tests that verify:
- Same-thread multiple acquisitions work without deadlock
- RefCell interior mutability enables
&mut selfmethods - Production code patterns work correctly
See tests/user_output_reentrancy.rs for complete test verification.
This decision supersedes:
- Remove UserOutput Mutex for Simplified Architecture - Marked as superseded by this ReentrantMutex approach
This decision enables:
- Safer progress reporting patterns
- More natural error handling in nested call stacks
- Future expansion of UserOutput usage without deadlock concerns