1use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use serde_json::Map;
8use tokio::sync::Mutex;
9
10use crate::client::Client;
11use crate::errors::{Error, Result};
12use crate::session::{bootstrap_loop_session, connect_with_retries, BootstrapOptions};
13
14use super::session_store::{SessionRecord, SessionStore};
15
16#[derive(Debug, Clone)]
18pub struct PoolConfig {
19 pub pool_size: usize,
21 pub query_timeout: Duration,
23 pub connection_timeout: Duration,
25 pub max_idle_time: Duration,
27}
28
29impl Default for PoolConfig {
30 fn default() -> Self {
31 Self {
32 pool_size: 1000,
33 query_timeout: Duration::from_secs(30 * 60),
34 connection_timeout: Duration::from_secs(30),
35 max_idle_time: Duration::from_secs(10 * 60),
36 }
37 }
38}
39
40#[derive(Debug, Clone, Default)]
42pub struct PoolStats {
43 pub active: usize,
45 pub idle: usize,
47}
48
49pub struct PooledConn {
51 pub slot_id: String,
53 pub session_id: String,
55 pub loop_id: String,
57 pub client: Client,
59 last_used: Instant,
60}
61
62impl PooledConn {
63 pub fn is_connected(&self) -> bool {
65 self.client.is_connected()
66 }
67
68 pub fn get_loop_id(&self) -> &str {
70 &self.loop_id
71 }
72}
73
74struct PoolInner {
75 idle: HashMap<String, PooledConn>,
76 active: HashMap<String, PooledConn>,
77}
78
79pub struct ConnectionPool<S: SessionStore> {
81 daemon_url: String,
82 store: Arc<S>,
83 cfg: PoolConfig,
84 inner: Mutex<PoolInner>,
85}
86
87impl<S: SessionStore + 'static> ConnectionPool<S> {
88 pub fn new(daemon_url: impl Into<String>, store: Arc<S>, cfg: Option<PoolConfig>) -> Self {
90 Self {
91 daemon_url: daemon_url.into(),
92 store,
93 cfg: cfg.unwrap_or_default(),
94 inner: Mutex::new(PoolInner {
95 idle: HashMap::new(),
96 active: HashMap::new(),
97 }),
98 }
99 }
100
101 pub async fn stats(&self) -> PoolStats {
103 let inner = self.inner.lock().await;
104 PoolStats {
105 active: inner.active.len(),
106 idle: inner.idle.len(),
107 }
108 }
109
110 pub async fn acquire(
112 &self,
113 session_id: &str,
114 workspace_id: &str,
115 user_id: &str,
116 ) -> Result<PooledConn> {
117 {
118 let mut inner = self.inner.lock().await;
119 if let Some(mut conn) = inner.idle.remove(session_id) {
120 if conn.last_used.elapsed() > self.cfg.max_idle_time
121 || !conn.client.is_connection_alive()
122 {
123 let _ = conn.client.close().await;
124 } else {
125 conn.last_used = Instant::now();
126 let out = PooledConn {
127 slot_id: conn.slot_id.clone(),
128 session_id: conn.session_id.clone(),
129 loop_id: conn.loop_id.clone(),
130 client: conn.client.clone(),
131 last_used: conn.last_used,
132 };
133 inner.active.insert(session_id.to_string(), conn);
134 return Ok(out);
135 }
136 }
137 if inner.active.len() + inner.idle.len() >= self.cfg.pool_size {
138 return Err(Error::msg("pool exhausted"));
139 }
140 }
141
142 if self.store.get_session(session_id).await.is_none() {
144 self.store
145 .create_session(SessionRecord {
146 session_id: session_id.to_string(),
147 workspace_id: workspace_id.to_string(),
148 user_id: user_id.to_string(),
149 ..Default::default()
150 })
151 .await;
152 }
153
154 let client = Client::new(&self.daemon_url);
155 connect_with_retries(&client, 40, Duration::from_millis(250)).await?;
156
157 let loop_id = if let Some(existing) = self.store.get_loop_id_for_session(session_id).await {
158 match client.reattach_and_probe(&existing).await {
159 Ok(()) => existing,
160 Err(_) => {
161 let mut boot = BootstrapOptions::new();
162 boot.workspace = Some(workspace_id.to_string());
163 boot.user_id = Some(user_id.to_string());
164 let ready = bootstrap_loop_session(&client, boot, None).await?;
165 ready
166 .get("loop_id")
167 .and_then(|v| v.as_str())
168 .unwrap_or("")
169 .to_string()
170 }
171 }
172 } else {
173 let mut boot = BootstrapOptions::new();
174 boot.workspace = Some(workspace_id.to_string());
175 boot.user_id = Some(user_id.to_string());
176 let ready = bootstrap_loop_session(&client, boot, None).await?;
177 ready
178 .get("loop_id")
179 .and_then(|v| v.as_str())
180 .unwrap_or("")
181 .to_string()
182 };
183
184 if loop_id.is_empty() {
185 return Err(Error::msg("failed to bootstrap pooled loop"));
186 }
187 self.store.set_loop_id(session_id, &loop_id).await;
188 self.store.update_last_used(session_id).await;
189
190 let conn = PooledConn {
191 slot_id: format!("{session_id}:{loop_id}"),
192 session_id: session_id.to_string(),
193 loop_id,
194 client,
195 last_used: Instant::now(),
196 };
197 let out = PooledConn {
198 slot_id: conn.slot_id.clone(),
199 session_id: conn.session_id.clone(),
200 loop_id: conn.loop_id.clone(),
201 client: conn.client.clone(),
202 last_used: conn.last_used,
203 };
204 self.inner
205 .lock()
206 .await
207 .active
208 .insert(session_id.to_string(), conn);
209 Ok(out)
210 }
211
212 pub async fn release(&self, session_id: &str) {
214 let mut inner = self.inner.lock().await;
215 if let Some(mut conn) = inner.active.remove(session_id) {
216 conn.last_used = Instant::now();
217 inner.idle.insert(session_id.to_string(), conn);
218 }
219 }
220
221 pub async fn reset_session(&self, session_id: &str) -> Result<()> {
223 {
224 let mut inner = self.inner.lock().await;
225 if let Some(conn) = inner.active.remove(session_id) {
226 let _ = conn.client.close().await;
227 }
228 if let Some(conn) = inner.idle.remove(session_id) {
229 let _ = conn.client.close().await;
230 }
231 }
232 self.store.increment_reset_count(session_id).await;
233 self.store.set_loop_id(session_id, "").await;
234 Ok(())
235 }
236
237 pub async fn stop(&self) {
239 let mut inner = self.inner.lock().await;
240 let mut conns: Vec<PooledConn> = inner.active.drain().map(|(_, c)| c).collect();
241 conns.extend(inner.idle.drain().map(|(_, c)| c));
242 drop(inner);
243 for conn in conns {
244 let _ = conn.client.close().await;
245 }
246 }
247}
248
249pub fn input_message_for_loop(
251 text: &str,
252 loop_id: &str,
253 attachments: Option<serde_json::Value>,
254 opts: Option<&super::turn_runner::InputOpts>,
255) -> Map<String, serde_json::Value> {
256 use serde_json::json;
257 let mut m = Map::new();
258 m.insert("type".into(), json!("loop_input"));
259 m.insert("content".into(), json!(text));
260 m.insert("loop_id".into(), json!(loop_id));
261 if let Some(a) = attachments {
262 m.insert("attachments".into(), a);
263 }
264 if let Some(o) = opts {
265 if let Some(h) = &o.intent_hint {
266 m.insert("intent_hint".into(), json!(h));
267 }
268 if let Some(s) = &o.preferred_subagent {
269 m.insert("preferred_subagent".into(), json!(s));
270 }
271 if let Some(schema) = &o.response_schema {
272 m.insert("response_schema".into(), schema.clone());
273 }
274 }
275 m
276}