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}