Skip to main content

limon_core/
schedule.rs

1//! A module for managing scheduled items that can be periodically polled.
2//!
3//! The `schedule` module provides structures and traits to manage objects
4//! that need to be executed, updated, or checked at regular intervals.
5//! Each item must implement the `Schedulable` trait, which defines a unique
6//! identifier and an associated interval.
7//!
8//! The `Schedule` struct maintains:
9//! - A mapping of item `id` to the items themselves for fast lookup.
10//! - A mapping of `interval` to sets of item `id`, allowing efficient
11//!   retrieval of all items that should be polled at a given interval.
12//!
13//! # Example
14//!
15//! ```rust
16//! use std::collections::HashSet;
17//!
18//! use limon_core::schedule::{Schedule, Schedulable};
19//!
20//! struct Task {
21//!     id: i64,
22//!     interval: i64,
23//! }
24//!
25//! impl Schedulable for Task {
26//!     type Id = i64;
27//!     type Interval = i64;
28//!
29//!     fn get_id(&self) -> Self::Id { self.id }
30//!     fn get_interval(&self) -> Self::Interval { self.interval }
31//! }
32//!
33//! let schedule: Schedule<Task> = Schedule::new();
34//!
35//! # tokio_test::block_on(async {
36//! schedule.insert(Task { id: 1, interval: 30 }).await;
37//! schedule.insert(Task { id: 2, interval: 60 }).await;
38//!
39//! assert_eq!(schedule.get_due(0, 90).await.len(), 2);
40//! # })
41//! ```
42
43use std::collections::{HashMap, HashSet};
44use std::hash::Hash;
45use std::sync::Arc;
46
47use tokio::sync::RwLock;
48
49/// A trait for items that can be scheduled.
50///
51/// This trait defines the necessary requirements for an item to be
52/// stored and managed by a [Schedule]. Each item must have a unique
53/// identifier `id` and an associated `interval`. Both types must
54/// support hashing and equality checks, and be convertible to `i64`.
55pub trait Schedulable {
56  /// The unique identifier for the item.
57  type Id: Eq + Hash + Into<i64> + Copy;
58
59  /// The interval associated with the item.
60  type Interval: Eq + Hash + Into<i64> + Copy;
61
62  /// Returns the unique identifier of the item.
63  fn get_id(&self) -> Self::Id;
64
65  /// Returns the interval of the item.
66  fn get_interval(&self) -> Self::Interval;
67}
68
69/// A schedule for managing [Schedulable] items.
70///
71/// The [Schedule] structure stores items indexed by their unique
72/// identifiers and groups item `id` by their `interval`. This allows
73/// efficient lookup of items by `id` and retrieval of all `id` in a
74/// given interval.
75///
76/// | Operation | Time complexity |
77/// |-----------|-----------------|
78/// | Get       | O(1)            |
79/// | Get due   | O(m)            |
80/// | Insert    | O(1)            |
81/// | Remove    | O(1)            |
82///
83/// **m** - it's amount of unique intervals.
84pub struct Schedule<Item: Schedulable> {
85  items: RwLock<HashMap<Item::Id, Arc<Item>>>,
86  intervals: RwLock<HashMap<Item::Interval, HashSet<Item::Id>>>,
87}
88
89impl<Item: Schedulable> Schedule<Item> {
90  /// Create a new schedule.
91  pub fn new() -> Self {
92    Self {
93      items: RwLock::new(HashMap::new()),
94      intervals: RwLock::new(HashMap::new()),
95    }
96  }
97
98  /// Returns `true` if the [Schedule] doesn't contain elements.
99  pub async fn is_empty(&self) -> bool {
100    self.items.read().await.is_empty() && self.intervals.read().await.is_empty()
101  }
102
103  /// Get an item by `id`.
104  pub async fn get(&self, id: Item::Id) -> Option<Arc<Item>> {
105    self.items.read().await.get(&id).cloned()
106  }
107
108  /// Get items that are included in the interval `from` and `to`.
109  ///
110  /// An element is included in the interval if there is at least
111  /// one value between `from` and `to` that is divisible by
112  /// the item's [interval](Schedulable::Interval) without a remainder.
113  ///
114  /// `from` and `to` should be > 0 and `from` should be <= `to`.
115  pub async fn get_due(&self, from: i64, to: i64) -> Vec<Arc<Item>> {
116    let mut result = Vec::new();
117    let intervals = self.intervals.read().await;
118
119    for (interval, ids) in intervals.iter() {
120      let interval = (*interval).into();
121      let next_check = ((from + interval - 1) / interval) * interval;
122
123      if next_check <= to {
124        let guard = self.items.read().await;
125
126        for id in ids {
127          if let Some(item) = guard.get(id) {
128            result.push(item.clone());
129          }
130        }
131      }
132    }
133
134    result
135  }
136
137  /// Insert an item into schedule.
138  ///
139  /// If an item with this `id` is already in the schedule, it will be replaced.
140  pub async fn insert(&self, item: Item) {
141    let id = item.get_id();
142    let interval = item.get_interval();
143
144    {
145      let mut intervals = self.intervals.write().await;
146
147      if let Some(ids_set) = intervals.get_mut(&interval) {
148        ids_set.insert(id);
149      } else {
150        let mut set = HashSet::new();
151        set.insert(id);
152
153        intervals.insert(interval, set);
154      }
155    }
156
157    {
158      let mut items = self.items.write().await;
159
160      items.insert(id, Arc::new(item));
161    }
162  }
163
164  /// Remove an item by `id` from the schedule if it exists.
165  pub async fn remove(&self, id: Item::Id) {
166    if let Some(item) = self.items.write().await.remove(&id) {
167      let interval = item.get_interval();
168      let mut intervals = self.intervals.write().await;
169
170      if let Some(set) = intervals.get_mut(&interval) {
171        if set.remove(&id) && set.is_empty() {
172          intervals.remove(&interval);
173        }
174      }
175    }
176  }
177
178  /// Clears the schedule, removing all items. Keeps the allocated
179  /// memory for reuse.
180  pub async fn clear(&self) {
181    self.items.write().await.clear();
182    self.intervals.write().await.clear();
183  }
184}
185
186#[cfg(test)]
187mod tests {
188  use tokio::sync::RwLockReadGuard;
189
190  use super::*;
191
192  #[derive(Debug, PartialEq)]
193  struct Task {
194    id: i64,
195    interval: i64,
196    updated: bool,
197  }
198
199  impl<Item: Schedulable> Schedule<Item> {
200    pub async fn items_ref(&self) -> RwLockReadGuard<'_, HashMap<Item::Id, Arc<Item>>> {
201      self.items.read().await
202    }
203
204    pub async fn intervals_ref(
205      &self,
206    ) -> RwLockReadGuard<'_, HashMap<Item::Interval, HashSet<Item::Id>>> {
207      self.intervals.read().await
208    }
209  }
210
211  impl From<(i64, i64)> for Task {
212    fn from(args: (i64, i64)) -> Self {
213      Task {
214        id: args.0,
215        interval: args.1,
216        updated: false,
217      }
218    }
219  }
220
221  impl Schedulable for Task {
222    type Id = i64;
223    type Interval = i64;
224
225    fn get_id(&self) -> Self::Id {
226      self.id
227    }
228
229    fn get_interval(&self) -> Self::Interval {
230      self.interval
231    }
232  }
233
234  #[tokio::test]
235  async fn empty_schedule() {
236    let schedule: Schedule<Task> = Schedule::new();
237
238    assert!(
239      schedule.items_ref().await.is_empty(),
240      "schedule items shouldn't be empty"
241    );
242    assert!(
243      schedule.intervals_ref().await.is_empty(),
244      "schedule intervals shouldn't be empty"
245    );
246  }
247
248  #[tokio::test]
249  async fn test_empty_schedule() {
250    let schedule: Schedule<Task> = Schedule::new();
251
252    assert!(
253      schedule.get_due(1, 100).await.is_empty(),
254      "empty schedule shouldn't return due items"
255    );
256  }
257
258  #[tokio::test]
259  async fn get_due_on_boundary() {
260    let schedule: Schedule<Task> = Schedule::new();
261
262    schedule.insert(Task::from((1, 10))).await;
263
264    assert_eq!(
265      schedule.get_due(1, 10).await.len(),
266      1,
267      "schedule should return item on boundary"
268    );
269    assert_eq!(
270      schedule.get_due(10, 10).await.len(),
271      1,
272      "schedule should return item on boundary equals"
273    );
274  }
275
276  #[tokio::test]
277  async fn get_due_before_boundary() {
278    let schedule: Schedule<Task> = Schedule::new();
279
280    schedule.insert(Task::from((1, 10))).await;
281
282    assert!(
283      schedule.get_due(1, 9).await.is_empty(),
284      "schedule shouldn't return due items before boundary"
285    );
286  }
287
288  #[tokio::test]
289  async fn test_multiple_intervals() {
290    let schedule: Schedule<Task> = Schedule::new();
291
292    schedule.insert(Task::from((1, 5))).await;
293    schedule.insert(Task::from((2, 10))).await;
294
295    let ids: Vec<i64> = schedule.get_due(1, 10).await.iter().map(|t| t.id).collect();
296
297    assert!(
298      ids.contains(&1),
299      "schedule should return item with interval 5"
300    );
301    assert!(
302      ids.contains(&2),
303      "schedule should return item with interval 10"
304    );
305  }
306
307  #[tokio::test]
308  async fn test_skip_multiple_intervals() {
309    let schedule: Schedule<Task> = Schedule::new();
310
311    schedule.insert(Task::from((1, 10))).await;
312
313    assert_eq!(
314      schedule.get_due(1, 35).await.len(),
315      1,
316      "schedule should return due item even if multiple intervals were passed"
317    );
318  }
319
320  #[tokio::test]
321  async fn insert_single_item_into_schedule() {
322    let schedule: Schedule<Task> = Schedule::new();
323
324    schedule.insert(Task::from((1, 30))).await;
325
326    assert!(
327      schedule.items_ref().await.contains_key(&1),
328      "schedule items should contain entry"
329    );
330    assert!(
331      schedule.intervals_ref().await.contains_key(&30),
332      "schedule intervals should contain entry"
333    );
334    assert_eq!(
335      schedule.get(1).await,
336      Some(Arc::new(Task::from((1, 30)))),
337      "schedule should return entry by id"
338    );
339  }
340
341  #[tokio::test]
342  async fn insert_multiple_items_into_schedule() {
343    let schedule: Schedule<Task> = Schedule::new();
344
345    schedule.insert(Task::from((1, 30))).await;
346    schedule.insert(Task::from((2, 30))).await;
347
348    assert!(
349      schedule.items_ref().await.contains_key(&1),
350      "schedule items should contain entry"
351    );
352    assert!(
353      schedule.items_ref().await.contains_key(&2),
354      "schedule items should contain entry"
355    );
356    assert!(
357      schedule.intervals_ref().await.contains_key(&30),
358      "schedule intervals should contain entry"
359    );
360    assert_eq!(
361      schedule.get(1).await,
362      Some(Arc::new(Task::from((1, 30)))),
363      "schedule should return entry by id"
364    );
365    assert_eq!(
366      schedule.get(2).await,
367      Some(Arc::new(Task::from((2, 30)))),
368      "schedule should return entry by id"
369    );
370  }
371
372  #[tokio::test]
373  async fn insert_the_sane_item_twice() {
374    let schedule: Schedule<Task> = Schedule::new();
375
376    schedule.insert(Task::from((1, 30))).await;
377    schedule.insert(Task::from((1, 30))).await;
378
379    assert_eq!(
380      schedule.items_ref().await.len(),
381      1,
382      "schedule items shouldn't be empty"
383    );
384    assert_eq!(
385      schedule.intervals_ref().await.len(),
386      1,
387      "schedule intervals shouldn't be empty"
388    );
389  }
390
391  #[tokio::test]
392  async fn remove_item_from_schedule() {
393    let schedule: Schedule<Task> = Schedule::new();
394
395    schedule.insert(Task::from((1, 30))).await;
396    schedule.remove(1).await;
397
398    assert!(
399      schedule.items_ref().await.is_empty(),
400      "schedule items should be empty"
401    );
402    assert!(
403      schedule.intervals_ref().await.is_empty(),
404      "schedule intervals should be empty"
405    );
406  }
407
408  #[tokio::test]
409  async fn clear() {
410    let schedule: Schedule<Task> = Schedule::new();
411
412    schedule.insert(Task::from((1, 10))).await;
413    schedule.insert(Task::from((2, 20))).await;
414
415    assert!(!schedule.is_empty().await, "schedule shouldn't be empty");
416
417    schedule.clear().await;
418    assert!(schedule.is_empty().await, "schedule should be empty");
419  }
420}