Skip to main content

libpetri_debug/
debug_session_registry.rs

1//! Registry for managing Petri net debug sessions.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use libpetri_core::petri_net::PetriNet;
8use libpetri_export::dot_exporter::dot_export;
9use libpetri_export::mapper::sanitize;
10
11use crate::debug_event_store::DebugEventStore;
12use crate::debug_response::{NetStructure, PlaceInfo, TransitionInfo};
13use crate::place_analysis::PlaceAnalysis;
14
15/// A registered debug session.
16pub struct DebugSession {
17    pub session_id: String,
18    pub net_name: String,
19    pub dot_diagram: String,
20    pub places: Option<PlaceAnalysis>,
21    pub transition_names: Vec<String>,
22    pub event_store: Arc<DebugEventStore>,
23    pub start_time: u64,
24    pub active: bool,
25    pub imported_structure: Option<NetStructure>,
26}
27
28/// Callback invoked when a session completes.
29pub type SessionCompletionListener = Box<dyn Fn(&DebugSession) + Send + Sync>;
30
31/// Builds the `NetStructure` from a session's stored place and transition info.
32pub fn build_net_structure(session: &DebugSession) -> NetStructure {
33    if let Some(ref imported) = session.imported_structure {
34        return imported.clone();
35    }
36
37    let Some(ref places) = session.places else {
38        return NetStructure {
39            places: Vec::new(),
40            transitions: Vec::new(),
41        };
42    };
43
44    let place_infos: Vec<PlaceInfo> = places
45        .data()
46        .iter()
47        .map(|(name, info)| PlaceInfo {
48            name: name.clone(),
49            graph_id: format!("p_{}", sanitize(name)),
50            token_type: info.token_type.clone(),
51            is_start: !info.has_incoming,
52            is_end: !info.has_outgoing,
53            is_environment: false,
54        })
55        .collect();
56
57    let transition_infos: Vec<TransitionInfo> = session
58        .transition_names
59        .iter()
60        .map(|name| TransitionInfo {
61            name: name.clone(),
62            graph_id: format!("t_{}", sanitize(name)),
63        })
64        .collect();
65
66    NetStructure {
67        places: place_infos,
68        transitions: transition_infos,
69    }
70}
71
72/// Factory function for creating `DebugEventStore` instances.
73pub type EventStoreFactory = Box<dyn Fn(&str) -> DebugEventStore + Send + Sync>;
74
75/// Registry for managing debug sessions.
76pub struct DebugSessionRegistry {
77    sessions: HashMap<String, DebugSession>,
78    max_sessions: usize,
79    event_store_factory: EventStoreFactory,
80    completion_listeners: Vec<SessionCompletionListener>,
81}
82
83impl DebugSessionRegistry {
84    /// Creates a new registry with default settings.
85    pub fn new() -> Self {
86        Self::with_options(50, None, Vec::new())
87    }
88
89    /// Creates a registry with custom options.
90    pub fn with_options(
91        max_sessions: usize,
92        event_store_factory: Option<EventStoreFactory>,
93        completion_listeners: Vec<SessionCompletionListener>,
94    ) -> Self {
95        Self {
96            sessions: HashMap::new(),
97            max_sessions,
98            event_store_factory: event_store_factory
99                .unwrap_or_else(|| Box::new(|id: &str| DebugEventStore::new(id.to_string()))),
100            completion_listeners,
101        }
102    }
103
104    /// Registers a new debug session for the given Petri net.
105    pub fn register(&mut self, session_id: String, net: &PetriNet) -> Arc<DebugEventStore> {
106        let dot_diagram = dot_export(net, None);
107        let places = PlaceAnalysis::from_net(net);
108        let event_store = Arc::new((self.event_store_factory)(&session_id));
109
110        let transition_names: Vec<String> = net
111            .transitions()
112            .iter()
113            .map(|t| t.name().to_string())
114            .collect();
115
116        let session = DebugSession {
117            session_id: session_id.clone(),
118            net_name: net.name().to_string(),
119            dot_diagram,
120            places: Some(places),
121            transition_names,
122            event_store: Arc::clone(&event_store),
123            start_time: now_ms(),
124            active: true,
125            imported_structure: None,
126        };
127
128        self.evict_if_necessary();
129        self.sessions.insert(session_id, session);
130        event_store
131    }
132
133    /// Marks a session as completed.
134    pub fn complete(&mut self, session_id: &str) {
135        if let Some(session) = self.sessions.get_mut(session_id) {
136            session.active = false;
137            for listener in &self.completion_listeners {
138                listener(session);
139            }
140        }
141    }
142
143    /// Removes a session from the registry.
144    pub fn remove(&mut self, session_id: &str) -> Option<DebugSession> {
145        let removed = self.sessions.remove(session_id);
146        if let Some(ref session) = removed {
147            session.event_store.close();
148        }
149        removed
150    }
151
152    /// Returns a reference to a session by ID.
153    pub fn get_session(&self, session_id: &str) -> Option<&DebugSession> {
154        self.sessions.get(session_id)
155    }
156
157    /// Lists sessions, ordered by start time (most recent first).
158    pub fn list_sessions(&self, limit: usize) -> Vec<&DebugSession> {
159        let mut sessions: Vec<&DebugSession> = self.sessions.values().collect();
160        sessions.sort_by(|a, b| b.start_time.cmp(&a.start_time));
161        sessions.truncate(limit);
162        sessions
163    }
164
165    /// Lists only active sessions.
166    pub fn list_active_sessions(&self, limit: usize) -> Vec<&DebugSession> {
167        let mut sessions: Vec<&DebugSession> =
168            self.sessions.values().filter(|s| s.active).collect();
169        sessions.sort_by(|a, b| b.start_time.cmp(&a.start_time));
170        sessions.truncate(limit);
171        sessions
172    }
173
174    /// Total number of sessions.
175    pub fn size(&self) -> usize {
176        self.sessions.len()
177    }
178
179    /// Registers an imported (archived) session as inactive.
180    pub fn register_imported(
181        &mut self,
182        session_id: String,
183        net_name: String,
184        dot_diagram: String,
185        structure: NetStructure,
186        event_store: Arc<DebugEventStore>,
187        start_time: u64,
188    ) {
189        self.evict_if_necessary();
190
191        let session = DebugSession {
192            session_id: session_id.clone(),
193            net_name,
194            dot_diagram,
195            places: None,
196            transition_names: Vec::new(),
197            event_store,
198            start_time,
199            active: false,
200            imported_structure: Some(structure),
201        };
202
203        self.sessions.insert(session_id, session);
204    }
205
206    fn evict_if_necessary(&mut self) {
207        if self.sessions.len() < self.max_sessions {
208            return;
209        }
210
211        // Sort: inactive first, then oldest
212        let mut candidates: Vec<(&String, bool, u64)> = self
213            .sessions
214            .iter()
215            .map(|(id, s)| (id, s.active, s.start_time))
216            .collect();
217        candidates.sort_by(|a, b| {
218            if a.1 != b.1 {
219                return if a.1 {
220                    std::cmp::Ordering::Greater
221                } else {
222                    std::cmp::Ordering::Less
223                };
224            }
225            a.2.cmp(&b.2)
226        });
227
228        let to_remove: Vec<String> = candidates
229            .iter()
230            .take_while(|_| self.sessions.len() >= self.max_sessions)
231            .map(|(id, _, _)| (*id).clone())
232            .collect();
233
234        for id in to_remove {
235            if self.sessions.len() < self.max_sessions {
236                break;
237            }
238            if let Some(session) = self.sessions.remove(&id) {
239                session.event_store.close();
240            }
241        }
242    }
243}
244
245impl Default for DebugSessionRegistry {
246    fn default() -> Self {
247        Self::new()
248    }
249}
250
251fn now_ms() -> u64 {
252    SystemTime::now()
253        .duration_since(UNIX_EPOCH)
254        .unwrap_or_default()
255        .as_millis() as u64
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use libpetri_core::input::one;
262    use libpetri_core::output::out_place;
263    use libpetri_core::place::Place;
264    use libpetri_core::transition::Transition;
265
266    fn test_net() -> PetriNet {
267        let p1 = Place::<i32>::new("p1");
268        let p2 = Place::<i32>::new("p2");
269        let t = Transition::builder("t1")
270            .input(one(&p1))
271            .output(out_place(&p2))
272            .build();
273        PetriNet::builder("test").transition(t).build()
274    }
275
276    #[test]
277    fn register_and_get_session() {
278        let mut registry = DebugSessionRegistry::new();
279        let net = test_net();
280        let _store = registry.register("s1".into(), &net);
281
282        let session = registry.get_session("s1").unwrap();
283        assert_eq!(session.net_name, "test");
284        assert!(session.active);
285        assert!(!session.dot_diagram.is_empty());
286    }
287
288    #[test]
289    fn complete_session() {
290        let mut registry = DebugSessionRegistry::new();
291        let net = test_net();
292        let _store = registry.register("s1".into(), &net);
293
294        registry.complete("s1");
295        let session = registry.get_session("s1").unwrap();
296        assert!(!session.active);
297    }
298
299    #[test]
300    fn list_sessions() {
301        let mut registry = DebugSessionRegistry::new();
302        let net = test_net();
303        let _s1 = registry.register("s1".into(), &net);
304        let _s2 = registry.register("s2".into(), &net);
305
306        assert_eq!(registry.list_sessions(10).len(), 2);
307        assert_eq!(registry.size(), 2);
308    }
309
310    #[test]
311    fn list_active_sessions() {
312        let mut registry = DebugSessionRegistry::new();
313        let net = test_net();
314        let _s1 = registry.register("s1".into(), &net);
315        let _s2 = registry.register("s2".into(), &net);
316        registry.complete("s1");
317
318        assert_eq!(registry.list_active_sessions(10).len(), 1);
319    }
320
321    #[test]
322    fn remove_session() {
323        let mut registry = DebugSessionRegistry::new();
324        let net = test_net();
325        let _store = registry.register("s1".into(), &net);
326
327        let removed = registry.remove("s1");
328        assert!(removed.is_some());
329        assert!(registry.get_session("s1").is_none());
330        assert_eq!(registry.size(), 0);
331    }
332
333    #[test]
334    fn build_net_structure_from_live_session() {
335        let mut registry = DebugSessionRegistry::new();
336        let net = test_net();
337        let _store = registry.register("s1".into(), &net);
338
339        let session = registry.get_session("s1").unwrap();
340        let structure = build_net_structure(session);
341
342        assert_eq!(structure.places.len(), 2);
343        assert_eq!(structure.transitions.len(), 1);
344
345        let p1 = structure.places.iter().find(|p| p.name == "p1").unwrap();
346        assert_eq!(p1.graph_id, "p_p1");
347        assert!(p1.is_start);
348        assert!(!p1.is_end);
349
350        let p2 = structure.places.iter().find(|p| p.name == "p2").unwrap();
351        assert!(p2.is_end);
352        assert!(!p2.is_start);
353
354        assert_eq!(structure.transitions[0].name, "t1");
355        assert_eq!(structure.transitions[0].graph_id, "t_t1");
356    }
357
358    #[test]
359    fn eviction_at_capacity() {
360        let mut registry = DebugSessionRegistry::with_options(2, None, Vec::new());
361        let net = test_net();
362
363        let _s1 = registry.register("s1".into(), &net);
364        let _s2 = registry.register("s2".into(), &net);
365        registry.complete("s1");
366        // s3 should evict s1 (inactive, oldest)
367        let _s3 = registry.register("s3".into(), &net);
368
369        assert_eq!(registry.size(), 2);
370        assert!(registry.get_session("s1").is_none());
371        assert!(registry.get_session("s2").is_some());
372        assert!(registry.get_session("s3").is_some());
373    }
374}