1use std::collections::HashMap;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::Arc;
8use std::time::{Duration, Instant};
9
10use serde_json::{json, Map, Value};
11use tokio::sync::Mutex;
12use tokio::task::JoinHandle;
13
14use crate::client::Client;
15use crate::errors::{Error, Result};
16use crate::protocol::new_notification;
17use crate::session::{bootstrap_loop_session, BootstrapOptions};
18
19use super::session_store::{SessionRecord, SessionStore};
20
21#[derive(Debug, Clone, thiserror::Error)]
23#[error("appkit: connection pool exhausted")]
24pub struct ErrPoolExhausted;
25
26#[derive(Debug, Clone)]
28pub struct PoolConfig {
29 pub pool_size: usize,
31 pub max_idle_time: Duration,
33}
34
35impl Default for PoolConfig {
36 fn default() -> Self {
37 Self {
38 pool_size: 1000,
39 max_idle_time: Duration::from_secs(10 * 60),
40 }
41 }
42}
43
44#[derive(Debug, Clone, Default)]
46pub struct PoolStats {
47 pub active: usize,
49 pub idle: usize,
51}
52
53pub struct PooledConn {
55 pub slot_id: u32,
57 pub session_id: String,
59 pub loop_id: Mutex<String>,
61 pub workspace_id: String,
63 pub client: Client,
65 pub event_rx: Mutex<Option<tokio::sync::mpsc::Receiver<Value>>>,
67 pub reader_live: AtomicBool,
69 pub last_used: Mutex<Instant>,
71 reader_task: Mutex<Option<JoinHandle<()>>>,
72}
73
74impl PooledConn {
75 pub async fn get_loop_id(&self) -> String {
77 self.loop_id.lock().await.clone()
78 }
79
80 pub async fn is_connected(&self) -> bool {
82 self.client.is_connected() && !Self::client_disconnect_notified(&self.client).await
83 }
84
85 pub async fn event_stream_live(&self) -> bool {
87 let has_rx = self.event_rx.lock().await.is_some();
88 has_rx && self.reader_live.load(Ordering::SeqCst)
89 }
90
91 async fn client_disconnect_notified(client: &Client) -> bool {
92 client.disconnect_cause().await.is_some() || !client.is_connection_alive()
93 }
94}
95
96pub type PooledConnHandle = Arc<PooledConn>;
98
99type ClientFactory = Arc<dyn Fn(&str) -> Client + Send + Sync>;
100type BootstrapFn = Arc<
101 dyn Fn(Client, String, String, String) -> Pin<Box<dyn Future<Output = Result<String>> + Send>>
102 + Send
103 + Sync,
104>;
105
106struct IdleSlot {
107 client: Client,
108}
109
110struct PoolState {
111 active_slots: HashMap<String, PooledConnHandle>,
112 next_slot_id: u32,
113 idle_rx: tokio::sync::mpsc::Receiver<IdleSlot>,
114 idle_tx: tokio::sync::mpsc::Sender<IdleSlot>,
115 idle_count: usize,
116}
117
118pub struct ConnectionPool<S: SessionStore> {
120 daemon_url: String,
121 store: Arc<S>,
122 cfg: PoolConfig,
123 factory: ClientFactory,
124 bootstrap: BootstrapFn,
125 state: Mutex<PoolState>,
126}
127
128impl<S: SessionStore + 'static> ConnectionPool<S> {
129 pub fn new(daemon_url: impl Into<String>, store: Arc<S>, cfg: Option<PoolConfig>) -> Self {
131 let cfg = cfg.unwrap_or_default();
132 let pool_size = if cfg.pool_size == 0 {
133 PoolConfig::default().pool_size
134 } else {
135 cfg.pool_size
136 };
137 let daemon_url = daemon_url.into();
138 let factory: ClientFactory = Arc::new(|url| Client::new(url));
139 let bootstrap = default_bootstrap_fn();
140 let (idle_tx, idle_rx) = tokio::sync::mpsc::channel(pool_size);
141 for _ in 0..pool_size {
142 let _ = idle_tx.try_send(IdleSlot {
143 client: factory(&daemon_url),
144 });
145 }
146 Self {
147 daemon_url,
148 store,
149 cfg: PoolConfig { pool_size, ..cfg },
150 factory,
151 bootstrap,
152 state: Mutex::new(PoolState {
153 active_slots: HashMap::new(),
154 next_slot_id: 1,
155 idle_rx,
156 idle_tx,
157 idle_count: pool_size,
158 }),
159 }
160 }
161
162 pub fn with_client_factory(mut self, f: Box<dyn Fn(&str) -> Client + Send + Sync>) -> Self {
164 self.factory = Arc::from(f);
165 self
166 }
167
168 pub fn with_bootstrap(
170 mut self,
171 f: impl Fn(
172 Client,
173 String,
174 String,
175 String,
176 ) -> Pin<Box<dyn Future<Output = Result<String>> + Send>>
177 + Send
178 + Sync
179 + 'static,
180 ) -> Self {
181 self.bootstrap = Arc::new(f);
182 self
183 }
184
185 pub async fn stats(&self) -> PoolStats {
187 let state = self.state.lock().await;
188 PoolStats {
189 active: state.active_slots.len(),
190 idle: state.idle_count,
191 }
192 }
193
194 pub async fn acquire(
199 &self,
200 session_id: &str,
201 workspace_id: &str,
202 user_id: &str,
203 ) -> Result<PooledConnHandle> {
204 if let Some(existing) = {
206 let state = self.state.lock().await;
207 state.active_slots.get(session_id).cloned()
208 } {
209 let disconnect = PooledConn::client_disconnect_notified(&existing.client).await
210 || !existing.is_connected().await
211 || !existing.event_stream_live().await;
212 if disconnect {
213 tracing::warn!(
214 session_id,
215 "previous connection dropped or event stream dead, releasing for fresh bootstrap"
216 );
217 self.release(session_id).await;
218 } else {
219 let idle_too_long = self.cfg.max_idle_time > Duration::ZERO
220 && existing.last_used.lock().await.elapsed() > self.cfg.max_idle_time;
221 if idle_too_long {
222 tracing::warn!(
223 session_id,
224 max_idle = ?self.cfg.max_idle_time,
225 "session idle beyond max, releasing"
226 );
227 self.release(session_id).await;
228 } else {
229 *existing.last_used.lock().await = Instant::now();
230 self.store.update_last_used(session_id).await;
231 return Ok(existing);
232 }
233 }
234 }
235
236 let idle = {
238 let mut state = self.state.lock().await;
239 match state.idle_rx.try_recv() {
240 Ok(slot) => {
241 state.idle_count = state.idle_count.saturating_sub(1);
242 slot
243 }
244 Err(_) => return Err(Error::from(ErrPoolExhausted)),
245 }
246 };
247
248 let slot_id = {
249 let mut state = self.state.lock().await;
250 let id = state.next_slot_id;
251 state.next_slot_id += 1;
252 id
253 };
254
255 let conn = Arc::new(PooledConn {
256 slot_id,
257 session_id: session_id.to_string(),
258 loop_id: Mutex::new(String::new()),
259 workspace_id: workspace_id.to_string(),
260 client: idle.client,
261 event_rx: Mutex::new(None),
262 reader_live: AtomicBool::new(false),
263 last_used: Mutex::new(Instant::now()),
264 reader_task: Mutex::new(None),
265 });
266
267 let (stored_loop_id, has_loop) = self.loop_id_for(session_id).await;
268 let loop_id = if !has_loop || stored_loop_id.is_empty() {
269 conn.client
270 .connect()
271 .await
272 .map_err(|e| Error::msg(format!("connect: {e}")))?;
273 let loop_id = match self
274 .bootstrap_new(Arc::clone(&conn), session_id, workspace_id, user_id)
275 .await
276 {
277 Ok(id) => id,
278 Err(e) => {
279 self.release(session_id).await;
280 return Err(Error::msg(format!("bootstrap new loop: {e}")));
281 }
282 };
283 if let Err(e) = self
284 .persist_new_session(session_id, workspace_id, user_id, &loop_id)
285 .await
286 {
287 tracing::warn!(session_id, error = %e, "create session failed");
288 }
289 loop_id
290 } else if self
291 .resume_and_reattach(Arc::clone(&conn), &stored_loop_id)
292 .await
293 .is_err()
294 {
295 tracing::warn!(session_id, "reattach failed, bootstrapping fresh");
296 if let Err(e) = conn.client.connect().await {
297 self.release(session_id).await;
298 return Err(Error::msg(format!("connect after reattach fail: {e}")));
299 }
300 let loop_id = match self
301 .bootstrap_new(Arc::clone(&conn), session_id, workspace_id, user_id)
302 .await
303 {
304 Ok(id) => id,
305 Err(e) => {
306 self.release(session_id).await;
307 return Err(Error::msg(format!("bootstrap after reattach fail: {e}")));
308 }
309 };
310 if let Err(e) = self
311 .persist_new_session(session_id, workspace_id, user_id, &loop_id)
312 .await
313 {
314 tracing::warn!(session_id, error = %e, "create session after bootstrap failed");
315 }
316 loop_id
317 } else {
318 stored_loop_id
319 };
320
321 *conn.loop_id.lock().await = loop_id.clone();
322 *conn.last_used.lock().await = Instant::now();
323 self.store.update_last_used(session_id).await;
324
325 {
326 let mut state = self.state.lock().await;
327 state
328 .active_slots
329 .insert(session_id.to_string(), Arc::clone(&conn));
330 }
331
332 tracing::info!(slot_id, session_id, loop_id, "acquired pool slot");
333 Ok(conn)
334 }
335
336 pub async fn release(&self, session_id: &str) {
338 let conn = {
339 let mut state = self.state.lock().await;
340 state.active_slots.remove(session_id)
341 };
342 let Some(conn) = conn else {
343 return;
344 };
345
346 if let Some(handle) = conn.reader_task.lock().await.take() {
347 handle.abort();
348 }
349 conn.reader_live.store(false, Ordering::SeqCst);
350 *conn.event_rx.lock().await = None;
351 let _ = conn.client.close().await;
352
353 tracing::info!(slot_id = conn.slot_id, session_id, "released pool slot");
354
355 let new_slot = IdleSlot {
356 client: (self.factory)(&self.daemon_url),
357 };
358 let mut state = self.state.lock().await;
359 if state.idle_tx.try_send(new_slot).is_ok() {
360 state.idle_count += 1;
361 } else {
362 tracing::warn!(session_id, "pool full when returning slot");
363 }
364 }
365
366 pub async fn reset_session(&self, session_id: &str) {
368 self.release(session_id).await;
369 tracing::info!(
370 session_id,
371 "reset session — next message will create new loop"
372 );
373 }
374
375 pub async fn stop(&self) {
377 let active: Vec<PooledConnHandle> = {
378 let mut state = self.state.lock().await;
379 state.active_slots.drain().map(|(_, c)| c).collect()
380 };
381 for conn in active {
382 if let Some(handle) = conn.reader_task.lock().await.take() {
383 handle.abort();
384 }
385 let _ = conn.client.close().await;
386 }
387 loop {
388 let slot = {
389 let mut state = self.state.lock().await;
390 match state.idle_rx.try_recv() {
391 Ok(s) => {
392 state.idle_count = state.idle_count.saturating_sub(1);
393 Some(s)
394 }
395 Err(_) => None,
396 }
397 };
398 match slot {
399 Some(s) => {
400 let _ = s.client.close().await;
401 }
402 None => break,
403 }
404 }
405 }
406
407 async fn loop_id_for(&self, session_id: &str) -> (String, bool) {
408 let Some(loop_id) = self.store.get_loop_id_for_session(session_id).await else {
409 return (String::new(), false);
410 };
411 let loop_id = loop_id.trim().to_string();
412 if loop_id.is_empty() || loop_id.starts_with("pending-") {
413 return (String::new(), false);
414 }
415 (loop_id, true)
416 }
417
418 async fn persist_new_session(
419 &self,
420 session_id: &str,
421 workspace_id: &str,
422 user_id: &str,
423 loop_id: &str,
424 ) -> Result<()> {
425 if self.store.get_session(session_id).await.is_none() {
426 self.store
427 .create_session(SessionRecord {
428 session_id: session_id.to_string(),
429 workspace_id: workspace_id.to_string(),
430 user_id: user_id.to_string(),
431 loop_id: Some(loop_id.to_string()),
432 ..Default::default()
433 })
434 .await;
435 } else {
436 self.store.set_loop_id(session_id, loop_id).await;
437 }
438 Ok(())
439 }
440
441 async fn bootstrap_new(
442 &self,
443 conn: PooledConnHandle,
444 session_id: &str,
445 workspace_id: &str,
446 user_id: &str,
447 ) -> Result<String> {
448 let loop_id = (self.bootstrap)(
449 conn.client.clone(),
450 session_id.to_string(),
451 workspace_id.to_string(),
452 user_id.to_string(),
453 )
454 .await?;
455 self.start_reader(&conn).await;
456 Ok(loop_id)
457 }
458
459 async fn resume_and_reattach(&self, conn: PooledConnHandle, loop_id: &str) -> Result<()> {
460 conn.client.connect().await?;
461 conn.client.reattach_and_probe(loop_id).await?;
462 self.start_reader(&conn).await;
463 Ok(())
464 }
465
466 async fn start_reader(&self, conn: &PooledConnHandle) {
468 if let Some(handle) = conn.reader_task.lock().await.take() {
469 handle.abort();
470 }
471 conn.reader_live.store(false, Ordering::SeqCst);
472
473 let raw_rx = conn.client.receive_messages(256);
474 let (tx, event_rx) = tokio::sync::mpsc::channel(256);
475 *conn.event_rx.lock().await = Some(event_rx);
476 conn.reader_live.store(true, Ordering::SeqCst);
477
478 let reader_live_flag = Arc::clone(conn);
479 let session_id = conn.session_id.clone();
480
481 let handle = tokio::spawn(async move {
482 let mut raw = raw_rx;
483 while let Some(msg) = raw.recv().await {
484 if tx.send(msg).await.is_err() {
485 break;
486 }
487 }
488 reader_live_flag.reader_live.store(false, Ordering::SeqCst);
489 tracing::debug!(session_id, "pool event reader stopped");
490 });
491
492 *conn.reader_task.lock().await = Some(handle);
493 }
494}
495
496fn default_bootstrap_fn() -> BootstrapFn {
497 Arc::new(|client, _session_id, workspace_id, user_id| {
498 Box::pin(async move {
499 let mut boot = BootstrapOptions::new();
500 boot.workspace = Some(workspace_id);
501 boot.user_id = Some(user_id);
502 let ready = bootstrap_loop_session(&client, boot, None).await?;
503 let loop_id = ready
504 .get("loop_id")
505 .and_then(|v| v.as_str())
506 .unwrap_or("")
507 .trim()
508 .to_string();
509 if loop_id.is_empty() {
510 return Err(Error::msg("bootstrap missing loop_id"));
511 }
512 Ok(loop_id)
513 })
514 })
515}
516
517impl From<ErrPoolExhausted> for Error {
518 fn from(e: ErrPoolExhausted) -> Self {
519 Error::msg(e.to_string())
520 }
521}
522
523pub fn input_message_for_loop(
525 text: &str,
526 loop_id: &str,
527 attachments: Option<Value>,
528 opts: Option<&super::turn_runner::InputOpts>,
529) -> Map<String, Value> {
530 let mut params = Map::new();
531 params.insert("content".into(), json!(text));
532 params.insert("autonomous".into(), json!(false));
533 if !loop_id.trim().is_empty() {
534 params.insert("loop_id".into(), json!(loop_id));
535 }
536 if let Some(atts) = attachments {
537 params.insert("attachments".into(), atts);
538 }
539 if let Some(o) = opts {
540 if let Some(h) = o
541 .intent_hint
542 .as_ref()
543 .map(|s| s.trim())
544 .filter(|s| !s.is_empty())
545 {
546 params.insert("intent_hint".into(), json!(h));
547 }
548 if let Some(s) = o
549 .preferred_subagent
550 .as_ref()
551 .map(|s| s.trim())
552 .filter(|s| !s.is_empty())
553 {
554 params.insert("preferred_subagent".into(), json!(s));
555 }
556 if let Some(schema) = &o.response_schema {
557 params.insert("response_schema".into(), schema.clone());
558 }
559 if let Some(n) = o
560 .response_schema_name
561 .as_ref()
562 .map(|s| s.trim())
563 .filter(|s| !s.is_empty())
564 {
565 params.insert("response_schema_name".into(), json!(n));
566 }
567 if let Some(strict) = o.response_schema_strict {
568 params.insert("response_schema_strict".into(), json!(strict));
569 }
570 }
571 let env = new_notification("loop_input", params);
572 match serde_json::to_value(env) {
573 Ok(Value::Object(m)) => m,
574 _ => {
575 let mut fallback = Map::new();
576 fallback.insert("proto".into(), json!(crate::protocol::PROTO_VERSION));
577 fallback.insert("type".into(), json!("notification"));
578 fallback.insert("method".into(), json!("loop_input"));
579 fallback.insert(
580 "params".into(),
581 Value::Object({
582 let mut p = Map::new();
583 p.insert("content".into(), json!(text));
584 p.insert("autonomous".into(), json!(false));
585 if !loop_id.trim().is_empty() {
586 p.insert("loop_id".into(), json!(loop_id));
587 }
588 p
589 }),
590 );
591 fallback
592 }
593 }
594}