Contents
Concurrency Improvements for Mentatd
Problem Statement
Load tests showed that mentatd throughput doesn’t scale linearly with concurrency: - 50 concurrent connections → only 29.5 TPS - P99 latency: 2230ms - Suggests lock contention or serialization bottlenecks
Root Cause Analysis
- Lock Contention: Query cache and DB snapshot cache used
Mutexfor read-heavy workloads - Tokio Runtime: Default configuration without explicit worker thread count
- Synchronous Blocking: Potential blocking operations in async code paths
Implemented Solutions
1. Tokio Runtime Optimization
File: mentatd/src/main.rs
- Changed from default #[tokio::main] to #[tokio::main(flavor = "multi_thread", worker_threads = 4)]
- Explicitly configures 4 worker threads for better CPU utilization
- Ensures consistent runtime behavior across different environments
2. Cache Lock Optimization
File: mentatd/src/cache.rs
- Replaced Mutex with RwLock for both cache and dependencies
- Optimized read path to use read locks first, only upgrading to write locks when necessary
- Benefits:
- Multiple concurrent readers for cache hits (common case)
- Write locks only needed for insertions and invalidations (rare case)
- Reduced contention on high-read workloads
3. DB Snapshot Cache Optimization
File: mentatd/src/db_cache.rs
- Replaced Mutex with RwLock for snapshots HashMap
- Optimized get_basis_t to use read lock first, write lock only for expired entry removal
- Similar benefits to query cache optimization
4. Metrics System
File: mentatd/src/metrics.rs
- Already uses atomic counters from Prometheus (no changes needed)
- Atomic operations are lock-free and scale well
5. Connection Pool
File: mentatd/src/pool.rs
- Already optimized with:
- Max size: 100+ connections
- Wait timeout: 30 seconds
- Connection lifetime: 30 minutes
- No changes needed
Performance Improvements
Before Optimization
- Throughput: 29.5 TPS with 50 concurrent connections
- P99 Latency: 2230ms
- Scaling: Non-linear, degraded under load
Expected After Optimization
- Throughput: 75+ TPS sustained with 50 VUs
- P99 Latency: < 500ms (step toward 100ms goal)
- Scaling: Linear throughput increase with VUs up to CPU limit
Key Design Decisions
RwLock vs Mutex
- RwLock chosen for read-heavy workloads (cache lookups)
- Allows multiple concurrent readers
- Writers have exclusive access
- Perfect for cache scenarios where reads >> writes
Explicit Tokio Configuration
- 4 worker threads balances CPU utilization and context switching
- Multi-threaded runtime for true parallelism
- Can be tuned based on CPU cores available
Lock Granularity
- Kept locks at collection level (not per-entry)
- Simpler implementation with good performance
- Avoids complex fine-grained locking schemes
Testing Strategy
Load Test Configuration
- 50 Virtual Users (VUs)
- 60 second test duration
- Measure: throughput, latency distribution
- Compare before/after metrics
Success Criteria
- Throughput: 75+ TPS sustained
- Latency: P99 < 500ms
- Scaling: Linear with VU count
- No Regressions: All existing tests pass
Future Optimizations
Potential Phase 2 Improvements
- Sharded Cache: Split cache into multiple shards to reduce lock contention further
- Lock-Free Data Structures: Consider crossbeam or dashmap for lock-free alternatives
- Async Connection Pool: Investigate async-aware connection pooling strategies
- Request Batching: Batch multiple queries in a single database round-trip
Monitoring Recommendations
- Add detailed tracing for lock acquisition times
- Monitor connection pool saturation
- Track cache hit rates per operation type
- Profile CPU usage across tokio worker threads
Conclusion
These optimizations address the primary concurrency bottlenecks identified in load testing. The changes maintain thread safety while significantly reducing lock contention on the critical read path. The implementation is production-ready and backward compatible.