Skip to main content

des_sim/execution/strategy/continue_strategy/
limit_abort.rs

1//! The `limit_abort` module provides the `LimitAbortStrategy`, an implementation
2//! of the `ContinueStrategy` trait that enforces limits on micro-step execution.
3//!
4//! This strategy allows for a configurable maximum number of micro-steps per tick
5//! and a grace period for exceeding this limit. If the limit is continuously
6//! surpassed, the simulation is aborted with a `LimitAbortStrategyError`.
7
8use crate::execution::phase::UncheckedActiveExecutor;
9use crate::execution::strategy::{ContinueStrategy, ContinuousStrategyResult};
10use crate::modeling::model::Model;
11use std::fmt::{Display, Formatter};
12
13/// Errors associated with the `LimitAbortStrategy`.
14#[derive(Debug)]
15pub enum LimitAbortStrategyError<RunnerError> {
16    /// An error originating from the execution engine.
17    Runner(RunnerError),
18    /// Simulation aborted due to exceeding the maximum micro-step limit.
19    LimitExceeded {
20        limit_micro_step_count: u64,
21        limit_micro_step_exceeded_count: usize,
22    },
23}
24
25impl<RunnerError: Display> Display for LimitAbortStrategyError<RunnerError> {
26    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
27        match self {
28            LimitAbortStrategyError::Runner(e) => {
29                write!(f, "{}", e)
30            }
31            LimitAbortStrategyError::LimitExceeded {
32                limit_micro_step_count,
33                limit_micro_step_exceeded_count,
34            } => {
35                write!(
36                    f,
37                    "The maximum limit of {} micro-steps has been exceeded {} times.",
38                    limit_micro_step_count, limit_micro_step_exceeded_count
39                )
40            }
41        }
42    }
43}
44
45impl<RunnerError: std::error::Error> std::error::Error for LimitAbortStrategyError<RunnerError> {}
46
47/// A continuation strategy that monitors and limits the number of micro-steps.
48///
49/// This strategy tracks the micro-step count within each tick. If the count exceeds
50/// `limit_micro_step_count`, the strategy allows for a grace period defined by
51/// `allow_error_count`. Within this grace period, the strategy attempts to recover
52/// by discarding remaining tasks in the current tick. Once the limit is exceeded
53/// beyond the allowed count, the simulation is forcibly terminated with an error.
54#[derive(Clone)]
55pub struct LimitAbortStrategy {
56    limit_micro_step_count: u64,
57    allow_error_count: usize,
58    error_count: usize,
59}
60
61impl<E, M: Model<E>, RunnerError> ContinueStrategy<E, M, RunnerError> for LimitAbortStrategy {
62    type Err = LimitAbortStrategyError<RunnerError>;
63
64    fn handle_micro_step_continue(
65        &mut self,
66        model: &M,
67        unchecked: UncheckedActiveExecutor<E, M>,
68    ) -> ContinuousStrategyResult<E, M, Self::Err> {
69        let current_micro_step = unchecked.current_micro_step();
70
71        // Continue unconditionally if within the limit.
72        if current_micro_step.value() < self.limit_micro_step_count {
73            return Ok(unchecked.into_active_executor());
74        }
75
76        self.error_count += 1;
77        let mut next_active = unchecked.into_active_executor();
78
79        // Check if the error is within the allowed threshold.
80        if self.error_count <= self.allow_error_count {
81            // Within limit: Purge remaining tasks in the current tick to attempt recovery.
82            next_active.discard_remain_micro_step(model);
83            Ok(next_active)
84        } else {
85            // Exceeded limit: Clean up the current tick and terminate with an error.
86            next_active.discard_remain_micro_step(model);
87            Err((
88                next_active,
89                LimitAbortStrategyError::LimitExceeded {
90                    limit_micro_step_count: self.limit_micro_step_count,
91                    limit_micro_step_exceeded_count: self.error_count,
92                },
93            ))
94        }
95    }
96}
97
98impl LimitAbortStrategy {
99    /// Creates a new `LimitAbortStrategy`.
100    ///
101    /// # Arguments
102    /// * `limit_micro_step_count` - Maximum micro-steps permitted per tick.
103    /// * `allow_error_count` - Number of times exceeding the limit is permitted.
104    pub fn new(limit_micro_step_count: u64, allow_error_count: usize) -> Self {
105        LimitAbortStrategy {
106            limit_micro_step_count,
107            allow_error_count,
108            error_count: 0,
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::context::{ActiveExecutorContext, EventContext};
117    use crate::event_scheduler::EventScheduler;
118    use crate::modeling::event::Event;
119    use crate::modeling::hook::instance::HookDelegate;
120    use crate::primitive::time::{MicroStep, MicroStepStatus, TickStatus};
121    use crate::source_handler::SourceHandler;
122
123    #[derive(Debug, PartialEq, Eq, Clone, Copy)]
124    enum TestEvent {}
125
126    struct TestModel;
127
128    impl Model<TestEvent> for TestModel {
129        fn handle_event(
130            &mut self,
131            _context: &mut EventContext<TestEvent, Self>,
132            _event: &Event<TestEvent>,
133        ) {
134            // No-op for testing
135        }
136    }
137
138    /// Helper function to generate an `UncheckedActiveExecutor` for testing.
139    fn create_unchecked_executor(
140        micro_step_val: u64,
141    ) -> UncheckedActiveExecutor<TestEvent, TestModel> {
142        let active_context = ActiveExecutorContext {
143            current_tick_status: TickStatus::initialize(),
144            next_micro_step_status: MicroStepStatus::new(MicroStep::new(micro_step_val + 1)),
145            hook_delegate: HookDelegate::new(),
146            source_handler: SourceHandler::new(),
147            event_scheduler: EventScheduler::new(),
148        };
149        UncheckedActiveExecutor::new(active_context, MicroStep::new(micro_step_val))
150    }
151
152    #[test]
153    fn test_limit_abort_strategy_under_limit() {
154        let model = TestModel;
155        // Limit: 10, Allow Error Count: 1
156        let mut strategy = LimitAbortStrategy::new(10, 1);
157
158        // Current micro-step: 9 (Below limit of 10)
159        let unchecked = create_unchecked_executor(9);
160        let result: ContinuousStrategyResult<
161            TestEvent,
162            TestModel,
163            LimitAbortStrategyError<String>,
164        > = strategy.handle_micro_step_continue(&model, unchecked);
165
166        // Should continue unconditionally when below limit.
167        assert!(result.is_ok());
168        assert_eq!(strategy.error_count, 0);
169    }
170
171    #[test]
172    fn test_limit_abort_strategy_reach_limit_within_allowance() {
173        let model = TestModel;
174        // Limit: 10, Allow Error Count: 1
175        let mut strategy = LimitAbortStrategy::new(10, 1);
176
177        // 1st time reaching limit (Micro-step 10)
178        let unchecked = create_unchecked_executor(10);
179        let result: ContinuousStrategyResult<
180            TestEvent,
181            TestModel,
182            LimitAbortStrategyError<String>,
183        > = strategy.handle_micro_step_continue(&model, unchecked);
184
185        // Within grace period (1), so should return Ok.
186        assert!(result.is_ok());
187        assert_eq!(strategy.error_count, 1);
188    }
189
190    #[test]
191    fn test_limit_abort_strategy_exceed_allowance_aborts() {
192        let model = TestModel;
193        // Limit: 10, Allow Error Count: 1
194        let mut strategy = LimitAbortStrategy::new(10, 1);
195
196        // 1st reach (Within allowance)
197        let unchecked_1 = create_unchecked_executor(10);
198        let unchecked_1_result: ContinuousStrategyResult<
199            TestEvent,
200            TestModel,
201            LimitAbortStrategyError<String>,
202        > = strategy.handle_micro_step_continue(&model, unchecked_1);
203        assert!(unchecked_1_result.is_ok());
204
205        // 2nd reach (Exceeds allowance)
206        let unchecked_2 = create_unchecked_executor(10);
207        let unchecked_2_result: ContinuousStrategyResult<
208            TestEvent,
209            TestModel,
210            LimitAbortStrategyError<String>,
211        > = strategy.handle_micro_step_continue(&model, unchecked_2);
212
213        // Limit exceeded, should return Err.
214        assert!(unchecked_2_result.is_err());
215        assert_eq!(strategy.error_count, 2);
216
217        // Verify error details.
218        let (_context, error) = unchecked_2_result.err().unwrap();
219        match error {
220            LimitAbortStrategyError::LimitExceeded {
221                limit_micro_step_count,
222                limit_micro_step_exceeded_count,
223            } => {
224                assert_eq!(limit_micro_step_count, 10);
225                assert_eq!(limit_micro_step_exceeded_count, 2);
226            }
227            _ => panic!("Expected LimitAbortStrategyError::LimitExceeded variant"),
228        }
229    }
230
231    #[test]
232    fn test_error_display() {
233        // Validate Display implementation for the custom error.
234        let error: LimitAbortStrategyError<String> = LimitAbortStrategyError::LimitExceeded {
235            limit_micro_step_count: 5,
236            limit_micro_step_exceeded_count: 3,
237        };
238        let message = format!("{}", error);
239        assert_eq!(
240            message,
241            "The maximum limit of 5 micro-steps has been exceeded 3 times."
242        );
243
244        let runner_error = LimitAbortStrategyError::Runner("internal error".to_string());
245        assert_eq!(format!("{}", runner_error), "internal error");
246    }
247}