Skip to main content

ryo_suggest/
intent_lock.rs

1//! IntentLock - Dirty read prevention for concurrent LLM operations
2//!
3//! When an Intent is being executed, other LLM agents should not read stale state
4//! or queue conflicting operations. This prevents dirty reads and ensures consistency.
5//!
6//! # Design Overview
7//!
8//! ```text
9//! ┌─────────────────────────────────────────────────────────────────┐
10//! │                     IntentLock Flow                              │
11//! ├─────────────────────────────────────────────────────────────────┤
12//! │                                                                  │
13//! │  LLM Agent 1                    LLM Agent 2                      │
14//! │       │                              │                           │
15//! │       │ try_acquire(symbols)         │                           │
16//! │       ▼                              │                           │
17//! │  ┌─────────┐                         │                           │
18//! │  │ LOCKED  │◄────────────────────────┤ try_acquire(same symbols) │
19//! │  │SymbolA  │                         │      → Blocked!           │
20//! │  │SymbolB  │                         │                           │
21//! │  └────┬────┘                         │                           │
22//! │       │                              │                           │
23//! │       │ execute mutation             │                           │
24//! │       │                              │                           │
25//! │       │ drop(guard)                  │                           │
26//! │       ▼                              ▼                           │
27//! │  ┌─────────┐                    ┌─────────┐                      │
28//! │  │UNLOCKED │                    │ LOCKED  │ ← Now can acquire    │
29//! │  └─────────┘                    └─────────┘                      │
30//! │                                                                  │
31//! └─────────────────────────────────────────────────────────────────┘
32//! ```
33//!
34//! # Usage
35//!
36//! ```ignore
37//! use ryo_suggest::intent_lock::{IntentLock, IntentId};
38//!
39//! let lock = IntentLock::new();
40//!
41//! // Try to acquire lock for symbols
42//! let guard = lock.try_acquire(IntentId::new(), &target_symbols)?;
43//!
44//! // Execute with lock held
45//! executor.execute(mutation).await?;
46//!
47//! // Lock released when guard drops
48//! drop(guard);
49//! ```
50//!
51//! # Integration with SuggestService
52//!
53//! When querying suggestions, the IntentLock should be consulted to filter out
54//! suggestions targeting locked symbols:
55//!
56//! ```ignore
57//! impl SuggestService {
58//!     pub fn top_unlocked(
59//!         &self,
60//!         n: usize,
61//!         intent_lock: &impl IntentLockQuery,
62//!     ) -> Vec<RankedSuggestion> {
63//!         self.store.read()
64//!             .iter()
65//!             .filter(|(_, sug)| !intent_lock.is_locked(&sug.opportunity.targets))
66//!             .take(n)
67//!             .collect()
68//!     }
69//! }
70//! ```
71//!
72//! # Implementation Notes
73//!
74//! This module provides traits for IntentLock. The actual implementation lives
75//! in the server layer (ryo-server) where the full concurrent execution context
76//! is available.
77
78use ryo_analysis::SymbolId;
79use serde::{Deserialize, Serialize};
80use std::fmt;
81
82/// Unique identifier for an Intent (mutation request)
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
84pub struct IntentId(pub u64);
85
86impl IntentId {
87    /// Create a new IntentId
88    pub fn new(id: u64) -> Self {
89        Self(id)
90    }
91
92    /// Get the raw ID value
93    pub fn as_u64(self) -> u64 {
94        self.0
95    }
96}
97
98impl fmt::Display for IntentId {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        write!(f, "I{:06}", self.0)
101    }
102}
103
104/// Error when acquiring an IntentLock
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum LockError {
107    /// Lock acquisition blocked by another intent
108    Blocked {
109        /// The symbol that is locked
110        symbol: SymbolId,
111        /// The intent holding the lock
112        blocking_intent: IntentId,
113    },
114}
115
116impl fmt::Display for LockError {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        match self {
119            Self::Blocked {
120                symbol,
121                blocking_intent,
122            } => {
123                write!(
124                    f,
125                    "Symbol {:?} is locked by intent {}",
126                    symbol, blocking_intent
127                )
128            }
129        }
130    }
131}
132
133impl std::error::Error for LockError {}
134
135/// Query interface for checking lock status
136///
137/// This trait allows checking if symbols are locked without requiring
138/// write access to the lock state.
139pub trait IntentLockQuery: Send + Sync {
140    /// Check if any of the given symbols are currently locked
141    fn is_locked(&self, symbols: &[SymbolId]) -> bool;
142
143    /// Get the intent holding the lock on a symbol (if any)
144    fn locked_by(&self, symbol: &SymbolId) -> Option<IntentId>;
145
146    /// Get all currently locked symbols
147    fn locked_symbols(&self) -> Vec<SymbolId>;
148}
149
150/// Full interface for IntentLock operations
151///
152/// Extends IntentLockQuery with mutation capabilities.
153/// Implementations should use interior mutability (e.g., RwLock)
154/// to allow concurrent query access.
155pub trait IntentLockOps: IntentLockQuery {
156    /// Guard type returned when lock is acquired
157    type Guard<'a>
158    where
159        Self: 'a;
160
161    /// Try to acquire locks for the given symbols
162    ///
163    /// Returns:
164    /// - `Ok(guard)` if all symbols were successfully locked
165    /// - `Err(LockError::Blocked)` if any symbol is already locked
166    ///
167    /// The guard should release the locks when dropped.
168    fn try_acquire(
169        &self,
170        intent_id: IntentId,
171        symbols: &[SymbolId],
172    ) -> Result<Self::Guard<'_>, LockError>;
173
174    /// Force release all locks for an intent (for cleanup/timeout)
175    fn force_release(&self, intent_id: IntentId);
176}
177
178/// A no-op implementation for single-threaded or testing contexts
179#[derive(Debug, Default)]
180pub struct NoOpIntentLock;
181
182impl IntentLockQuery for NoOpIntentLock {
183    fn is_locked(&self, _symbols: &[SymbolId]) -> bool {
184        false
185    }
186
187    fn locked_by(&self, _symbol: &SymbolId) -> Option<IntentId> {
188        None
189    }
190
191    fn locked_symbols(&self) -> Vec<SymbolId> {
192        vec![]
193    }
194}
195
196impl IntentLockOps for NoOpIntentLock {
197    type Guard<'a> = ();
198
199    fn try_acquire(
200        &self,
201        _intent_id: IntentId,
202        _symbols: &[SymbolId],
203    ) -> Result<Self::Guard<'_>, LockError> {
204        Ok(())
205    }
206
207    fn force_release(&self, _intent_id: IntentId) {}
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_intent_id_display() {
216        let id = IntentId::new(42);
217        assert_eq!(id.to_string(), "I000042");
218    }
219
220    #[test]
221    fn test_lock_error_display() {
222        let err = LockError::Blocked {
223            symbol: SymbolId::parse("100v1").unwrap(),
224            blocking_intent: IntentId::new(1),
225        };
226        assert!(err.to_string().contains("locked by intent"));
227    }
228
229    #[test]
230    fn test_noop_lock() {
231        let lock = NoOpIntentLock;
232        let sym = SymbolId::parse("100v1").unwrap();
233
234        assert!(!lock.is_locked(&[sym]));
235        assert!(lock.try_acquire(IntentId::new(1), &[sym]).is_ok());
236    }
237}