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