Skip to main content

linkrs_server/session/
session_manager.rs

1use dashmap::DashMap;
2use linkrs_core::event_dispatch::{EventFilter, EventSubscriptions, SubscriptionId};
3use linkrs_query::{SessionEvent, SessionEventCallback};
4use log::{info, warn};
5use parking_lot::RwLock;
6use rand::RngCore;
7use std::collections::HashMap;
8use std::sync::{
9    atomic::{AtomicBool, AtomicU64, Ordering},
10    Arc,
11};
12use std::time::{Duration, Instant, SystemTime};
13use tokio::time;
14
15use super::{SessionError, SessionResult};
16use crate::client::{ClientSession, Session};
17
18pub const DEFAULT_MAX_ALLOWED_CONNECTIONS: usize = 100; // Default maximum number of connections (in a single-node scenario)
19pub const DEFAULT_SESSION_IDLE_TIMEOUT: Duration = Duration::from_secs(600); // 10 minutes
20
21/// Session information, used to display the list of sessions
22#[derive(Debug, Clone)]
23pub struct SessionInfo {
24    pub session_id: i64,
25    pub user_name: String,
26    pub space_name: Option<String>,
27    pub graph_addr: Option<String>,
28    pub create_time: SystemTime,
29    pub last_access_time: SystemTime,
30    pub active_queries: usize,
31    pub timezone: Option<i32>,
32}
33
34impl SessionInfo {
35    pub fn from_client_session(session: &ClientSession, create_time: SystemTime) -> Self {
36        Self {
37            session_id: session.id(),
38            user_name: session.user(),
39            space_name: session.space_name(),
40            graph_addr: session.graph_addr(),
41            create_time,
42            last_access_time: SystemTime::now() - Duration::from_millis(session.idle_seconds()),
43            active_queries: session.active_queries_count(),
44            timezone: session.timezone(),
45        }
46    }
47
48    pub fn from_params(
49        session_id_str: &str,
50        user_name: &str,
51        space_name: Option<String>,
52        client_ip: &str,
53        client_port: u16,
54    ) -> Result<Self, String> {
55        let session_id = session_id_str
56            .parse::<i64>()
57            .map_err(|_| format!("Invalid session ID: {}", session_id_str))?;
58
59        let graph_addr = if client_ip.is_empty() {
60            None
61        } else {
62            Some(format!("{}:{}", client_ip, client_port))
63        };
64
65        let now = SystemTime::now();
66        Ok(Self {
67            session_id,
68            user_name: user_name.to_string(),
69            space_name,
70            graph_addr,
71            create_time: now,
72            last_access_time: now,
73            active_queries: 0,
74            timezone: None,
75        })
76    }
77
78    pub fn touch(&mut self) {
79        self.last_access_time = SystemTime::now();
80    }
81}
82
83pub struct GraphSessionManager {
84    // Use DashMap to achieve true concurrent access without the need for explicit locking.
85    sessions: Arc<DashMap<i64, Arc<ClientSession>>>,
86    active_sessions: Arc<DashMap<i64, Instant>>, // session_id -> last_activity_time
87    // Read more, write less – and use RwLock.
88    session_create_times: Arc<RwLock<HashMap<i64, SystemTime>>>, // session_id -> create_time
89    host_addr: String,
90    max_connections: usize,
91    session_idle_timeout: Duration,
92    /// Initial auto-commit mode for newly created sessions, from
93    /// `[transaction].auto_commit`. Explicit BEGIN/COMMIT still overrides it.
94    default_auto_commit: AtomicBool,
95    /// Is the background cleanup task currently running?
96    cleanup_task_running: Arc<AtomicBool>,
97    /// Cumulative number of sessions created (monotonic, never decremented).
98    total_sessions_created: Arc<AtomicU64>,
99    // Shared with `QueryManager` when co-located: single enum dual emission
100    // source (`SessionCreated/Destroyed` here, `Query*` there).
101    session_callbacks: Arc<EventSubscriptions<SessionEvent>>,
102}
103
104impl std::fmt::Debug for GraphSessionManager {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        f.debug_struct("GraphSessionManager")
107            .field("sessions_count", &self.sessions.len())
108            .field("host_addr", &self.host_addr)
109            .field("max_connections", &self.max_connections)
110            .field("session_callbacks", &self.session_callbacks.len())
111            .finish()
112    }
113}
114
115impl GraphSessionManager {
116    /// Create a new session manager.
117    ///
118    /// This constructor does not automatically initiate background cleanup tasks.
119    /// Need to explicitly call `start_cleanup_task()` to start
120    pub fn new(
121        host_addr: String,
122        max_connections: usize,
123        session_idle_timeout: Duration,
124    ) -> Arc<Self> {
125        Self::new_with_shared(
126            host_addr,
127            max_connections,
128            session_idle_timeout,
129            Arc::new(EventSubscriptions::new()),
130        )
131    }
132
133    /// Build a manager sharing one session-event registry with `QueryManager`.
134    pub fn new_with_shared(
135        host_addr: String,
136        max_connections: usize,
137        session_idle_timeout: Duration,
138        shared: Arc<EventSubscriptions<SessionEvent>>,
139    ) -> Arc<Self> {
140        Arc::new(Self {
141            sessions: Arc::new(DashMap::new()),
142            active_sessions: Arc::new(DashMap::new()),
143            session_create_times: Arc::new(RwLock::new(HashMap::new())),
144            host_addr,
145            max_connections,
146            session_idle_timeout,
147            default_auto_commit: AtomicBool::new(true),
148            cleanup_task_running: Arc::new(AtomicBool::new(false)),
149            total_sessions_created: Arc::new(AtomicU64::new(0)),
150            session_callbacks: shared,
151        })
152    }
153
154    /// Shared session-event registry behind this manager.
155    pub fn shared_session_callbacks(&self) -> Arc<EventSubscriptions<SessionEvent>> {
156        Arc::clone(&self.session_callbacks)
157    }
158
159    /// Mask a session id for logs: prefix plus a short hash only.
160    pub fn mask_session_id(session_id: i64) -> String {
161        use std::collections::hash_map::DefaultHasher;
162        use std::hash::{Hash, Hasher};
163        let mut hasher = DefaultHasher::new();
164        session_id.hash(&mut hasher);
165        format!("{:x}..{:04x}", session_id >> 48, hasher.finish() & 0xffff)
166    }
167
168    /// Session idle timeout driving reclamation and expiry responses.
169    pub fn idle_timeout(&self) -> Duration {
170        self.session_idle_timeout
171    }
172
173    /// Set the auto-commit mode applied to sessions created from now on.
174    pub fn set_default_auto_commit(&self, auto_commit: bool) {
175        self.default_auto_commit
176            .store(auto_commit, Ordering::SeqCst);
177    }
178
179    /// Point this manager at a shared session-event registry.
180    ///
181    /// The field is an `Arc`, so an already-published instance needs `&mut`
182    /// access to switch sources; managers behind `Arc` should prefer
183    /// `new_with_shared` at construction time instead.
184    pub fn set_shared_session_callbacks(&mut self, shared: Arc<EventSubscriptions<SessionEvent>>) {
185        self.session_callbacks = shared;
186    }
187
188    /// Register a runtime observer for session lifecycle events.
189    pub fn register_session_callback(&self, callback: SessionEventCallback) -> SubscriptionId {
190        self.session_callbacks.add(callback)
191    }
192
193    /// Register a filtered observer invoked only when `filter` returns true.
194    pub fn register_session_callback_filtered(
195        &self,
196        callback: SessionEventCallback,
197        filter: EventFilter<SessionEvent>,
198    ) -> SubscriptionId {
199        self.session_callbacks.add_filtered(callback, Some(filter))
200    }
201
202    /// Remove a previously registered observer. Returns true if present.
203    pub fn unregister_session_callback(&self, id: SubscriptionId) -> bool {
204        self.session_callbacks.remove(id)
205    }
206
207    /// Number of registered session observers.
208    pub fn session_callback_count(&self) -> usize {
209        self.session_callbacks.len()
210    }
211
212    fn emit_session_event(&self, event: SessionEvent) {
213        if self.session_callbacks.is_empty() {
214            return;
215        }
216        self.session_callbacks.dispatch("session", &event);
217    }
218
219    /// Start the background session cleanup task.
220    ///
221    /// If the task is already running, this method will not start it again.
222    pub async fn start_cleanup_task(self: &Arc<Self>) {
223        if self.cleanup_task_running.swap(true, Ordering::SeqCst) {
224            info!("Session cleanup task is already running");
225            return;
226        }
227
228        info!("Starting session cleanup task");
229        let manager_clone = Arc::clone(self);
230        tokio::spawn(async move {
231            manager_clone.background_reclamation_task().await;
232        });
233    }
234
235    /// Stop the background session cleaning task.
236    ///
237    /// Set a stop flag; the background task will exit during the next iteration.
238    pub fn stop_cleanup_task(&self) {
239        info!("Stopping session cleanup task");
240        self.cleanup_task_running.store(false, Ordering::SeqCst);
241    }
242
243    /// Check whether the background cleanup task is currently running.
244    pub fn is_cleanup_task_running(&self) -> bool {
245        self.cleanup_task_running.load(Ordering::SeqCst)
246    }
247
248    /// Creates a new session
249    pub async fn create_session(
250        &self,
251        user_name: String,
252        _client_ip: String,
253    ) -> Result<Arc<ClientSession>, String> {
254        info!("Creating new session for user: {}", user_name);
255
256        // Check if we're out of connections
257        if self.is_out_of_connections().await {
258            warn!(
259                "Failed to create session for user {}: maximum connections exceeded",
260                user_name
261            );
262            return Err("Exceeded maximum allowed connections".to_string());
263        }
264
265        // Generate a new session ID, retrying on the unlikely event of a
266        // collision with a live session.
267        let session_id = loop {
268            let candidate = self.generate_session_id();
269            if !self.sessions.contains_key(&candidate) {
270                break candidate;
271            }
272        };
273        info!(
274            "Generated session {} for user: {}",
275            Self::mask_session_id(session_id),
276            user_name
277        );
278
279        let session = Session {
280            session_id,
281            user_name: user_name.clone(),
282            space_name: None,
283            graph_addr: Some(self.host_addr.clone()),
284            timezone: None,
285        };
286
287        let client_session = ClientSession::new(session);
288        client_session.set_auto_commit(self.default_auto_commit.load(Ordering::SeqCst));
289
290        // Add to sessions and active sessions
291        let create_time = SystemTime::now();
292
293        // DashMap does not require explicit locking; concurrent insertions are possible without any issues.
294        self.sessions
295            .insert(session_id, Arc::clone(&client_session));
296        self.active_sessions.insert(session_id, Instant::now());
297        self.total_sessions_created.fetch_add(1, Ordering::Relaxed);
298
299        // Write lock protection creation time
300        {
301            let mut create_times = self.session_create_times.write();
302            create_times.insert(session_id, create_time);
303        }
304
305        info!(
306            "Successfully created session {} for user: {}",
307            Self::mask_session_id(session_id),
308            user_name
309        );
310        self.emit_session_event(SessionEvent::SessionCreated {
311            session_id,
312            user_name,
313        });
314        Ok(client_session)
315    }
316
317    /// Finds an existing session
318    pub fn find_session(&self, session_id: i64) -> Option<Arc<ClientSession>> {
319        // DashMap supports true concurrent reading without the need for locking.
320        self.sessions.get(&session_id).map(|entry| entry.clone())
321    }
322
323    /// Finds an existing session only from local cache
324    pub fn find_session_from_cache(&self, session_id: i64) -> Option<Arc<ClientSession>> {
325        self.find_session(session_id)
326    }
327
328    /// Removes a session from local cache
329    pub async fn remove_session(&self, session_id: i64) {
330        info!("Removing session {}", Self::mask_session_id(session_id));
331
332        if let Some(entry) = self.sessions.get(&session_id) {
333            entry.value().mark_all_queries_killed();
334            entry.value().close_all_cursors();
335        }
336        // DashMap does not require explicit locking.
337        let existed = self.sessions.remove(&session_id).is_some();
338        self.active_sessions.remove(&session_id);
339
340        // Write lock protection creation time
341        {
342            let mut create_times = self.session_create_times.write();
343            create_times.remove(&session_id);
344        }
345
346        info!(
347            "Successfully removed session {}",
348            Self::mask_session_id(session_id)
349        );
350        if existed {
351            self.emit_session_event(SessionEvent::SessionDestroyed { session_id });
352        }
353    }
354
355    /// Invalidate all sessions of a user, optionally keeping one session.
356    ///
357    /// Used when a user is disabled or dropped so existing sessions stop
358    /// working immediately. Returns the removed session ids.
359    pub async fn remove_sessions_by_username(
360        &self,
361        username: &str,
362        except_session_id: Option<i64>,
363    ) -> Vec<i64> {
364        let targets: Vec<i64> = self
365            .sessions
366            .iter()
367            .filter(|entry| {
368                entry.value().user() == username && Some(*entry.key()) != except_session_id
369            })
370            .map(|entry| *entry.key())
371            .collect();
372        for session_id in &targets {
373            self.remove_session(*session_id).await;
374        }
375        targets
376    }
377
378    /// Gets all sessions from the local cache
379    pub fn get_sessions_from_local_cache(&self) -> Vec<Session> {
380        // DashMap supports iterators, and no locking is required.
381        self.sessions
382            .iter()
383            .map(|entry| entry.value().get_session())
384            .collect()
385    }
386
387    /// Obtain information about the session list, which is used for the SHOW SESSIONS command.
388    pub async fn list_sessions(&self) -> Vec<SessionInfo> {
389        // Read lock acquisition time
390        let create_times = self.session_create_times.read();
391
392        // DashMap’s iteration process does not require locking.
393        self.sessions
394            .iter()
395            .filter_map(|entry| {
396                let session_id = entry.key();
397                let client_session = entry.value();
398                create_times.get(session_id).map(|&create_time| {
399                    SessionInfo::from_client_session(client_session, create_time)
400                })
401            })
402            .collect()
403    }
404
405    /// Obtain detailed information about the specified session.
406    pub async fn get_session_info(&self, session_id: i64) -> Option<SessionInfo> {
407        // Reading DashMap does not require any locking mechanisms.
408        let client_session = self.sessions.get(&session_id)?;
409
410        // Read lock acquisition time
411        let create_times = self.session_create_times.read();
412        create_times
413            .get(&session_id)
414            .map(|&create_time| SessionInfo::from_client_session(&client_session, create_time))
415    }
416
417    /// Terminate the specified session (KILL SESSION)
418    ///
419    /// # Parameters
420    /// `session_id` – The ID of the session that needs to be terminated.
421    /// `current_user` – The username of the user who is performing the termination operation.
422    /// `is_admin` – Determines whether the current user has the Admin role.
423    ///
424    /// # Return
425    /// * `Ok(())` - Successfully terminate the session
426    /// * `Err(SessionError)` - Specific reasons for termination failure
427    pub async fn kill_session(
428        &self,
429        session_id: i64,
430        current_user: &str,
431        is_admin: bool,
432    ) -> SessionResult<()> {
433        info!(
434            "Attempting to kill session {} by user: {} (is_admin: {})",
435            Self::mask_session_id(session_id),
436            current_user,
437            is_admin
438        );
439
440        // Find the target conversation.
441        let target_session = self
442            .find_session(session_id)
443            .ok_or(SessionError::session_not_found(session_id))?;
444
445        let target_user = target_session.user();
446
447        // Permission check: You can only terminate your own session, or you must have Admin privileges.
448        if !is_admin && target_user != current_user {
449            warn!(
450                "User {} attempted to kill session {} without permission (target user: {})",
451                current_user, session_id, target_user
452            );
453            return Err(SessionError::insufficient_permission());
454        }
455
456        info!(
457            "Killing session {} (user: {}, active queries: {})",
458            Self::mask_session_id(session_id),
459            target_user,
460            target_session.active_queries_count()
461        );
462
463        // Remove the session from the manager, which terminates all
464        // queries and closes all cursors before dropping the entry.
465        self.remove_session(session_id).await;
466
467        info!(
468            "Successfully killed session {} by user: {}",
469            Self::mask_session_id(session_id),
470            current_user
471        );
472        Ok(())
473    }
474
475    /// Batch termination of multiple sessions
476    pub async fn kill_multiple_sessions(
477        &self,
478        session_ids: &[i64],
479        current_user: &str,
480        is_admin: bool,
481    ) -> Vec<SessionResult<()>> {
482        let mut results = Vec::with_capacity(session_ids.len());
483        for &session_id in session_ids {
484            results.push(self.kill_session(session_id, current_user, is_admin).await);
485        }
486        results
487    }
488
489    /// Whether exceeds the max allowed connections
490    pub async fn is_out_of_connections(&self) -> bool {
491        // DashMap's len() is an O(1) operation, no lock required
492        self.active_sessions.len() >= self.max_connections
493    }
494
495    /// Obtain the number of active sessions
496    pub async fn active_session_count(&self) -> usize {
497        self.active_sessions.len()
498    }
499
500    /// Cumulative number of sessions created since process start.
501    pub fn total_sessions_created(&self) -> u64 {
502        self.total_sessions_created.load(Ordering::Relaxed)
503    }
504
505    /// Obtaining the maximum limit for the number of connections
506    pub fn max_connections(&self) -> usize {
507        self.max_connections
508    }
509
510    /// Generate a cryptographically random positive session ID.
511    ///
512    /// High-entropy 63-bit random values make session IDs unguessable even on
513    /// public deployments, eliminating session-hijacking via brute force.
514    fn generate_session_id(&self) -> i64 {
515        let mut rng = rand::thread_rng();
516        loop {
517            let raw = rng.next_u64();
518            // Keep the sign bit clear so we always emit positive i64 values.
519            let candidate = (raw & 0x7FFF_FFFF_FFFF_FFFF) as i64;
520            if candidate > 0 {
521                return candidate;
522            }
523        }
524    }
525
526    /// Background task: Regularly clean up expired sessions.
527    ///
528    /// Check every 30 seconds and remove sessions that have exceeded the idle timeout period.
529    /// Can be stopped via the `stop_cleanup_task()` method
530    async fn background_reclamation_task(self: Arc<Self>) {
531        let mut interval = time::interval(Duration::from_secs(30));
532
533        loop {
534            interval.tick().await;
535
536            // Check whether it should be stopped.
537            if !self.cleanup_task_running.load(Ordering::SeqCst) {
538                info!("Session cleanup task is stopping");
539                break;
540            }
541
542            self.reclaim_expired_sessions().await;
543        }
544
545        info!("Session cleanup task has stopped");
546    }
547
548    /// Reclaims expired sessions
549    async fn reclaim_expired_sessions(&self) {
550        // DashMap supports iteration without the need for locking.
551        let expired_sessions: Vec<i64> = self
552            .active_sessions
553            .iter()
554            .filter(|entry| entry.value().elapsed() > self.session_idle_timeout)
555            .map(|entry| *entry.key())
556            .collect();
557
558        if !expired_sessions.is_empty() {
559            info!(
560                "Found {} expired sessions to reclaim",
561                expired_sessions.len()
562            );
563        }
564
565        // Remove expired sessions
566        for session_id in expired_sessions {
567            info!(
568                "Reclaiming expired session {}",
569                Self::mask_session_id(session_id)
570            );
571            self.remove_session(session_id).await;
572        }
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579
580    fn create_test_session_manager() -> Arc<GraphSessionManager> {
581        GraphSessionManager::new(
582            "127.0.0.1:9669".to_string(),
583            DEFAULT_MAX_ALLOWED_CONNECTIONS,
584            DEFAULT_SESSION_IDLE_TIMEOUT,
585        )
586    }
587
588    #[tokio::test]
589    async fn test_session_manager_creation() {
590        let session_manager = create_test_session_manager();
591
592        assert_eq!(session_manager.host_addr, "127.0.0.1:9669");
593        assert_eq!(session_manager.get_sessions_from_local_cache().len(), 0);
594        assert!(!session_manager.is_cleanup_task_running());
595    }
596
597    #[tokio::test]
598    async fn test_default_auto_commit_applies_to_new_sessions() {
599        let session_manager = create_test_session_manager();
600
601        let session = session_manager
602            .create_session("autocommit".into(), "127.0.0.1".to_string())
603            .await
604            .expect("Failed to create session");
605        assert!(session.is_auto_commit());
606
607        session_manager.set_default_auto_commit(false);
608        let session = session_manager
609            .create_session("explicit".into(), "127.0.0.1".to_string())
610            .await
611            .expect("Failed to create session");
612        assert!(!session.is_auto_commit());
613    }
614
615    #[tokio::test]
616    async fn test_create_and_find_session() {
617        let session_manager = create_test_session_manager();
618
619        let session = session_manager
620            .create_session("testuser".into(), "127.0.0.1".to_string())
621            .await
622            .expect("Failed to create session");
623
624        assert_eq!(session.user(), "testuser");
625        assert!(!session_manager.is_out_of_connections().await);
626
627        let found_session = session_manager
628            .find_session(session.id())
629            .expect("Failed to find session");
630        assert_eq!(found_session.user(), "testuser");
631
632        // Test find non-existent session
633        assert!(session_manager.find_session(999999).is_none());
634    }
635
636    #[tokio::test]
637    async fn test_remove_session() {
638        let session_manager = create_test_session_manager();
639
640        let session = session_manager
641            .create_session("testuser".into(), "127.0.0.1".to_string())
642            .await
643            .expect("Failed to create session");
644
645        assert!(session_manager.find_session(session.id()).is_some());
646
647        session_manager.remove_session(session.id()).await;
648        assert!(session_manager.find_session(session.id()).is_none());
649    }
650
651    #[tokio::test]
652    async fn test_max_connections() {
653        let session_manager = GraphSessionManager::new(
654            "127.0.0.1:9669".to_string(),
655            5,
656            DEFAULT_SESSION_IDLE_TIMEOUT,
657        );
658
659        assert!(!session_manager.is_out_of_connections().await);
660
661        for i in 0..5 {
662            let _ = session_manager
663                .create_session(format!("user{}", i), "127.0.0.1".to_string())
664                .await;
665        }
666
667        assert!(session_manager.is_out_of_connections().await);
668
669        // Attempts to create a 6th session should fail
670        let result = session_manager
671            .create_session("user6".into(), "127.0.0.1".to_string())
672            .await;
673        assert!(result.is_err());
674    }
675
676    #[tokio::test]
677    async fn test_kill_session() {
678        let session_manager = create_test_session_manager();
679
680        let session = session_manager
681            .create_session("testuser".into(), "127.0.0.1".to_string())
682            .await
683            .expect("Failed to create session");
684
685        let session_id = session.id();
686
687        // Normal users try to terminate their own session - should succeed
688        let result = session_manager
689            .kill_session(session_id, "testuser", false)
690            .await;
691        assert!(result.is_ok());
692        assert!(session_manager.find_session(session_id).is_none());
693
694        // Create a new session to test permission checking
695        let session2 = session_manager
696            .create_session("user2".into(), "127.0.0.1".to_string())
697            .await
698            .expect("Failed to create session");
699
700        // Normal user attempts to terminate another user's session - should fail
701        let result = session_manager
702            .kill_session(session2.id(), "otheruser", false)
703            .await;
704        assert!(result.is_err());
705
706        // Admin Terminate another user's session - should succeed!
707        let result = session_manager
708            .kill_session(session2.id(), "admin", true)
709            .await;
710        assert!(result.is_ok());
711    }
712
713    #[tokio::test]
714    async fn test_list_sessions() {
715        let session_manager = create_test_session_manager();
716
717        // Creating Multiple Sessions
718        for i in 0..3 {
719            let _ = session_manager
720                .create_session(format!("user{}", i), "127.0.0.1".to_string())
721                .await;
722        }
723
724        let sessions = session_manager.list_sessions().await;
725        assert_eq!(sessions.len(), 3);
726    }
727
728    #[tokio::test]
729    async fn test_concurrent_access() {
730        use tokio::task;
731
732        let session_manager = create_test_session_manager();
733        let mut handles = vec![];
734
735        // Concurrent Session Creation
736        for i in 0..10 {
737            let manager = Arc::clone(&session_manager);
738            let handle = task::spawn(async move {
739                manager
740                    .create_session(format!("user{}", i), "127.0.0.1".to_string())
741                    .await
742            });
743            handles.push(handle);
744        }
745
746        // Waiting for all tasks to be completed
747        for handle in handles {
748            let _ = handle.await.expect("Task should complete");
749        }
750
751        // Verify that all sessions were created successfully
752        assert_eq!(session_manager.get_sessions_from_local_cache().len(), 10);
753    }
754}