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 = 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
200/// 图数据库连接池
201///
202/// v3.1.0 改进:从 `Arc<Mutex<Vec<GraphConnection>>>` 改为
203/// `Arc<ArrayQueue<GraphConnection>> + AtomicU32 + Notify`,
204/// 使用无锁 MPMC 队列消除锁竞争,与 sz-orm-core 连接池设计一致。
205pub struct GraphPool {
206    config: GraphConfig,
207    /// 无锁 MPMC 队列存储空闲连接
208    idle: Arc<ArrayQueue<GraphConnection>>,
209    /// 池中总连接数(idle + borrowed)
210    total_count: AtomicU32,
211    /// 池是否已关闭
212    closed: AtomicBool,
213    /// 异步通知等待者(池满时 acquire 等待 release 唤醒)
214    notify: Arc<Notify>,
215    /// 等待 acquire 的任务数(监控用)
216    waiters_count: AtomicU32,
217}
218
219/// 连接池状态快照
220#[derive(Debug, Clone)]
221pub struct GraphPoolStatus {
222    /// 空闲连接数
223    pub idle_count: usize,
224    /// 总连接数(idle + borrowed)
225    pub total_count: u32,
226    /// 等待 acquire 的任务数
227    pub waiters_count: u32,
228    /// 池是否已关闭
229    pub closed: bool,
230    /// 最大连接数
231    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    /// 获取连接
252    ///
253    /// 优先从空闲队列取;若空且未超 max_pool_size 则新建;
254    /// 若已满则等待 release 唤醒。
255    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        // 快速路径:从无锁队列取空闲连接
261        if let Some(conn) = self.idle.pop() {
262            return Ok(conn);
263        }
264
265        // 尝试创建新连接(CAS 循环确保不超 max_pool_size)
266        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                // 池满,等待 release 唤醒
274                self.waiters_count.fetch_add(1, Ordering::SeqCst);
275                self.notify.notified().await;
276                self.waiters_count.fetch_sub(1, Ordering::SeqCst);
277
278                // 被唤醒后重试快速路径
279                if let Some(conn) = self.idle.pop() {
280                    return Ok(conn);
281                }
282                continue;
283            }
284
285            // CAS:尝试将 total_count + 1
286            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    /// 带超时的 acquire
308    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    /// 归还连接到空闲队列
321    pub async fn release(&self, conn: GraphConnection) {
322        if self.closed.load(Ordering::Acquire) {
323            // 池已关闭,直接丢弃连接(total_count 不变,close 时统一处理)
324            return;
325        }
326        // push 到无锁队列(容量 = max_pool_size,不会溢出)
327        let _ = self.idle.push(conn);
328        self.notify.notify_one();
329    }
330
331    /// 空闲连接数
332    pub fn idle_count(&self) -> usize {
333        self.idle.len()
334    }
335
336    /// 总连接数(idle + borrowed)
337    pub fn total_count(&self) -> u32 {
338        self.total_count.load(Ordering::Acquire)
339    }
340
341    /// 等待者数
342    pub fn waiters_count(&self) -> u32 {
343        self.waiters_count.load(Ordering::Acquire)
344    }
345
346    /// 关闭连接池
347    ///
348    /// 设置 closed 标志,后续 acquire 返回错误,release 直接丢弃。
349    /// 已借出的连接不受影响(调用方自行 disconnect)。
350    pub fn close(&self) {
351        self.closed.store(true, Ordering::Release);
352        // 唤醒所有等待者
353        self.notify.notify_waiters();
354    }
355
356    /// 池是否已关闭
357    pub fn is_closed(&self) -> bool {
358        self.closed.load(Ordering::Acquire)
359    }
360
361    /// 获取池状态快照
362    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        // 第三个 acquire 会等待,用超时验证
443        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}