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 = dsn
145 .strip_prefix("neo4j://")
146 .or_else(|| dsn.strip_prefix("bolt://"))?;
147 let after_auth = if let Some(at_pos) = after_scheme.find('@') {
148 &after_scheme[at_pos + 1..]
149 } else {
150 after_scheme
151 };
152 let host_port = after_auth.split('/').next().unwrap_or(after_auth);
153 if host_port.is_empty() {
154 None
155 } else {
156 Some(host_port.to_string())
157 }
158 }
159
160 pub fn disconnect(&mut self) {
161 self.connected = false;
162 self.engine = None;
163 }
164
165 pub fn engine(&self) -> Option<&InMemoryGraphEngine> {
166 self.engine.as_ref()
167 }
168
169 pub fn engine_mut(&mut self) -> Option<&mut InMemoryGraphEngine> {
170 self.engine.as_mut()
171 }
172
173 pub fn add_node(&mut self, node: GraphNode) -> Result<(), GraphError> {
174 if !self.connected {
175 return Err(GraphError::ConnectionError("not connected".into()));
176 }
177 let engine = self
178 .engine
179 .as_mut()
180 .ok_or_else(|| GraphError::ConnectionError("engine not initialized".into()))?;
181 engine.add_node(node)
182 }
183
184 pub fn add_relationship(&mut self, rel: GraphRelationship) -> Result<(), GraphError> {
185 if !self.connected {
186 return Err(GraphError::ConnectionError("not connected".into()));
187 }
188 let engine = self
189 .engine
190 .as_mut()
191 .ok_or_else(|| GraphError::ConnectionError("engine not initialized".into()))?;
192 engine.add_relationship(rel)
193 }
194}
195
196pub struct GraphPool {
202 config: GraphConfig,
203 idle: Arc<ArrayQueue<GraphConnection>>,
205 total_count: AtomicU32,
207 closed: AtomicBool,
209 notify: Arc<Notify>,
211 waiters_count: AtomicU32,
213}
214
215#[derive(Debug, Clone)]
217pub struct GraphPoolStatus {
218 pub idle_count: usize,
220 pub total_count: u32,
222 pub waiters_count: u32,
224 pub closed: bool,
226 pub max_size: usize,
228}
229
230impl GraphPool {
231 pub fn new(config: GraphConfig) -> Self {
232 let max_size = config.max_pool_size;
233 Self {
234 config,
235 idle: Arc::new(ArrayQueue::new(max_size)),
236 total_count: AtomicU32::new(0),
237 closed: AtomicBool::new(false),
238 notify: Arc::new(Notify::new()),
239 waiters_count: AtomicU32::new(0),
240 }
241 }
242
243 pub fn config(&self) -> &GraphConfig {
244 &self.config
245 }
246
247 pub async fn acquire(&self) -> Result<GraphConnection, GraphError> {
252 if self.closed.load(Ordering::Acquire) {
253 return Err(GraphError::ConnectionError("pool closed".into()));
254 }
255
256 if let Some(conn) = self.idle.pop() {
258 return Ok(conn);
259 }
260
261 loop {
263 if self.closed.load(Ordering::Acquire) {
264 return Err(GraphError::ConnectionError("pool closed".into()));
265 }
266
267 let current = self.total_count.load(Ordering::Acquire);
268 if current >= self.config.max_pool_size as u32 {
269 self.waiters_count.fetch_add(1, Ordering::SeqCst);
271 self.notify.notified().await;
272 self.waiters_count.fetch_sub(1, Ordering::SeqCst);
273
274 if let Some(conn) = self.idle.pop() {
276 return Ok(conn);
277 }
278 continue;
279 }
280
281 match self.total_count.compare_exchange(
283 current,
284 current + 1,
285 Ordering::AcqRel,
286 Ordering::Acquire,
287 ) {
288 Ok(_) => {
289 let mut conn = GraphConnection::new(self.config.clone());
290 match conn.connect() {
291 Ok(()) => return Ok(conn),
292 Err(e) => {
293 self.total_count.fetch_sub(1, Ordering::SeqCst);
294 return Err(e);
295 }
296 }
297 }
298 Err(_) => continue,
299 }
300 }
301 }
302
303 pub async fn acquire_timeout(&self, timeout: Duration) -> Result<GraphConnection, GraphError> {
305 tokio::time::timeout(timeout, self.acquire())
306 .await
307 .map_err(|_| {
308 GraphError::ConnectionError(format!(
309 "acquire timeout after {:?}, DSN: {}",
310 timeout,
311 self.config.sanitized_dsn()
312 ))
313 })?
314 }
315
316 pub async fn release(&self, conn: GraphConnection) {
318 if self.closed.load(Ordering::Acquire) {
319 return;
321 }
322 let _ = self.idle.push(conn);
324 self.notify.notify_one();
325 }
326
327 pub fn idle_count(&self) -> usize {
329 self.idle.len()
330 }
331
332 pub fn total_count(&self) -> u32 {
334 self.total_count.load(Ordering::Acquire)
335 }
336
337 pub fn waiters_count(&self) -> u32 {
339 self.waiters_count.load(Ordering::Acquire)
340 }
341
342 pub fn close(&self) {
347 self.closed.store(true, Ordering::Release);
348 self.notify.notify_waiters();
350 }
351
352 pub fn is_closed(&self) -> bool {
354 self.closed.load(Ordering::Acquire)
355 }
356
357 pub fn status(&self) -> GraphPoolStatus {
359 GraphPoolStatus {
360 idle_count: self.idle.len(),
361 total_count: self.total_count.load(Ordering::Acquire),
362 waiters_count: self.waiters_count.load(Ordering::Acquire),
363 closed: self.closed.load(Ordering::Acquire),
364 max_size: self.config.max_pool_size,
365 }
366 }
367}
368
369impl Clone for GraphPool {
370 fn clone(&self) -> Self {
371 Self {
372 config: self.config.clone(),
373 idle: Arc::clone(&self.idle),
374 total_count: AtomicU32::new(self.total_count.load(Ordering::Acquire)),
375 closed: AtomicBool::new(self.closed.load(Ordering::Acquire)),
376 notify: Arc::clone(&self.notify),
377 waiters_count: AtomicU32::new(self.waiters_count.load(Ordering::Acquire)),
378 }
379 }
380}
381
382impl GraphConfig {
383 pub fn connect_timeout(&self) -> Duration {
384 Duration::from_secs(self.connect_timeout_secs)
385 }
386
387 pub fn query_timeout(&self) -> Duration {
388 Duration::from_secs(self.query_timeout_secs)
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 fn test_config() -> GraphConfig {
397 GraphConfig::new("memory://localhost")
398 }
399
400 #[tokio::test]
401 async fn test_pool_acquire_creates_new_connection() {
402 let pool = GraphPool::new(test_config());
403 let conn = pool.acquire().await.unwrap();
404 assert!(conn.is_connected());
405 assert_eq!(pool.total_count(), 1);
406 assert_eq!(pool.idle_count(), 0);
407 }
408
409 #[tokio::test]
410 async fn test_pool_release_returns_to_idle() {
411 let pool = GraphPool::new(test_config());
412 let conn = pool.acquire().await.unwrap();
413 assert_eq!(pool.idle_count(), 0);
414 pool.release(conn).await;
415 assert_eq!(pool.idle_count(), 1);
416 assert_eq!(pool.total_count(), 1);
417 }
418
419 #[tokio::test]
420 async fn test_pool_acquire_reuses_idle() {
421 let pool = GraphPool::new(test_config());
422 let conn = pool.acquire().await.unwrap();
423 pool.release(conn).await;
424 let conn2 = pool.acquire().await.unwrap();
425 assert!(conn2.is_connected());
426 assert_eq!(pool.total_count(), 1);
427 assert_eq!(pool.idle_count(), 0);
428 }
429
430 #[tokio::test]
431 async fn test_pool_max_size_enforced() {
432 let config = test_config().with_pool_size(2);
433 let pool = GraphPool::new(config);
434 let c1 = pool.acquire().await.unwrap();
435 let c2 = pool.acquire().await.unwrap();
436 assert_eq!(pool.total_count(), 2);
437
438 let result = pool.acquire_timeout(Duration::from_millis(100)).await;
440 assert!(result.is_err());
441 assert!(result.unwrap_err().to_string().contains("timeout"));
442
443 pool.release(c1).await;
444 pool.release(c2).await;
445 }
446
447 #[tokio::test]
448 async fn test_pool_close_rejects_acquire() {
449 let pool = GraphPool::new(test_config());
450 pool.close();
451 assert!(pool.is_closed());
452 let result = pool.acquire().await;
453 assert!(result.is_err());
454 assert!(result.unwrap_err().to_string().contains("pool closed"));
455 }
456
457 #[tokio::test]
458 async fn test_pool_release_after_close_drops_connection() {
459 let pool = GraphPool::new(test_config());
460 let conn = pool.acquire().await.unwrap();
461 pool.close();
462 pool.release(conn).await;
463 assert_eq!(pool.idle_count(), 0);
464 }
465
466 #[tokio::test]
467 async fn test_pool_status_snapshot() {
468 let config = test_config().with_pool_size(5);
469 let pool = GraphPool::new(config);
470 let _c1 = pool.acquire().await.unwrap();
471 let c2 = pool.acquire().await.unwrap();
472 pool.release(c2).await;
473
474 let status = pool.status();
475 assert_eq!(status.max_size, 5);
476 assert_eq!(status.total_count, 2);
477 assert_eq!(status.idle_count, 1);
478 assert!(!status.closed);
479 }
480
481 #[tokio::test]
482 async fn test_pool_acquire_timeout_succeeds_when_connection_available() {
483 let pool = GraphPool::new(test_config());
484 let conn = pool.acquire_timeout(Duration::from_secs(1)).await;
485 assert!(conn.is_ok());
486 }
487
488 #[tokio::test]
489 async fn test_pool_concurrent_acquire_release() {
490 let config = test_config().with_pool_size(4);
491 let pool = Arc::new(GraphPool::new(config));
492
493 let mut handles = Vec::new();
494 for _ in 0..8 {
495 let p = Arc::clone(&pool);
496 handles.push(tokio::spawn(async move {
497 let conn = p.acquire().await.unwrap();
498 tokio::time::sleep(Duration::from_millis(10)).await;
499 p.release(conn).await;
500 }));
501 }
502 for h in handles {
503 h.await.unwrap();
504 }
505 assert!(pool.total_count() <= 4);
506 assert!(pool.idle_count() <= 4);
507 }
508
509 #[test]
510 fn test_graph_config_builders() {
511 let config = test_config()
512 .with_connect_timeout(5)
513 .with_query_timeout(60)
514 .with_pool_size(20);
515 assert_eq!(config.connect_timeout_secs, 5);
516 assert_eq!(config.query_timeout_secs, 60);
517 assert_eq!(config.max_pool_size, 20);
518 assert_eq!(config.connect_timeout(), Duration::from_secs(5));
519 assert_eq!(config.query_timeout(), Duration::from_secs(60));
520 }
521
522 #[test]
523 fn test_graph_config_sanitized_dsn() {
524 let config = GraphConfig::new("neo4j://neo4j:test123@127.0.0.1:7687");
525 let sanitized = config.sanitized_dsn();
526 assert!(!sanitized.contains("test123"));
527 assert!(sanitized.contains("***"));
528 }
529
530 #[test]
531 fn test_graph_connection_connect_invalid_dsn() {
532 let config = GraphConfig::new("");
533 let mut conn = GraphConnection::new(config);
534 let result = conn.connect();
535 assert!(result.is_err());
536 }
537
538 #[test]
539 fn test_graph_connection_connect_invalid_scheme() {
540 let config = GraphConfig::new("http://127.0.0.1:7687");
541 let mut conn = GraphConnection::new(config);
542 let result = conn.connect();
543 assert!(result.is_err());
544 }
545
546 #[test]
547 fn test_graph_connection_connect_bolt_scheme() {
548 let config = GraphConfig::new("bolt://neo4j:pass@127.0.0.1:7687");
549 let mut conn = GraphConnection::new(config);
550 let result = conn.connect();
551 assert!(result.is_err());
552 assert!(matches!(result.unwrap_err(), GraphError::DriverError(_)));
553 assert!(!conn.is_connected());
554 }
555
556 #[test]
557 fn test_graph_connection_disconnect() {
558 let config = test_config();
559 let mut conn = GraphConnection::new(config);
560 conn.connect().unwrap();
561 assert!(conn.is_connected());
562 conn.disconnect();
563 assert!(!conn.is_connected());
564 }
565}