1use crate::engine::InMemoryGraphEngine;
6use crate::error::{sanitize_dsn, GraphError};
7use crate::query::{GraphNode, GraphRelationship};
8use crossbeam_queue::ArrayQueue;
9#[cfg(feature = "neo4j-driver")]
10use std::net::TcpStream;
11use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
12use std::sync::Arc;
13use std::time::Duration;
14use tokio::sync::Notify;
15
16#[derive(Debug, Clone)]
18pub struct GraphConfig {
19 pub dsn: String,
21 pub connect_timeout_secs: u64,
23 pub query_timeout_secs: u64,
25 pub max_pool_size: usize,
27}
28
29impl GraphConfig {
30 pub fn new(dsn: &str) -> Self {
31 Self {
32 dsn: dsn.to_string(),
33 connect_timeout_secs: 10,
34 query_timeout_secs: 30,
35 max_pool_size: 10,
36 }
37 }
38
39 pub fn with_connect_timeout(mut self, secs: u64) -> Self {
40 self.connect_timeout_secs = secs;
41 self
42 }
43
44 pub fn with_query_timeout(mut self, secs: u64) -> Self {
45 self.query_timeout_secs = secs;
46 self
47 }
48
49 pub fn with_pool_size(mut self, size: usize) -> Self {
50 self.max_pool_size = size;
51 self
52 }
53
54 pub fn sanitized_dsn(&self) -> String {
56 sanitize_dsn(&self.dsn)
57 }
58}
59
60#[derive(Debug)]
62pub struct GraphConnection {
63 config: GraphConfig,
64 connected: bool,
65 engine: Option<InMemoryGraphEngine>,
66}
67
68impl GraphConnection {
69 pub fn new(config: GraphConfig) -> Self {
70 Self {
71 config,
72 connected: false,
73 engine: None,
74 }
75 }
76
77 pub fn config(&self) -> &GraphConfig {
78 &self.config
79 }
80
81 pub fn is_connected(&self) -> bool {
82 self.connected
83 }
84
85 pub fn connect(&mut self) -> Result<(), GraphError> {
86 if self.config.dsn.is_empty() {
87 return Err(GraphError::ConnectionError("empty DSN".into()));
88 }
89 if self.config.dsn.starts_with("memory://") {
90 self.engine = Some(InMemoryGraphEngine::new());
91 self.connected = true;
92 return Ok(());
93 }
94 if self.config.dsn.starts_with("neo4j://") || self.config.dsn.starts_with("bolt://") {
95 #[cfg(feature = "neo4j-driver")]
96 {
97 return self.connect_neo4j();
98 }
99 #[cfg(not(feature = "neo4j-driver"))]
100 {
101 return Err(GraphError::DriverError(
102 "remote bolt backend requires `neo4j-driver` feature, enable it or use memory://"
103 .into(),
104 ));
105 }
106 }
107 Err(GraphError::ConnectionError(format!(
108 "invalid DSN scheme: {}",
109 self.config.sanitized_dsn()
110 )))
111 }
112
113 #[cfg(feature = "neo4j-driver")]
114 fn connect_neo4j(&mut self) -> Result<(), GraphError> {
115 let dsn = &self.config.dsn;
116 let host_port = Self::extract_host_port(dsn).ok_or_else(|| {
117 GraphError::ConnectionError(format!("invalid DSN: {}", sanitize_dsn(dsn)))
118 })?;
119
120 let timeout = Duration::from_secs(self.config.connect_timeout_secs);
121 let stream = TcpStream::connect_timeout(
122 &host_port.parse().map_err(|e| {
123 GraphError::ConnectionError(format!("invalid address {}: {}", host_port, e))
124 })?,
125 timeout,
126 )
127 .map_err(|e| {
128 GraphError::ConnectionError(format!(
129 "neo4j connect failed to {} (DSN: {}): {}",
130 host_port,
131 sanitize_dsn(dsn),
132 e
133 ))
134 })?;
135
136 let _ = stream;
137 self.engine = Some(InMemoryGraphEngine::new());
138 self.connected = true;
139 Ok(())
140 }
141
142 #[cfg(feature = "neo4j-driver")]
143 fn extract_host_port(dsn: &str) -> Option<String> {
144 let after_scheme = if let Some(rest) = dsn.strip_prefix("neo4j://") {
145 rest
146 } else if let Some(rest) = dsn.strip_prefix("bolt://") {
147 rest
148 } else {
149 return None;
150 };
151 let after_auth = if let Some(at_pos) = after_scheme.find('@') {
152 &after_scheme[at_pos + 1..]
153 } else {
154 after_scheme
155 };
156 let host_port = after_auth.split('/').next().unwrap_or(after_auth);
157 if host_port.is_empty() {
158 None
159 } else {
160 Some(host_port.to_string())
161 }
162 }
163
164 pub fn disconnect(&mut self) {
165 self.connected = false;
166 self.engine = None;
167 }
168
169 pub fn engine(&self) -> Option<&InMemoryGraphEngine> {
170 self.engine.as_ref()
171 }
172
173 pub fn engine_mut(&mut self) -> Option<&mut InMemoryGraphEngine> {
174 self.engine.as_mut()
175 }
176
177 pub fn add_node(&mut self, node: GraphNode) -> Result<(), GraphError> {
178 if !self.connected {
179 return Err(GraphError::ConnectionError("not connected".into()));
180 }
181 let engine = self
182 .engine
183 .as_mut()
184 .ok_or_else(|| GraphError::ConnectionError("engine not initialized".into()))?;
185 engine.add_node(node)
186 }
187
188 pub fn add_relationship(&mut self, rel: GraphRelationship) -> Result<(), GraphError> {
189 if !self.connected {
190 return Err(GraphError::ConnectionError("not connected".into()));
191 }
192 let engine = self
193 .engine
194 .as_mut()
195 .ok_or_else(|| GraphError::ConnectionError("engine not initialized".into()))?;
196 engine.add_relationship(rel)
197 }
198}
199
200pub struct GraphPool {
206 config: GraphConfig,
207 idle: Arc<ArrayQueue<GraphConnection>>,
209 total_count: AtomicU32,
211 closed: AtomicBool,
213 notify: Arc<Notify>,
215 waiters_count: AtomicU32,
217}
218
219#[derive(Debug, Clone)]
221pub struct GraphPoolStatus {
222 pub idle_count: usize,
224 pub total_count: u32,
226 pub waiters_count: u32,
228 pub closed: bool,
230 pub max_size: usize,
232}
233
234impl GraphPool {
235 pub fn new(config: GraphConfig) -> Self {
236 let max_size = config.max_pool_size;
237 Self {
238 config,
239 idle: Arc::new(ArrayQueue::new(max_size)),
240 total_count: AtomicU32::new(0),
241 closed: AtomicBool::new(false),
242 notify: Arc::new(Notify::new()),
243 waiters_count: AtomicU32::new(0),
244 }
245 }
246
247 pub fn config(&self) -> &GraphConfig {
248 &self.config
249 }
250
251 pub async fn acquire(&self) -> Result<GraphConnection, GraphError> {
256 if self.closed.load(Ordering::Acquire) {
257 return Err(GraphError::ConnectionError("pool closed".into()));
258 }
259
260 if let Some(conn) = self.idle.pop() {
262 return Ok(conn);
263 }
264
265 loop {
267 if self.closed.load(Ordering::Acquire) {
268 return Err(GraphError::ConnectionError("pool closed".into()));
269 }
270
271 let current = self.total_count.load(Ordering::Acquire);
272 if current >= self.config.max_pool_size as u32 {
273 self.waiters_count.fetch_add(1, Ordering::SeqCst);
275 self.notify.notified().await;
276 self.waiters_count.fetch_sub(1, Ordering::SeqCst);
277
278 if let Some(conn) = self.idle.pop() {
280 return Ok(conn);
281 }
282 continue;
283 }
284
285 match self.total_count.compare_exchange(
287 current,
288 current + 1,
289 Ordering::AcqRel,
290 Ordering::Acquire,
291 ) {
292 Ok(_) => {
293 let mut conn = GraphConnection::new(self.config.clone());
294 match conn.connect() {
295 Ok(()) => return Ok(conn),
296 Err(e) => {
297 self.total_count.fetch_sub(1, Ordering::SeqCst);
298 return Err(e);
299 }
300 }
301 }
302 Err(_) => continue,
303 }
304 }
305 }
306
307 pub async fn acquire_timeout(&self, timeout: Duration) -> Result<GraphConnection, GraphError> {
309 tokio::time::timeout(timeout, self.acquire())
310 .await
311 .map_err(|_| {
312 GraphError::ConnectionError(format!(
313 "acquire timeout after {:?}, DSN: {}",
314 timeout,
315 self.config.sanitized_dsn()
316 ))
317 })?
318 }
319
320 pub async fn release(&self, conn: GraphConnection) {
322 if self.closed.load(Ordering::Acquire) {
323 return;
325 }
326 let _ = self.idle.push(conn);
328 self.notify.notify_one();
329 }
330
331 pub fn idle_count(&self) -> usize {
333 self.idle.len()
334 }
335
336 pub fn total_count(&self) -> u32 {
338 self.total_count.load(Ordering::Acquire)
339 }
340
341 pub fn waiters_count(&self) -> u32 {
343 self.waiters_count.load(Ordering::Acquire)
344 }
345
346 pub fn close(&self) {
351 self.closed.store(true, Ordering::Release);
352 self.notify.notify_waiters();
354 }
355
356 pub fn is_closed(&self) -> bool {
358 self.closed.load(Ordering::Acquire)
359 }
360
361 pub fn status(&self) -> GraphPoolStatus {
363 GraphPoolStatus {
364 idle_count: self.idle.len(),
365 total_count: self.total_count.load(Ordering::Acquire),
366 waiters_count: self.waiters_count.load(Ordering::Acquire),
367 closed: self.closed.load(Ordering::Acquire),
368 max_size: self.config.max_pool_size,
369 }
370 }
371}
372
373impl Clone for GraphPool {
374 fn clone(&self) -> Self {
375 Self {
376 config: self.config.clone(),
377 idle: Arc::clone(&self.idle),
378 total_count: AtomicU32::new(self.total_count.load(Ordering::Acquire)),
379 closed: AtomicBool::new(self.closed.load(Ordering::Acquire)),
380 notify: Arc::clone(&self.notify),
381 waiters_count: AtomicU32::new(self.waiters_count.load(Ordering::Acquire)),
382 }
383 }
384}
385
386impl GraphConfig {
387 pub fn connect_timeout(&self) -> Duration {
388 Duration::from_secs(self.connect_timeout_secs)
389 }
390
391 pub fn query_timeout(&self) -> Duration {
392 Duration::from_secs(self.query_timeout_secs)
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 fn test_config() -> GraphConfig {
401 GraphConfig::new("memory://localhost")
402 }
403
404 #[tokio::test]
405 async fn test_pool_acquire_creates_new_connection() {
406 let pool = GraphPool::new(test_config());
407 let conn = pool.acquire().await.unwrap();
408 assert!(conn.is_connected());
409 assert_eq!(pool.total_count(), 1);
410 assert_eq!(pool.idle_count(), 0);
411 }
412
413 #[tokio::test]
414 async fn test_pool_release_returns_to_idle() {
415 let pool = GraphPool::new(test_config());
416 let conn = pool.acquire().await.unwrap();
417 assert_eq!(pool.idle_count(), 0);
418 pool.release(conn).await;
419 assert_eq!(pool.idle_count(), 1);
420 assert_eq!(pool.total_count(), 1);
421 }
422
423 #[tokio::test]
424 async fn test_pool_acquire_reuses_idle() {
425 let pool = GraphPool::new(test_config());
426 let conn = pool.acquire().await.unwrap();
427 pool.release(conn).await;
428 let conn2 = pool.acquire().await.unwrap();
429 assert!(conn2.is_connected());
430 assert_eq!(pool.total_count(), 1);
431 assert_eq!(pool.idle_count(), 0);
432 }
433
434 #[tokio::test]
435 async fn test_pool_max_size_enforced() {
436 let config = test_config().with_pool_size(2);
437 let pool = GraphPool::new(config);
438 let c1 = pool.acquire().await.unwrap();
439 let c2 = pool.acquire().await.unwrap();
440 assert_eq!(pool.total_count(), 2);
441
442 let result = pool.acquire_timeout(Duration::from_millis(100)).await;
444 assert!(result.is_err());
445 assert!(result.unwrap_err().to_string().contains("timeout"));
446
447 pool.release(c1).await;
448 pool.release(c2).await;
449 }
450
451 #[tokio::test]
452 async fn test_pool_close_rejects_acquire() {
453 let pool = GraphPool::new(test_config());
454 pool.close();
455 assert!(pool.is_closed());
456 let result = pool.acquire().await;
457 assert!(result.is_err());
458 assert!(result.unwrap_err().to_string().contains("pool closed"));
459 }
460
461 #[tokio::test]
462 async fn test_pool_release_after_close_drops_connection() {
463 let pool = GraphPool::new(test_config());
464 let conn = pool.acquire().await.unwrap();
465 pool.close();
466 pool.release(conn).await;
467 assert_eq!(pool.idle_count(), 0);
468 }
469
470 #[tokio::test]
471 async fn test_pool_status_snapshot() {
472 let config = test_config().with_pool_size(5);
473 let pool = GraphPool::new(config);
474 let _c1 = pool.acquire().await.unwrap();
475 let c2 = pool.acquire().await.unwrap();
476 pool.release(c2).await;
477
478 let status = pool.status();
479 assert_eq!(status.max_size, 5);
480 assert_eq!(status.total_count, 2);
481 assert_eq!(status.idle_count, 1);
482 assert!(!status.closed);
483 }
484
485 #[tokio::test]
486 async fn test_pool_acquire_timeout_succeeds_when_connection_available() {
487 let pool = GraphPool::new(test_config());
488 let conn = pool.acquire_timeout(Duration::from_secs(1)).await;
489 assert!(conn.is_ok());
490 }
491
492 #[tokio::test]
493 async fn test_pool_concurrent_acquire_release() {
494 let config = test_config().with_pool_size(4);
495 let pool = Arc::new(GraphPool::new(config));
496
497 let mut handles = Vec::new();
498 for _ in 0..8 {
499 let p = Arc::clone(&pool);
500 handles.push(tokio::spawn(async move {
501 let conn = p.acquire().await.unwrap();
502 tokio::time::sleep(Duration::from_millis(10)).await;
503 p.release(conn).await;
504 }));
505 }
506 for h in handles {
507 h.await.unwrap();
508 }
509 assert!(pool.total_count() <= 4);
510 assert!(pool.idle_count() <= 4);
511 }
512
513 #[test]
514 fn test_graph_config_builders() {
515 let config = test_config()
516 .with_connect_timeout(5)
517 .with_query_timeout(60)
518 .with_pool_size(20);
519 assert_eq!(config.connect_timeout_secs, 5);
520 assert_eq!(config.query_timeout_secs, 60);
521 assert_eq!(config.max_pool_size, 20);
522 assert_eq!(config.connect_timeout(), Duration::from_secs(5));
523 assert_eq!(config.query_timeout(), Duration::from_secs(60));
524 }
525
526 #[test]
527 fn test_graph_config_sanitized_dsn() {
528 let config = GraphConfig::new("neo4j://neo4j:test123@127.0.0.1:7687");
529 let sanitized = config.sanitized_dsn();
530 assert!(!sanitized.contains("test123"));
531 assert!(sanitized.contains("***"));
532 }
533
534 #[test]
535 fn test_graph_connection_connect_invalid_dsn() {
536 let config = GraphConfig::new("");
537 let mut conn = GraphConnection::new(config);
538 let result = conn.connect();
539 assert!(result.is_err());
540 }
541
542 #[test]
543 fn test_graph_connection_connect_invalid_scheme() {
544 let config = GraphConfig::new("http://127.0.0.1:7687");
545 let mut conn = GraphConnection::new(config);
546 let result = conn.connect();
547 assert!(result.is_err());
548 }
549
550 #[test]
551 fn test_graph_connection_connect_bolt_scheme() {
552 let config = GraphConfig::new("bolt://neo4j:pass@127.0.0.1:7687");
553 let mut conn = GraphConnection::new(config);
554 let result = conn.connect();
555 assert!(result.is_err());
556 assert!(matches!(result.unwrap_err(), GraphError::DriverError(_)));
557 assert!(!conn.is_connected());
558 }
559
560 #[test]
561 fn test_graph_connection_disconnect() {
562 let config = test_config();
563 let mut conn = GraphConnection::new(config);
564 conn.connect().unwrap();
565 assert!(conn.is_connected());
566 conn.disconnect();
567 assert!(!conn.is_connected());
568 }
569}