Skip to main content

spire_core/frontier/
memory.rs

1//! In-memory storage: [`InMemFrontier`], a FIFO, LIFO or priority queue.
2
3use std::cmp::Ordering;
4use std::collections::{BinaryHeap, VecDeque};
5use std::fmt;
6use std::future::{Future, ready};
7use std::sync::Mutex;
8
9use crate::Result;
10use crate::frontier::seed::Seed;
11use crate::frontier::{Ack, Admission, Frontier, Next, Report};
12use crate::sync::lock;
13
14/// An in-memory [`Frontier`] that crawls in insertion, reverse or priority
15/// order.
16pub struct InMemFrontier {
17    seeds: Mutex<Store>,
18}
19
20enum Store {
21    Fifo(VecDeque<Seed>),
22    Lifo(VecDeque<Seed>),
23    Priority(BinaryHeap<Prioritized>, u64),
24}
25
26/// A seed ordered by priority, then by insertion.
27struct Prioritized {
28    priority: i32,
29    sequence: u64,
30    seed: Seed,
31}
32
33impl Ord for Prioritized {
34    fn cmp(&self, other: &Self) -> Ordering {
35        self.priority
36            .cmp(&other.priority)
37            .then_with(|| other.sequence.cmp(&self.sequence))
38    }
39}
40
41impl PartialOrd for Prioritized {
42    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
43        Some(self.cmp(other))
44    }
45}
46
47impl PartialEq for Prioritized {
48    fn eq(&self, other: &Self) -> bool {
49        self.cmp(other).is_eq()
50    }
51}
52
53impl Eq for Prioritized {}
54
55impl Store {
56    fn len(&self) -> usize {
57        match self {
58            Self::Fifo(seeds) | Self::Lifo(seeds) => seeds.len(),
59            Self::Priority(seeds, _) => seeds.len(),
60        }
61    }
62
63    const fn order(&self) -> &'static str {
64        match self {
65            Self::Fifo(_) => "fifo",
66            Self::Lifo(_) => "lifo",
67            Self::Priority(..) => "priority",
68        }
69    }
70}
71
72impl InMemFrontier {
73    /// Crawls seeds in the order they were pushed: breadth-first.
74    #[must_use]
75    pub const fn fifo() -> Self {
76        Self::with_store(Store::Fifo(VecDeque::new()))
77    }
78
79    /// Crawls the most recently pushed seed first: depth-first.
80    #[must_use]
81    pub const fn lifo() -> Self {
82        Self::with_store(Store::Lifo(VecDeque::new()))
83    }
84
85    /// Crawls the seed with the highest [`priority`](Seed::with_priority)
86    /// first, and seeds of equal priority in the order they were pushed.
87    #[must_use]
88    pub const fn priority() -> Self {
89        Self::with_store(Store::Priority(BinaryHeap::new(), 0))
90    }
91
92    const fn with_store(store: Store) -> Self {
93        Self {
94            seeds: Mutex::new(store),
95        }
96    }
97
98    fn insert(&self, seed: Seed) {
99        match &mut *lock(&self.seeds) {
100            Store::Fifo(seeds) | Store::Lifo(seeds) => seeds.push_back(seed),
101            Store::Priority(seeds, sequence) => {
102                *sequence += 1;
103                let priority = seed.priority();
104                seeds.push(Prioritized {
105                    priority,
106                    sequence: *sequence,
107                    seed,
108                });
109            }
110        }
111    }
112}
113
114impl Default for InMemFrontier {
115    fn default() -> Self {
116        Self::fifo()
117    }
118}
119
120impl fmt::Debug for InMemFrontier {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        let store = lock(&self.seeds);
123        f.debug_struct("InMemFrontier")
124            .field("order", &store.order())
125            .field("len", &store.len())
126            .finish()
127    }
128}
129
130impl Frontier for InMemFrontier {
131    fn push(&self, seed: Seed) -> impl Future<Output = Result<Admission>> + Send {
132        self.insert(seed);
133        ready(Ok(Admission::Queued))
134    }
135
136    fn pop(&self) -> impl Future<Output = Result<Next>> + Send {
137        let seed = match &mut *lock(&self.seeds) {
138            Store::Fifo(seeds) => seeds.pop_front(),
139            Store::Lifo(seeds) => seeds.pop_back(),
140            Store::Priority(seeds, _) => seeds.pop().map(|prioritized| prioritized.seed),
141        };
142        ready(Ok(seed.map_or(Next::Empty, Next::Ready)))
143    }
144
145    /// Re-queues seeds the crawl stopped before finishing, so a
146    /// [`snapshot`](Frontier::snapshot) taken after the run includes them.
147    fn report(&self, report: Report) -> impl Future<Output = Result<Ack>> + Send {
148        let ack = match report {
149            Report::Cancelled(seed) => {
150                self.insert(seed);
151                Ack::Applied
152            }
153            Report::Retry { .. } => Ack::RetryRefused,
154            _ => Ack::Applied,
155        };
156        ready(Ok(ack))
157    }
158
159    fn size_hint(&self) -> Option<usize> {
160        Some(lock(&self.seeds).len())
161    }
162
163    fn snapshot(&self) -> Option<Vec<Seed>> {
164        let store = lock(&self.seeds);
165        let seeds = match &*store {
166            Store::Fifo(seeds) | Store::Lifo(seeds) => seeds.iter().cloned().collect(),
167            Store::Priority(seeds, _) => seeds.iter().map(|entry| entry.seed.clone()).collect(),
168        };
169        Some(seeds)
170    }
171}
172
173impl Extend<Seed> for InMemFrontier {
174    /// Queues seeds as is, e.g. from a [`snapshot`](Frontier::snapshot),
175    /// without any facade refusing them.
176    fn extend<I: IntoIterator<Item = Seed>>(&mut self, seeds: I) {
177        for seed in seeds {
178            self.insert(seed);
179        }
180    }
181}
182
183impl FromIterator<Seed> for InMemFrontier {
184    /// Collects seeds into a FIFO frontier, e.g. to restore a snapshot.
185    fn from_iter<I: IntoIterator<Item = Seed>>(seeds: I) -> Self {
186        let mut frontier = Self::fifo();
187        frontier.extend(seeds);
188        frontier
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use crate::frontier::test_util::{pop_path, seed};
196
197    #[tokio::test]
198    async fn fifo_and_lifo_order() {
199        let fifo = InMemFrontier::fifo();
200        let lifo = InMemFrontier::lifo();
201        for path in ["a", "b"] {
202            assert!(fifo.push(seed(path)).await.unwrap().is_queued());
203            assert!(lifo.push(seed(path)).await.unwrap().is_queued());
204        }
205        assert_eq!(fifo.size_hint(), Some(2));
206
207        assert_eq!(pop_path(&fifo).await.as_deref(), Some("a"));
208        assert_eq!(pop_path(&lifo).await.as_deref(), Some("b"));
209        assert_eq!(pop_path(&fifo).await.as_deref(), Some("b"));
210        assert!(matches!(fifo.pop().await.unwrap(), Next::Empty));
211    }
212
213    #[tokio::test]
214    async fn priority_order() {
215        let frontier = InMemFrontier::priority();
216        for (path, priority) in [("low", -1), ("first", 0), ("high", 5), ("second", 0)] {
217            frontier
218                .push(seed(path).with_priority(priority))
219                .await
220                .unwrap();
221        }
222        let mut order = Vec::new();
223        while let Some(path) = pop_path(&frontier).await {
224            order.push(path);
225        }
226        assert_eq!(order, ["high", "first", "second", "low"]);
227    }
228}