Skip to main content

consortium/engine/
mod.rs

1//! I/O engine abstraction.
2//!
3//! Rust implementation of `ClusterShell.Engine`.
4//!
5//! The Python version has EPoll, Poll, and Select backends.
6//! In Rust we will likely use a single backend (mio or polling crate).
7
8use slotmap::{new_key_type, SlotMap};
9use std::cmp::Reverse;
10use std::collections::{BinaryHeap, HashMap};
11use std::io;
12use std::os::unix::io::RawFd;
13use thiserror::Error;
14
15// ============================================================================
16// Constants
17// ============================================================================
18
19/// Read event flag
20pub const E_READ: u32 = 0x1;
21
22/// Write event flag
23pub const E_WRITE: u32 = 0x2;
24
25/// Time epsilon for floating point comparisons
26pub const EPSILON: f64 = 1.0e-3;
27
28/// Unlimited fanout value
29pub const FANOUT_UNLIMITED: i32 = -1;
30
31// ============================================================================
32// Error Types
33// ============================================================================
34
35/// Error types for the Engine module
36#[derive(Error, Debug)]
37pub enum EngineError {
38    /// Operation aborted, optionally with kill flag
39    #[error("Operation aborted (kill={kill})")]
40    Abort { kill: bool },
41
42    /// Operation timed out
43    #[error("Operation timed out")]
44    Timeout,
45
46    /// Illegal operation was attempted
47    #[error("Illegal operation: {0}")]
48    IllegalOperation(String),
49
50    /// Engine is already running
51    #[error("Engine is already running")]
52    AlreadyRunning,
53
54    /// Operation not supported
55    #[error("Not supported: {0}")]
56    NotSupported(String),
57
58    /// I/O error
59    #[error("I/O error: {0}")]
60    Io(#[from] io::Error),
61}
62
63// ============================================================================
64// Timer ID
65// ============================================================================
66
67new_key_type! { pub struct TimerId; }
68
69// ============================================================================
70// EngineBaseTimer
71// ============================================================================
72
73/// Base timer structure with fire delay, interval, and autoclose settings.
74///
75/// This is the core timer struct that Engine uses. It can be armed/disarmed
76/// and supports repeating timers via interval.
77#[derive(Debug, Clone)]
78pub struct EngineBaseTimer {
79    fire_delay: f64,
80    interval: f64,
81    autoclose: bool,
82}
83
84impl EngineBaseTimer {
85    /// Create a new EngineBaseTimer
86    ///
87    /// # Arguments
88    /// * `fire_delay` - Seconds until timer fires (-1.0 means not armed)
89    /// * `interval` - Seconds between repeats (-1.0 means no repeat)
90    /// * `autoclose` - Whether to autoclose the client when timer fires
91    pub fn new(fire_delay: f64, interval: f64, autoclose: bool) -> Self {
92        Self {
93            fire_delay,
94            interval,
95            autoclose,
96        }
97    }
98
99    /// Invalidate this timer (disarm it)
100    pub fn invalidate(&mut self) {
101        self.fire_delay = -1.0;
102    }
103
104    /// Check if this timer is valid (armed)
105    pub fn is_valid(&self) -> bool {
106        self.fire_delay >= 0.0
107    }
108
109    /// Set the next fire delay and interval
110    ///
111    /// # Arguments
112    /// * `fire_delay` - Seconds until next fire (-1.0 means disarm)
113    /// * `interval` - Seconds between repeats (-1.0 means no repeat)
114    pub fn set_nextfire(&mut self, fire_delay: f64, interval: f64) {
115        self.fire_delay = fire_delay;
116        self.interval = interval;
117    }
118
119    /// Get the fire delay
120    pub fn fire_delay(&self) -> f64 {
121        self.fire_delay
122    }
123
124    /// Get the interval
125    pub fn interval(&self) -> f64 {
126        self.interval
127    }
128
129    /// Get the autoclose flag
130    pub fn autoclose(&self) -> bool {
131        self.autoclose
132    }
133}
134
135// ============================================================================
136// TimerQueue (Internal)
137// ============================================================================
138
139/// Internal timer entry used by TimerQueue
140#[derive(Debug, Clone, PartialEq)]
141struct TimerEntry {
142    fire_date: f64,
143    timer_id: TimerId,
144    armed: bool,
145}
146
147impl Eq for TimerEntry {}
148
149impl Ord for TimerEntry {
150    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
151        // Reverse ordering: lower fire_date comes first (min-heap behavior)
152        self.fire_date
153            .partial_cmp(&other.fire_date)
154            .unwrap_or(std::cmp::Ordering::Equal)
155            .then_with(|| self.timer_id.cmp(&other.timer_id))
156    }
157}
158
159impl PartialOrd for TimerEntry {
160    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
161        Some(self.cmp(other))
162    }
163}
164
165/// Timer queue managing a set of timers using a BinaryHeap.
166///
167/// Timers are ordered by their fire_date for efficient expiration checking.
168/// Repeating timers are automatically rearmed after firing.
169#[derive(Debug)]
170pub struct TimerQueue {
171    timers: BinaryHeap<Reverse<TimerEntry>>,
172    timer_data: SlotMap<TimerId, EngineBaseTimer>,
173    armed_count: usize,
174    current_time: f64,
175}
176
177impl TimerQueue {
178    /// Create a new empty TimerQueue
179    pub fn new() -> Self {
180        Self {
181            timers: BinaryHeap::new(),
182            timer_data: SlotMap::with_capacity_and_key(16),
183            armed_count: 0,
184            current_time: 0.0,
185        }
186    }
187
188    /// Schedule a timer for future firing
189    ///
190    /// # Arguments
191    /// * `timer_id` - The ID of the timer to schedule
192    pub fn schedule(&mut self, timer_id: TimerId) {
193        if let Some(timer) = self.timer_data.get(timer_id) {
194            if timer.fire_delay >= 0.0 {
195                let fire_date = self.current_time + timer.fire_delay;
196                let entry = TimerEntry {
197                    fire_date,
198                    timer_id,
199                    armed: true,
200                };
201                self.timers.push(Reverse(entry));
202                self.armed_count += 1;
203            }
204        }
205    }
206
207    /// Reschedule an existing timer
208    ///
209    /// # Arguments
210    /// * `timer_id` - The ID of the timer to reschedule
211    pub fn reschedule(&mut self, timer_id: TimerId) {
212        // Remove old entry if it exists
213        self.invalidate(timer_id);
214        self.schedule(timer_id);
215    }
216
217    /// Invalidate (disarm) a timer
218    ///
219    /// # Arguments
220    /// * `timer_id` - The ID of the timer to invalidate
221    pub fn invalidate(&mut self, timer_id: TimerId) {
222        // Find and remove the timer entry
223        let mut found = false;
224        let mut new_heap = BinaryHeap::new();
225        for Reverse(entry) in self.timers.drain() {
226            if entry.timer_id == timer_id {
227                found = true;
228                // Update the timer state
229                if let Some(timer) = self.timer_data.get_mut(timer_id) {
230                    timer.invalidate();
231                }
232            } else {
233                new_heap.push(Reverse(entry));
234            }
235        }
236        self.timers = new_heap;
237        if found {
238            self.armed_count = self.armed_count.saturating_sub(1);
239        }
240    }
241
242    /// Fire all expired timers
243    ///
244    /// Returns a list of (timer_id, autoclose) for timers that fired
245    pub fn fire_expired(&mut self) -> Vec<(TimerId, bool)> {
246        let mut fired = Vec::new();
247        let now = self.current_time;
248
249        // Process all timers that have expired
250        while let Some(Reverse(entry)) = self.timers.peek() {
251            if entry.fire_date > now + EPSILON {
252                break;
253            }
254
255            let entry = self.timers.pop().unwrap().0;
256            let timer_id = entry.timer_id;
257
258            if let Some(timer) = self.timer_data.get(timer_id) {
259                if entry.armed && timer.fire_delay >= 0.0 {
260                    fired.push((timer_id, timer.autoclose));
261
262                    // If this is a repeating timer, reschedule it
263                    if timer.interval >= 0.0 {
264                        let new_fire_date = now + timer.interval;
265                        let new_entry = TimerEntry {
266                            fire_date: new_fire_date,
267                            timer_id,
268                            armed: true,
269                        };
270                        self.timers.push(Reverse(new_entry));
271                    } else {
272                        // One-shot timer: disarm it
273                        if let Some(t) = self.timer_data.get_mut(timer_id) {
274                            t.invalidate();
275                        }
276                        self.armed_count = self.armed_count.saturating_sub(1);
277                    }
278                }
279            }
280        }
281
282        fired
283    }
284
285    /// Get the time until the next timer fires
286    ///
287    /// Returns None if no timers are armed
288    pub fn nextfire_delay(&self) -> Option<f64> {
289        if let Some(Reverse(entry)) = self.timers.peek() {
290            let delay = entry.fire_date - self.current_time;
291            if delay > EPSILON {
292                Some(delay)
293            } else {
294                Some(0.0)
295            }
296        } else {
297            None
298        }
299    }
300
301    /// Clear all timers
302    pub fn clear(&mut self) {
303        self.timers.clear();
304        self.timer_data.clear();
305        self.armed_count = 0;
306    }
307
308    /// Add a timer to the queue
309    ///
310    /// Returns the TimerId for the new timer
311    pub fn add_timer(&mut self, timer: EngineBaseTimer) -> TimerId {
312        self.timer_data.insert(timer)
313    }
314
315    /// Remove a timer by ID
316    pub fn remove_timer(&mut self, timer_id: TimerId) {
317        self.invalidate(timer_id);
318        self.timer_data.remove(timer_id);
319    }
320
321    /// Get the number of armed timers
322    pub fn armed_count(&self) -> usize {
323        self.armed_count
324    }
325
326    /// Update the current time (should be called before fire_expired)
327    pub fn update_time(&mut self, time: f64) {
328        self.current_time = time;
329    }
330
331    /// Check if a timer is valid
332    pub fn is_valid(&self, timer_id: TimerId) -> bool {
333        self.timer_data
334            .get(timer_id)
335            .map(|t| t.is_valid())
336            .unwrap_or(false)
337    }
338}
339
340impl Default for TimerQueue {
341    fn default() -> Self {
342        Self::new()
343    }
344}
345
346// ============================================================================
347// EngineTimer (concrete timer with handler)
348// ============================================================================
349
350/// Concrete timer with a callback handler.
351///
352/// When the timer fires, the handler function is called. This mirrors
353/// Python's `EngineTimer` which has `_fire()` calling `handler.ev_timer()`.
354pub struct EngineTimer {
355    base: EngineBaseTimer,
356    handler: Box<dyn FnMut() + Send>,
357}
358
359impl EngineTimer {
360    /// Create a new EngineTimer.
361    ///
362    /// # Arguments
363    /// * `fire_delay` - Seconds until timer fires (-1.0 means not armed)
364    /// * `interval` - Seconds between repeats (-1.0 means no repeat)
365    /// * `autoclose` - Whether to autoclose the associated client
366    /// * `handler` - Callback invoked when the timer fires
367    pub fn new(
368        fire_delay: f64,
369        interval: f64,
370        autoclose: bool,
371        handler: impl FnMut() + Send + 'static,
372    ) -> Self {
373        Self {
374            base: EngineBaseTimer::new(fire_delay, interval, autoclose),
375            handler: Box::new(handler),
376        }
377    }
378
379    /// Fire the timer (invoke the handler).
380    pub fn fire(&mut self) {
381        (self.handler)();
382    }
383
384    /// Get a reference to the underlying EngineBaseTimer.
385    pub fn base(&self) -> &EngineBaseTimer {
386        &self.base
387    }
388
389    /// Get a mutable reference to the underlying EngineBaseTimer.
390    pub fn base_mut(&mut self) -> &mut EngineBaseTimer {
391        &mut self.base
392    }
393}
394
395impl std::fmt::Debug for EngineTimer {
396    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397        f.debug_struct("EngineTimer")
398            .field("base", &self.base)
399            .field("handler", &"<fn>")
400            .finish()
401    }
402}
403
404// ============================================================================
405// EngineClient trait
406// ============================================================================
407
408/// Trait for objects that can be managed by the Engine.
409///
410/// Clients are I/O objects (files, sockets, pipes) that the engine
411/// monitors for readability/writability and manages through their lifecycle.
412pub trait EngineClient: Send {
413    /// Start the client
414    fn start(&mut self) -> io::Result<()>;
415
416    /// Close the client
417    ///
418    /// # Arguments
419    /// * `abort` - Whether this is an abort close
420    /// * `timeout` - Whether this is a timeout close
421    fn close(&mut self, abort: bool, timeout: bool);
422
423    /// Get the file descriptor, if any
424    fn fd(&self) -> Option<RawFd>;
425
426    /// Get the event interest bitmask (E_READ, E_WRITE, etc.)
427    fn events(&self) -> u32;
428
429    /// Check if this client can be delayed (batched)
430    fn is_delayable(&self) -> bool;
431
432    /// Check if this client is currently registered with the engine
433    fn is_registered(&self) -> bool;
434
435    /// Set the registered state
436    fn set_registered(&mut self, val: bool);
437
438    /// Check if this client should be autoclosed
439    fn autoclose(&self) -> bool;
440
441    /// Handle a read event on this client.
442    /// Returns the number of bytes read, or 0 on EOF.
443    fn handle_read(&mut self) -> io::Result<usize> {
444        Ok(0)
445    }
446
447    /// Handle a write event on this client.
448    /// Returns the number of bytes written.
449    fn handle_write(&mut self) -> io::Result<usize> {
450        Ok(0)
451    }
452}
453
454// ============================================================================
455// Client ID
456// ============================================================================
457
458new_key_type! { pub struct ClientId; }
459
460// ============================================================================
461// PollBackend trait
462// ============================================================================
463
464/// Trait for I/O polling backends.
465///
466/// This trait abstracts the underlying polling mechanism (epoll, kqueue, poll, etc.)
467/// and allows for different implementations without changing the core Engine logic.
468pub trait PollBackend: Send + Sync {
469    /// Register a file descriptor for events
470    ///
471    /// # Arguments
472    /// * `fd` - The file descriptor to register
473    /// * `events` - Event bitmask (E_READ, E_WRITE)
474    /// * `client_id` - The client ID associated with this fd
475    fn register(&mut self, fd: RawFd, events: u32, client_id: ClientId) -> io::Result<()>;
476
477    /// Modify event registration for a file descriptor
478    fn modify(&mut self, fd: RawFd, events: u32, client_id: ClientId) -> io::Result<()>;
479
480    /// Unregister a file descriptor
481    fn unregister(&mut self, fd: RawFd) -> io::Result<()>;
482
483    /// Wait for events with optional timeout (in seconds)
484    ///
485    /// Returns a list of (fd, events, client_id) tuples for ready events
486    fn wait(&mut self, timeout: Option<f64>) -> io::Result<Vec<(RawFd, u32, ClientId)>>;
487
488    /// Get the backend name
489    fn name(&self) -> &str;
490}
491
492// ============================================================================
493// Engine struct
494// ============================================================================
495
496/// The main I/O event loop engine.
497///
498/// The Engine manages:
499/// - Timer scheduling and expiration
500/// - Client registration and event monitoring
501/// - The main event loop
502/// - Fanout control (maximum concurrent operations)
503///
504/// # Example
505/// ```no_run
506/// use consortium::engine::{Engine, EngineBaseTimer, EngineClient, PollBackend, ClientId, FANOUT_UNLIMITED};
507/// use std::os::unix::io::RawFd;
508///
509/// // Engine requires a concrete PollBackend implementation.
510/// // See NullBackend in tests for a minimal example.
511/// ```
512pub struct Engine<B: PollBackend> {
513    clients: SlotMap<ClientId, Box<dyn EngineClient>>,
514    timerq: TimerQueue,
515    reg_clifds: HashMap<RawFd, ClientId>,
516    poll_backend: B,
517    fanout: usize,
518    running: bool,
519    exited: bool,
520    evlooprefcnt: i32,
521}
522
523impl<B: PollBackend> Engine<B> {
524    /// Create a new Engine instance with the given backend.
525    ///
526    /// # Arguments
527    /// * `backend` - The polling backend implementation
528    /// * `fanout` - Maximum concurrent operations (0 for no limit)
529    pub fn with_backend(backend: B, fanout: usize) -> Self {
530        Self {
531            clients: SlotMap::with_key(),
532            timerq: TimerQueue::new(),
533            reg_clifds: HashMap::new(),
534            poll_backend: backend,
535            fanout,
536            running: false,
537            exited: false,
538            evlooprefcnt: 0,
539        }
540    }
541
542    /// Add a timer and return its ID
543    ///
544    /// # Arguments
545    /// * `timer` - The timer to add
546    pub fn add_timer(&mut self, timer: EngineBaseTimer) -> TimerId {
547        let timer_id = self.timerq.add_timer(timer);
548        self.timerq.schedule(timer_id);
549        timer_id
550    }
551
552    /// Remove a timer by ID
553    ///
554    /// # Arguments
555    /// * `id` - The timer ID to remove
556    pub fn remove_timer(&mut self, id: TimerId) {
557        self.timerq.remove_timer(id);
558    }
559
560    /// Fire all expired timers
561    pub fn fire_timers(&mut self) {
562        let _fired = self.timerq.fire_expired();
563        // Timer firing is handled internally by the timer queue.
564        // Client-associated timers will be integrated when the
565        // full client lifecycle is implemented.
566    }
567
568    /// Run the engine event loop
569    ///
570    /// # Arguments
571    /// * `timeout` - Optional timeout in seconds (None for no limit)
572    ///
573    /// Backend (poll) error handling mirrors upstream EPoll/Select
574    /// runloops: `Interrupted` (EINTR) errors are retried, while any other
575    /// backend error terminates the run loop cleanly and is propagated to
576    /// the caller as `EngineError::Io` (upstream ClusterShell 7a39bd8).
577    pub fn run(&mut self, timeout: Option<f64>) -> Result<(), EngineError> {
578        if self.running {
579            return Err(EngineError::AlreadyRunning);
580        }
581
582        self.running = true;
583        self.exited = false;
584
585        let start_time = self.timerq.current_time;
586        let max_time = timeout.map(|t| start_time + t);
587
588        while self.running {
589            // Update current time
590            self.timerq.update_time(self.timerq.current_time + 0.001);
591
592            // Fire any expired timers
593            self.fire_timers();
594
595            // Check if we should exit
596            if !self.running {
597                break;
598            }
599
600            // Get next timer delay
601            let next_timer_delay = self.timerq.nextfire_delay();
602
603            // Calculate timeout for poll
604            let poll_timeout = match (max_time, next_timer_delay) {
605                (Some(max), Some(timer_delay)) => {
606                    let remaining = max - self.timerq.current_time;
607                    if remaining <= EPSILON {
608                        self.running = false;
609                        self.exited = true;
610                        return Err(EngineError::Timeout);
611                    }
612                    // Use f64::min by comparing manually since f64 doesn't implement Ord
613                    Some(if remaining < timer_delay {
614                        remaining
615                    } else {
616                        timer_delay
617                    })
618                }
619                (Some(max), None) => {
620                    let remaining = max - self.timerq.current_time;
621                    if remaining <= EPSILON {
622                        self.running = false;
623                        self.exited = true;
624                        return Err(EngineError::Timeout);
625                    }
626                    Some(remaining)
627                }
628                (None, Some(timer_delay)) => Some(timer_delay),
629                (None, None) => None,
630            };
631
632            // Wait for events
633            let events = match self.poll_backend.wait(poll_timeout.map(|t| t.max(0.0))) {
634                Ok(events) => events,
635                Err(err) if err.kind() == io::ErrorKind::Interrupted => {
636                    // EINTR: might get interrupted by a signal — retry,
637                    // like the EPoll and Select runloops do.
638                    continue;
639                }
640                Err(err) => {
641                    // Non-EINTR backend errors must surface to the caller
642                    // instead of being swallowed (which would spin on a
643                    // stale event list) — terminate the loop cleanly.
644                    self.running = false;
645                    self.exited = true;
646                    return Err(EngineError::Io(err));
647                }
648            };
649
650            // Process events — dispatch read/write handlers
651            for (_fd, events_mask, client_id) in events {
652                if let Some(client) = self.clients.get_mut(client_id) {
653                    if events_mask & E_READ != 0 {
654                        let _ = client.handle_read();
655                    }
656                    if events_mask & E_WRITE != 0 {
657                        let _ = client.handle_write();
658                    }
659                }
660            }
661
662            // Check timeout
663            if let Some(max) = max_time {
664                if self.timerq.current_time >= max {
665                    self.running = false;
666                    self.exited = true;
667                    return Err(EngineError::Timeout);
668                }
669            }
670        }
671
672        self.exited = true;
673        self.running = false;
674        Ok(())
675    }
676
677    /// Abort the engine
678    ///
679    /// # Arguments
680    /// * `kill` - If true, force kill all clients
681    pub fn abort(&mut self, kill: bool) {
682        if kill {
683            // Force kill all clients
684            for (_, client) in self.clients.iter_mut() {
685                client.close(true, false);
686            }
687        }
688        self.running = false;
689    }
690
691    /// Check if the engine is running
692    pub fn is_running(&self) -> bool {
693        self.running
694    }
695
696    /// Check if the engine has exited
697    pub fn exited(&self) -> bool {
698        self.exited
699    }
700
701    /// Add a client to the engine
702    ///
703    /// # Arguments
704    /// * `client` - The client to add
705    pub fn add_client(&mut self, client: Box<dyn EngineClient>) -> Result<ClientId, EngineError> {
706        let client_id = self.clients.insert(client);
707
708        if let Some(client) = self.clients.get(client_id) {
709            if let Some(fd) = client.fd() {
710                if fd >= 0 {
711                    let events = client.events();
712                    self.poll_backend.register(fd, events, client_id)?;
713                    self.reg_clifds.insert(fd, client_id);
714                }
715            }
716        }
717
718        Ok(client_id)
719    }
720
721    /// Remove a client by ID
722    ///
723    /// # Arguments
724    /// * `id` - The client ID to remove
725    pub fn remove_client(&mut self, id: ClientId) {
726        if let Some(client) = self.clients.get(id) {
727            if let Some(fd) = client.fd() {
728                if fd >= 0 {
729                    let _ = self.poll_backend.unregister(fd);
730                    self.reg_clifds.remove(&fd);
731                }
732            }
733        }
734        self.clients.remove(id);
735    }
736
737    /// Get the fanout value
738    pub fn fanout(&self) -> usize {
739        self.fanout
740    }
741
742    /// Update client event interests
743    pub fn update_client_events(&mut self, client_id: ClientId) -> Result<(), EngineError> {
744        if let Some(client) = self.clients.get(client_id) {
745            if let Some(fd) = client.fd() {
746                if fd >= 0 {
747                    let events = client.events();
748                    if self.reg_clifds.contains_key(&fd) {
749                        self.poll_backend.modify(fd, events, client_id)?;
750                    } else {
751                        self.poll_backend.register(fd, events, client_id)?;
752                        self.reg_clifds.insert(fd, client_id);
753                    }
754                }
755            }
756        }
757        Ok(())
758    }
759
760    /// Stop the engine event loop.
761    pub fn stop(&mut self) {
762        self.running = false;
763    }
764
765    /// Increment the event loop reference count.
766    /// While refcnt > 0, the engine loop stays alive.
767    pub fn evloop_acquire(&mut self) {
768        self.evlooprefcnt += 1;
769    }
770
771    /// Decrement the event loop reference count.
772    /// When refcnt reaches 0, the engine stops.
773    pub fn evloop_release(&mut self) {
774        self.evlooprefcnt -= 1;
775        if self.evlooprefcnt <= 0 {
776            self.running = false;
777        }
778    }
779
780    /// Get the number of registered clients.
781    pub fn client_count(&self) -> usize {
782        self.clients.len()
783    }
784
785    /// Get the timer queue (immutable).
786    pub fn timerq(&self) -> &TimerQueue {
787        &self.timerq
788    }
789
790    /// Get the timer queue (mutable).
791    pub fn timerq_mut(&mut self) -> &mut TimerQueue {
792        &mut self.timerq
793    }
794
795    /// Start all registered clients.
796    pub fn start_all_clients(&mut self) -> io::Result<()> {
797        let ids: Vec<ClientId> = self.clients.keys().collect();
798        for id in ids {
799            if let Some(client) = self.clients.get_mut(id) {
800                client.start()?;
801            }
802        }
803        Ok(())
804    }
805}
806
807// ============================================================================
808// NullBackend — no-op backend for testing
809// ============================================================================
810
811/// A no-op polling backend that never returns events.
812///
813/// Useful for testing timer logic and client management without actual I/O.
814#[derive(Debug, Default)]
815pub struct NullBackend;
816
817impl PollBackend for NullBackend {
818    fn register(&mut self, _fd: RawFd, _events: u32, _client_id: ClientId) -> io::Result<()> {
819        Ok(())
820    }
821
822    fn modify(&mut self, _fd: RawFd, _events: u32, _client_id: ClientId) -> io::Result<()> {
823        Ok(())
824    }
825
826    fn unregister(&mut self, _fd: RawFd) -> io::Result<()> {
827        Ok(())
828    }
829
830    fn wait(&mut self, _timeout: Option<f64>) -> io::Result<Vec<(RawFd, u32, ClientId)>> {
831        Ok(Vec::new())
832    }
833
834    fn name(&self) -> &str {
835        "null"
836    }
837}
838
839// ============================================================================
840// Tests
841// ============================================================================
842
843#[cfg(test)]
844mod tests {
845    use super::*;
846
847    #[test]
848    fn test_engine_constants() {
849        assert_eq!(E_READ, 0x1);
850        assert_eq!(E_WRITE, 0x2);
851        assert!((EPSILON - 1.0e-3).abs() < EPSILON);
852        assert_eq!(FANOUT_UNLIMITED, -1);
853    }
854
855    #[test]
856    fn test_engine_base_timer() {
857        let mut timer = EngineBaseTimer::new(5.0, -1.0, false);
858        assert!(timer.is_valid());
859        assert_eq!(timer.fire_delay(), 5.0);
860        assert_eq!(timer.interval(), -1.0);
861
862        timer.invalidate();
863        assert!(!timer.is_valid());
864        assert_eq!(timer.fire_delay(), -1.0);
865
866        timer.set_nextfire(10.0, 2.0);
867        assert!(timer.is_valid());
868        assert_eq!(timer.fire_delay(), 10.0);
869        assert_eq!(timer.interval(), 2.0);
870    }
871
872    #[test]
873    fn test_timer_queue_scheduling() {
874        let mut timerq = TimerQueue::new();
875
876        // Add some timers
877        let timer1 = timerq.add_timer(EngineBaseTimer::new(1.0, -1.0, false));
878        let timer2 = timerq.add_timer(EngineBaseTimer::new(2.0, -1.0, false));
879        let timer3 = timerq.add_timer(EngineBaseTimer::new(0.5, -1.0, false));
880
881        // Schedule them
882        timerq.schedule(timer1);
883        timerq.schedule(timer2);
884        timerq.schedule(timer3);
885
886        // Check armed count
887        assert_eq!(timerq.armed_count(), 3);
888
889        // Check next fire delay (should be timer3 at 0.5s)
890        let delay = timerq.nextfire_delay();
891        assert!(delay.is_some());
892        assert!((delay.unwrap() - 0.5).abs() < EPSILON);
893    }
894
895    #[test]
896    fn test_timer_queue_firing() {
897        let mut timerq = TimerQueue::new();
898
899        // Add timers with different delays
900        let timer1 = timerq.add_timer(EngineBaseTimer::new(0.1, -1.0, false));
901        let timer2 = timerq.add_timer(EngineBaseTimer::new(0.2, -1.0, false));
902
903        timerq.schedule(timer1);
904        timerq.schedule(timer2);
905
906        // Advance time past both timers
907        timerq.update_time(0.3);
908
909        // Fire expired timers
910        let fired = timerq.fire_expired();
911
912        // Should have fired both timers
913        assert_eq!(fired.len(), 2);
914
915        // Both should be invalid now (one-shot timers)
916        assert!(!timerq.is_valid(timer1));
917        assert!(!timerq.is_valid(timer2));
918    }
919
920    #[test]
921    fn test_timer_queue_invalidation() {
922        let mut timerq = TimerQueue::new();
923
924        let timer1 = timerq.add_timer(EngineBaseTimer::new(1.0, -1.0, false));
925        let timer2 = timerq.add_timer(EngineBaseTimer::new(2.0, -1.0, false));
926
927        timerq.schedule(timer1);
928        timerq.schedule(timer2);
929
930        // Invalidate timer1
931        timerq.invalidate(timer1);
932
933        // Should only have one armed timer now
934        assert_eq!(timerq.armed_count(), 1);
935
936        // Check next fire delay (should be timer2 at 2.0s)
937        let delay = timerq.nextfire_delay();
938        assert!(delay.is_some());
939        assert!((delay.unwrap() - 2.0).abs() < EPSILON);
940    }
941
942    #[test]
943    fn test_timer_queue_repeat() {
944        let mut timerq = TimerQueue::new();
945
946        // Add a repeating timer
947        let timer = timerq.add_timer(EngineBaseTimer::new(0.1, 0.1, false));
948        timerq.schedule(timer);
949
950        // Advance time past first fire
951        timerq.update_time(0.15);
952        let fired = timerq.fire_expired();
953        assert_eq!(fired.len(), 1);
954
955        // Timer should still be valid (repeating)
956        assert!(timerq.is_valid(timer));
957
958        // Advance time past second fire
959        timerq.update_time(0.25);
960        let fired = timerq.fire_expired();
961        assert_eq!(fired.len(), 1);
962
963        // Timer should still be valid
964        assert!(timerq.is_valid(timer));
965    }
966
967    #[test]
968    fn test_engine_error_types() {
969        let err = EngineError::Abort { kill: true };
970        assert!(matches!(err, EngineError::Abort { kill: true }));
971
972        let err = EngineError::Timeout;
973        assert!(matches!(err, EngineError::Timeout));
974
975        let err = EngineError::IllegalOperation("test".to_string());
976        assert!(matches!(err, EngineError::IllegalOperation(_)));
977
978        let err = EngineError::AlreadyRunning;
979        assert!(matches!(err, EngineError::AlreadyRunning));
980
981        let err = EngineError::NotSupported("test".to_string());
982        assert!(matches!(err, EngineError::NotSupported(_)));
983
984        let io_err = io::Error::new(io::ErrorKind::NotFound, "not found");
985        let err = EngineError::Io(io_err);
986        assert!(matches!(err, EngineError::Io(_)));
987    }
988
989    // -- NullBackend tests ---------------------------------------------------
990
991    #[test]
992    fn test_null_backend_name() {
993        let nb = NullBackend;
994        assert_eq!(nb.name(), "null");
995    }
996
997    #[test]
998    fn test_null_backend_wait_returns_empty() {
999        let mut nb = NullBackend;
1000        let events = nb.wait(Some(0.0)).unwrap();
1001        assert!(events.is_empty());
1002    }
1003
1004    #[test]
1005    fn test_null_backend_register_modify_unregister() {
1006        let mut nb = NullBackend;
1007        // SlotMap needs a ClientId but NullBackend ignores it — use a
1008        // stand-in from an ephemeral slotmap.
1009        let mut sm: SlotMap<ClientId, ()> = SlotMap::with_key();
1010        let cid = sm.insert(());
1011        nb.register(0, E_READ, cid).unwrap();
1012        nb.modify(0, E_WRITE, cid).unwrap();
1013        nb.unregister(0).unwrap();
1014    }
1015
1016    // -- Engine with NullBackend tests --------------------------------------
1017
1018    struct DummyClient {
1019        registered: bool,
1020        started: bool,
1021    }
1022
1023    impl DummyClient {
1024        fn new() -> Self {
1025            Self {
1026                registered: false,
1027                started: false,
1028            }
1029        }
1030    }
1031
1032    impl EngineClient for DummyClient {
1033        fn start(&mut self) -> io::Result<()> {
1034            self.started = true;
1035            Ok(())
1036        }
1037
1038        fn close(&mut self, _abort: bool, _timeout: bool) {}
1039
1040        fn fd(&self) -> Option<RawFd> {
1041            None // no real fd
1042        }
1043
1044        fn events(&self) -> u32 {
1045            E_READ
1046        }
1047
1048        fn is_delayable(&self) -> bool {
1049            false
1050        }
1051
1052        fn is_registered(&self) -> bool {
1053            self.registered
1054        }
1055
1056        fn set_registered(&mut self, val: bool) {
1057            self.registered = val;
1058        }
1059
1060        fn autoclose(&self) -> bool {
1061            false
1062        }
1063    }
1064
1065    #[test]
1066    fn test_engine_creation() {
1067        let engine = Engine::with_backend(NullBackend, 0);
1068        assert!(!engine.is_running());
1069        assert!(!engine.exited());
1070        assert_eq!(engine.fanout(), 0);
1071        assert_eq!(engine.client_count(), 0);
1072    }
1073
1074    #[test]
1075    fn test_engine_add_remove_client() {
1076        let mut engine = Engine::with_backend(NullBackend, 0);
1077        let cid = engine.add_client(Box::new(DummyClient::new())).unwrap();
1078        assert_eq!(engine.client_count(), 1);
1079        engine.remove_client(cid);
1080        assert_eq!(engine.client_count(), 0);
1081    }
1082
1083    #[test]
1084    fn test_engine_add_timer() {
1085        let mut engine = Engine::with_backend(NullBackend, 0);
1086        let tid = engine.add_timer(EngineBaseTimer::new(1.0, -1.0, false));
1087        assert!(engine.timerq().is_valid(tid));
1088        engine.remove_timer(tid);
1089        assert!(!engine.timerq().is_valid(tid));
1090    }
1091
1092    #[test]
1093    fn test_engine_fire_timers() {
1094        let mut engine = Engine::with_backend(NullBackend, 0);
1095        let tid = engine.add_timer(EngineBaseTimer::new(0.1, -1.0, false));
1096        assert!(engine.timerq().is_valid(tid));
1097        engine.timerq_mut().update_time(1.0);
1098        engine.fire_timers();
1099        assert!(!engine.timerq().is_valid(tid));
1100    }
1101
1102    #[test]
1103    fn test_engine_abort() {
1104        let mut engine = Engine::with_backend(NullBackend, 0);
1105        engine.abort(false);
1106        assert!(!engine.is_running());
1107    }
1108
1109    #[test]
1110    fn test_engine_evloop_refcnt() {
1111        let mut engine = Engine::with_backend(NullBackend, 0);
1112        engine.evloop_acquire();
1113        engine.evloop_acquire();
1114        engine.evloop_release();
1115        // refcnt is 1, engine should still be "runnable"
1116        // evloop_release doesn't start the engine, just would stop it if it were running
1117        engine.evloop_release();
1118        // refcnt is 0, running should be false
1119        assert!(!engine.is_running());
1120    }
1121
1122    #[test]
1123    fn test_engine_stop() {
1124        let mut engine = Engine::with_backend(NullBackend, 0);
1125        // Simulate running state
1126        engine.stop();
1127        assert!(!engine.is_running());
1128    }
1129
1130    #[test]
1131    fn test_engine_start_all_clients() {
1132        let mut engine = Engine::with_backend(NullBackend, 0);
1133        engine.add_client(Box::new(DummyClient::new())).unwrap();
1134        engine.add_client(Box::new(DummyClient::new())).unwrap();
1135        engine.start_all_clients().unwrap();
1136        assert_eq!(engine.client_count(), 2);
1137    }
1138
1139    #[test]
1140    fn test_engine_update_client_events() {
1141        let mut engine = Engine::with_backend(NullBackend, 0);
1142        let cid = engine.add_client(Box::new(DummyClient::new())).unwrap();
1143        // DummyClient has no fd, so this is a no-op but shouldn't error
1144        engine.update_client_events(cid).unwrap();
1145    }
1146
1147    // -- Backend error propagation tests ------------------------------------
1148    //
1149    // Rust mirror of upstream tests/EngineErrorTest.py (test_epoll_error,
1150    // test_select_error): a non-EINTR backend error must propagate out of
1151    // the run loop as an engine error — no hang, no panic, no spin — and
1152    // EINTR must be retried (upstream ClusterShell 7a39bd8).
1153
1154    /// Backend whose `wait()` always fails with the given errno, mirroring
1155    /// the BadPoller/BadSelect fixtures of tests/EngineErrorTest.py.
1156    struct FailBackend {
1157        errno: i32,
1158        wait_calls: usize,
1159    }
1160
1161    impl FailBackend {
1162        fn new(errno: i32) -> Self {
1163            Self {
1164                errno,
1165                wait_calls: 0,
1166            }
1167        }
1168    }
1169
1170    impl PollBackend for FailBackend {
1171        fn register(&mut self, _fd: RawFd, _events: u32, _client_id: ClientId) -> io::Result<()> {
1172            Ok(())
1173        }
1174
1175        fn modify(&mut self, _fd: RawFd, _events: u32, _client_id: ClientId) -> io::Result<()> {
1176            Ok(())
1177        }
1178
1179        fn unregister(&mut self, _fd: RawFd) -> io::Result<()> {
1180            Ok(())
1181        }
1182
1183        fn wait(&mut self, _timeout: Option<f64>) -> io::Result<Vec<(RawFd, u32, ClientId)>> {
1184            self.wait_calls += 1;
1185            Err(io::Error::from_raw_os_error(self.errno))
1186        }
1187
1188        fn name(&self) -> &str {
1189            "fail"
1190        }
1191    }
1192
1193    /// Backend that fails with EINTR `eintr_count` times, then with
1194    /// `final_errno`, to verify EINTR is retried like upstream EPoll.
1195    struct IntrThenFailBackend {
1196        eintr_count: usize,
1197        final_errno: i32,
1198        wait_calls: usize,
1199    }
1200
1201    impl PollBackend for IntrThenFailBackend {
1202        fn register(&mut self, _fd: RawFd, _events: u32, _client_id: ClientId) -> io::Result<()> {
1203            Ok(())
1204        }
1205
1206        fn modify(&mut self, _fd: RawFd, _events: u32, _client_id: ClientId) -> io::Result<()> {
1207            Ok(())
1208        }
1209
1210        fn unregister(&mut self, _fd: RawFd) -> io::Result<()> {
1211            Ok(())
1212        }
1213
1214        fn wait(&mut self, _timeout: Option<f64>) -> io::Result<Vec<(RawFd, u32, ClientId)>> {
1215            self.wait_calls += 1;
1216            if self.wait_calls <= self.eintr_count {
1217                Err(io::Error::from_raw_os_error(libc::EINTR))
1218            } else {
1219                Err(io::Error::from_raw_os_error(self.final_errno))
1220            }
1221        }
1222
1223        fn name(&self) -> &str {
1224            "intr-then-fail"
1225        }
1226    }
1227
1228    #[test]
1229    fn test_engine_poll_error_propagates() {
1230        // Mirror of EngineErrorTest.test_epoll_error / test_select_error:
1231        // inject EBADF into the backend; runloop must return the error.
1232        let mut engine = Engine::with_backend(FailBackend::new(libc::EBADF), 0);
1233        engine.evloop_acquire();
1234
1235        let result = engine.run(None);
1236        match result {
1237            Err(EngineError::Io(err)) => {
1238                assert_eq!(err.raw_os_error(), Some(libc::EBADF));
1239            }
1240            other => panic!("expected EngineError::Io(EBADF), got: {:?}", other),
1241        }
1242
1243        // The run loop must terminate cleanly: not running, exited once.
1244        assert!(!engine.is_running());
1245        assert!(engine.exited());
1246
1247        // State must not be corrupted: a subsequent run() surfaces the
1248        // backend error again instead of AlreadyRunning.
1249        match engine.run(None) {
1250            Err(EngineError::Io(err)) => {
1251                assert_eq!(err.raw_os_error(), Some(libc::EBADF));
1252            }
1253            other => panic!("expected EngineError::Io(EBADF), got: {:?}", other),
1254        }
1255    }
1256
1257    #[test]
1258    fn test_engine_select_style_errors_propagate() {
1259        // The upstream Select runloop logs "Increase RLIMIT_NOFILE?" for
1260        // EINVAL/EBADF/ENOMEM and then re-raises; the Rust engine has no
1261        // logging facade, so the errno surfaces via EngineError::Io.
1262        for errno in [libc::EINVAL, libc::EBADF, libc::ENOMEM] {
1263            let mut engine = Engine::with_backend(FailBackend::new(errno), 0);
1264            match engine.run(None) {
1265                Err(EngineError::Io(err)) => {
1266                    assert_eq!(err.raw_os_error(), Some(errno));
1267                }
1268                other => panic!("expected EngineError::Io({}), got: {:?}", errno, other),
1269            }
1270            assert!(!engine.is_running());
1271            assert!(engine.exited());
1272        }
1273    }
1274
1275    #[test]
1276    fn test_engine_eintr_is_retried() {
1277        // EINTR ("interrupted by a signal") must be swallowed and the poll
1278        // retried; only the subsequent non-EINTR error may propagate.
1279        let backend = IntrThenFailBackend {
1280            eintr_count: 2,
1281            final_errno: libc::EBADF,
1282            wait_calls: 0,
1283        };
1284        let mut engine = Engine::with_backend(backend, 0);
1285
1286        match engine.run(None) {
1287            Err(EngineError::Io(err)) => {
1288                assert_eq!(err.raw_os_error(), Some(libc::EBADF));
1289            }
1290            other => panic!("expected EngineError::Io(EBADF), got: {:?}", other),
1291        }
1292        // 2 retried EINTR calls + 1 final failing call
1293        assert_eq!(engine.poll_backend.wait_calls, 3);
1294        assert!(!engine.is_running());
1295        assert!(engine.exited());
1296    }
1297
1298    #[test]
1299    fn test_engine_timeout_leaves_clean_state() {
1300        // A timeout exit must also leave the engine re-runnable (upstream
1301        // Engine.run() resets running state in its finally clause).
1302        let mut engine = Engine::with_backend(NullBackend, 0);
1303        match engine.run(Some(0.0)) {
1304            Err(EngineError::Timeout) => {}
1305            other => panic!("expected EngineError::Timeout, got: {:?}", other),
1306        }
1307        assert!(!engine.is_running());
1308        assert!(engine.exited());
1309    }
1310
1311    // -- EngineTimer tests --------------------------------------------------
1312
1313    #[test]
1314    fn test_engine_timer_creation() {
1315        let timer = EngineTimer::new(1.0, -1.0, false, || {});
1316        assert!(timer.base().is_valid());
1317        assert_eq!(timer.base().fire_delay(), 1.0);
1318        assert_eq!(timer.base().interval(), -1.0);
1319        assert!(!timer.base().autoclose());
1320    }
1321
1322    #[test]
1323    fn test_engine_timer_fire_calls_handler() {
1324        use std::sync::{
1325            atomic::{AtomicUsize, Ordering},
1326            Arc,
1327        };
1328        let counter = Arc::new(AtomicUsize::new(0));
1329        let counter_clone = counter.clone();
1330
1331        let mut timer = EngineTimer::new(0.5, -1.0, false, move || {
1332            counter_clone.fetch_add(1, Ordering::SeqCst);
1333        });
1334
1335        assert_eq!(counter.load(Ordering::SeqCst), 0);
1336        timer.fire();
1337        assert_eq!(counter.load(Ordering::SeqCst), 1);
1338        timer.fire();
1339        assert_eq!(counter.load(Ordering::SeqCst), 2);
1340    }
1341
1342    #[test]
1343    fn test_engine_timer_invalidate() {
1344        let mut timer = EngineTimer::new(1.0, -1.0, false, || {});
1345        assert!(timer.base().is_valid());
1346        timer.base_mut().invalidate();
1347        assert!(!timer.base().is_valid());
1348    }
1349
1350    #[test]
1351    fn test_engine_timer_debug() {
1352        let timer = EngineTimer::new(1.0, -1.0, false, || {});
1353        let debug_str = format!("{:?}", timer);
1354        assert!(debug_str.contains("EngineTimer"));
1355        assert!(debug_str.contains("<fn>"));
1356    }
1357
1358    #[test]
1359    fn test_engine_timer_with_autoclose() {
1360        let timer = EngineTimer::new(2.0, 0.5, true, || {});
1361        assert!(timer.base().autoclose());
1362        assert_eq!(timer.base().interval(), 0.5);
1363    }
1364}