ricecoder_external_lsp/process/
pool.rs

1//! Client pool management
2
3use crate::error::Result;
4use crate::types::LspServerConfig;
5use std::collections::HashMap;
6use std::sync::Arc;
7use std::time::{Duration, Instant};
8use tokio::sync::RwLock;
9use tracing::{debug, info};
10
11/// Information about a pooled client
12#[derive(Clone)]
13struct PooledClient {
14    /// Configuration for this client
15    config: LspServerConfig,
16    /// Last access time
17    last_access: Instant,
18    /// Number of active references
19    ref_count: usize,
20}
21
22/// Manages a pool of LSP client connections
23pub struct ClientPool {
24    /// Map of language to pooled clients
25    clients: Arc<RwLock<HashMap<String, PooledClient>>>,
26    /// Maximum concurrent processes
27    max_processes: usize,
28    /// Idle timeout for clients
29    idle_timeout: Duration,
30}
31
32impl ClientPool {
33    /// Create a new client pool
34    pub fn new(max_processes: usize, idle_timeout: Duration) -> Self {
35        Self {
36            clients: Arc::new(RwLock::new(HashMap::new())),
37            max_processes,
38            idle_timeout,
39        }
40    }
41
42    /// Get or create a client for a language
43    pub async fn get_or_create(&self, config: LspServerConfig) -> Result<Arc<LspServerConfig>> {
44        let mut clients = self.clients.write().await;
45
46        // Check if we already have a client for this language
47        if let Some(client) = clients.get_mut(&config.language) {
48            client.last_access = Instant::now();
49            client.ref_count += 1;
50            debug!(
51                language = %config.language,
52                ref_count = client.ref_count,
53                "Reusing existing LSP client"
54            );
55            return Ok(Arc::new(client.config.clone()));
56        }
57
58        // Check if we can create a new client
59        if clients.len() >= self.max_processes {
60            debug!(
61                language = %config.language,
62                max_processes = self.max_processes,
63                "Client pool at capacity, attempting to reuse idle client"
64            );
65
66            // Try to find and remove an idle client
67            if let Some(idle_language) = self.find_idle_client(&clients) {
68                clients.remove(&idle_language);
69                info!(
70                    language = %idle_language,
71                    "Removed idle LSP client to make room"
72                );
73            } else {
74                return Err(crate::error::ExternalLspError::ProtocolError(
75                    format!(
76                        "Client pool at capacity ({}) and no idle clients to remove",
77                        self.max_processes
78                    ),
79                ));
80            }
81        }
82
83        // Create a new client
84        let pooled = PooledClient {
85            config: config.clone(),
86            last_access: Instant::now(),
87            ref_count: 1,
88        };
89
90        clients.insert(config.language.clone(), pooled);
91        info!(
92            language = %config.language,
93            pool_size = clients.len(),
94            "Created new LSP client in pool"
95        );
96
97        Ok(Arc::new(config))
98    }
99
100    /// Release a client reference
101    pub async fn release(&self, language: &str) {
102        let mut clients = self.clients.write().await;
103
104        if let Some(client) = clients.get_mut(language) {
105            if client.ref_count > 0 {
106                client.ref_count -= 1;
107                client.last_access = Instant::now();
108                debug!(
109                    language = language,
110                    ref_count = client.ref_count,
111                    "Released LSP client reference"
112                );
113            }
114        }
115    }
116
117    /// Get the number of active clients
118    pub async fn active_count(&self) -> usize {
119        let clients = self.clients.read().await;
120        clients.len()
121    }
122
123    /// Get the number of clients with active references
124    pub async fn referenced_count(&self) -> usize {
125        let clients = self.clients.read().await;
126        clients.values().filter(|c| c.ref_count > 0).count()
127    }
128
129    /// Clean up idle clients
130    pub async fn cleanup_idle(&self) -> usize {
131        let mut clients = self.clients.write().await;
132        let now = Instant::now();
133
134        let idle_languages: Vec<String> = clients
135            .iter()
136            .filter(|(_, client)| {
137                client.ref_count == 0 && now.duration_since(client.last_access) > self.idle_timeout
138            })
139            .map(|(lang, _)| lang.clone())
140            .collect();
141
142        for language in &idle_languages {
143            clients.remove(language);
144            debug!(
145                language = language,
146                "Cleaned up idle LSP client"
147            );
148        }
149
150        idle_languages.len()
151    }
152
153    /// Find an idle client to remove
154    fn find_idle_client(&self, clients: &HashMap<String, PooledClient>) -> Option<String> {
155        let now = Instant::now();
156
157        clients
158            .iter()
159            .filter(|(_, client)| {
160                client.ref_count == 0 && now.duration_since(client.last_access) > self.idle_timeout
161            })
162            .min_by_key(|(_, client)| client.last_access)
163            .map(|(lang, _)| lang.clone())
164    }
165
166    /// Clear all clients
167    pub async fn clear(&self) {
168        let mut clients = self.clients.write().await;
169        let count = clients.len();
170        clients.clear();
171        info!(
172            count = count,
173            "Cleared all LSP clients from pool"
174        );
175    }
176}
177
178impl Default for ClientPool {
179    fn default() -> Self {
180        Self::new(5, Duration::from_secs(300))
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    fn create_test_config(language: &str) -> LspServerConfig {
189        LspServerConfig {
190            language: language.to_string(),
191            extensions: vec![],
192            executable: format!("{}-lsp", language),
193            args: vec![],
194            env: Default::default(),
195            init_options: None,
196            enabled: true,
197            timeout_ms: 5000,
198            max_restarts: 3,
199            idle_timeout_ms: 300000,
200            output_mapping: None,
201        }
202    }
203
204    #[tokio::test]
205    async fn test_client_pool_creation() {
206        let pool = ClientPool::new(5, Duration::from_secs(300));
207        assert_eq!(pool.active_count().await, 0);
208    }
209
210    #[tokio::test]
211    async fn test_get_or_create_client() {
212        let pool = ClientPool::new(5, Duration::from_secs(300));
213        let config = create_test_config("rust");
214
215        let client = pool.get_or_create(config).await.unwrap();
216        assert_eq!(client.language, "rust");
217        assert_eq!(pool.active_count().await, 1);
218    }
219
220    #[tokio::test]
221    async fn test_reuse_existing_client() {
222        let pool = ClientPool::new(5, Duration::from_secs(300));
223        let config = create_test_config("rust");
224
225        let _client1 = pool.get_or_create(config.clone()).await.unwrap();
226        let _client2 = pool.get_or_create(config).await.unwrap();
227
228        // Should still have only 1 client
229        assert_eq!(pool.active_count().await, 1);
230    }
231
232    #[tokio::test]
233    async fn test_pool_capacity() {
234        let pool = ClientPool::new(2, Duration::from_secs(300));
235
236        let config1 = create_test_config("rust");
237        let config2 = create_test_config("typescript");
238        let config3 = create_test_config("python");
239
240        let _client1 = pool.get_or_create(config1).await.unwrap();
241        let _client2 = pool.get_or_create(config2).await.unwrap();
242
243        // Third client should fail (pool at capacity)
244        let result = pool.get_or_create(config3).await;
245        assert!(result.is_err());
246    }
247
248    #[tokio::test]
249    async fn test_release_client() {
250        let pool = ClientPool::new(5, Duration::from_secs(300));
251        let config = create_test_config("rust");
252
253        let _client = pool.get_or_create(config).await.unwrap();
254        assert_eq!(pool.referenced_count().await, 1);
255
256        pool.release("rust").await;
257        assert_eq!(pool.referenced_count().await, 0);
258    }
259
260    #[tokio::test]
261    async fn test_clear_pool() {
262        let pool = ClientPool::new(5, Duration::from_secs(300));
263        let config1 = create_test_config("rust");
264        let config2 = create_test_config("typescript");
265
266        let _client1 = pool.get_or_create(config1).await.unwrap();
267        let _client2 = pool.get_or_create(config2).await.unwrap();
268
269        assert_eq!(pool.active_count().await, 2);
270
271        pool.clear().await;
272        assert_eq!(pool.active_count().await, 0);
273    }
274}