Skip to main content

sz_orm_graph/
connection.rs

1//! # Connection — Bolt 协议连接与连接池
2//!
3//! GraphConfig + GraphConnection + GraphPool
4
5use 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/// 图数据库连接配置
17#[derive(Debug, Clone)]
18pub struct GraphConfig {
19    /// Bolt DSN,如 `neo4j://neo4j:password@127.0.0.1:7687`
20    pub dsn: String,
21    /// 连接超时(秒)
22    pub connect_timeout_secs: u64,
23    /// 查询超时(秒)
24    pub query_timeout_secs: u64,
25    /// 连接池最大大小
26    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    /// 脱敏的 DSN(不泄露密码)
55    pub fn sanitized_dsn(&self) -> String {
56        sanitize_dsn(&self.dsn)
57    }
58}
59
60/// 图连接句柄
61#[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
196/// 图数据库连接池
197///
198/// v3.1.0 改进:从 `Arc<Mutex<Vec<GraphConnection>>>` 改为
199/// `Arc<ArrayQueue<GraphConnection>> + AtomicU32 + Notify`,
200/// 使用无锁 MPMC 队列消除锁竞争,与 sz-orm-core 连接池设计一致。
201pub struct GraphPool {
202    config: GraphConfig,
203    /// 无锁 MPMC 队列存储空闲连接
204    idle: Arc<ArrayQueue<GraphConnection>>,
205    /// 池中总连接数(idle + borrowed)
206    total_count: AtomicU32,
207    /// 池是否已关闭
208    closed: AtomicBool,
209    /// 异步通知等待者(池满时 acquire 等待 release 唤醒)
210    notify: Arc<Notify>,
211    /// 等待 acquire 的任务数(监控用)
212    waiters_count: AtomicU32,
213}
214
215/// 连接池状态快照
216#[derive(Debug, Clone)]
217pub struct GraphPoolStatus {
218    /// 空闲连接数
219    pub idle_count: usize,
220    /// 总连接数(idle + borrowed)
221    pub total_count: u32,
222    /// 等待 acquire 的任务数
223    pub waiters_count: u32,
224    /// 池是否已关闭
225    pub closed: bool,
226    /// 最大连接数
227    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    /// 获取连接
248    ///
249    /// 优先从空闲队列取;若空且未超 max_pool_size 则新建;
250    /// 若已满则等待 release 唤醒。
251    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        // 快速路径:从无锁队列取空闲连接
257        if let Some(conn) = self.idle.pop() {
258            return Ok(conn);
259        }
260
261        // 尝试创建新连接(CAS 循环确保不超 max_pool_size)
262        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                // 池满,等待 release 唤醒
270                self.waiters_count.fetch_add(1, Ordering::SeqCst);
271                self.notify.notified().await;
272                self.waiters_count.fetch_sub(1, Ordering::SeqCst);
273
274                // 被唤醒后重试快速路径
275                if let Some(conn) = self.idle.pop() {
276                    return Ok(conn);
277                }
278                continue;
279            }
280
281            // CAS:尝试将 total_count + 1
282            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    /// 带超时的 acquire
304    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    /// 归还连接到空闲队列
317    pub async fn release(&self, conn: GraphConnection) {
318        if self.closed.load(Ordering::Acquire) {
319            // 池已关闭,直接丢弃连接(total_count 不变,close 时统一处理)
320            return;
321        }
322        // push 到无锁队列(容量 = max_pool_size,不会溢出)
323        let _ = self.idle.push(conn);
324        self.notify.notify_one();
325    }
326
327    /// 空闲连接数
328    pub fn idle_count(&self) -> usize {
329        self.idle.len()
330    }
331
332    /// 总连接数(idle + borrowed)
333    pub fn total_count(&self) -> u32 {
334        self.total_count.load(Ordering::Acquire)
335    }
336
337    /// 等待者数
338    pub fn waiters_count(&self) -> u32 {
339        self.waiters_count.load(Ordering::Acquire)
340    }
341
342    /// 关闭连接池
343    ///
344    /// 设置 closed 标志,后续 acquire 返回错误,release 直接丢弃。
345    /// 已借出的连接不受影响(调用方自行 disconnect)。
346    pub fn close(&self) {
347        self.closed.store(true, Ordering::Release);
348        // 唤醒所有等待者
349        self.notify.notify_waiters();
350    }
351
352    /// 池是否已关闭
353    pub fn is_closed(&self) -> bool {
354        self.closed.load(Ordering::Acquire)
355    }
356
357    /// 获取池状态快照
358    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        // 第三个 acquire 会等待,用超时验证
439        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}