ricecoder_external_lsp/process/
pool.rs1use 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#[derive(Clone)]
13struct PooledClient {
14 config: LspServerConfig,
16 last_access: Instant,
18 ref_count: usize,
20}
21
22pub struct ClientPool {
24 clients: Arc<RwLock<HashMap<String, PooledClient>>>,
26 max_processes: usize,
28 idle_timeout: Duration,
30}
31
32impl ClientPool {
33 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 pub async fn get_or_create(&self, config: LspServerConfig) -> Result<Arc<LspServerConfig>> {
44 let mut clients = self.clients.write().await;
45
46 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 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 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 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 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 pub async fn active_count(&self) -> usize {
119 let clients = self.clients.read().await;
120 clients.len()
121 }
122
123 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 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 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 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 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 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}