Skip to main content

radicle_protocol/fetcher/
state.rs

1//! Logical state for Git fetches happening in the node.
2//!
3//! See [`FetcherState`] for more information.
4//!
5//! See [`command`]'s for input into [`FetcherState`].
6//! See [`event`]'s for output from [`FetcherState`].
7
8pub mod command;
9pub mod event;
10
11pub use command::Command;
12pub use event::Event;
13use radicle::storage::refs::FeatureLevel;
14
15use std::collections::{BTreeMap, VecDeque};
16use std::num::NonZeroUsize;
17use std::time;
18
19use radicle_core::{NodeId, RepoId};
20
21use crate::fetcher::RefsToFetch;
22use crate::service::FETCH_TIMEOUT;
23
24/// Default for the maximum items per fetch queue.
25pub const MAX_FETCH_QUEUE_SIZE: usize = 128;
26/// Default for maximum concurrency per node.
27pub const MAX_CONCURRENCY: NonZeroUsize = NonZeroUsize::MIN;
28
29/// Configuration options for tuning the fetch process.
30///
31/// Note that these are not used directly by [`FetcherState`], but are
32/// maintained within the state so that the options can be tracked across queued
33/// fetches.
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
35pub struct FetchConfig {
36    timeout: time::Duration,
37    protocol: radicle_fetch::Config,
38}
39
40impl FetchConfig {
41    /// Construct the default [`FetchConfig`].
42    pub fn new() -> Self {
43        Self {
44            timeout: FETCH_TIMEOUT,
45            protocol: radicle_fetch::Config::default(),
46        }
47    }
48
49    /// Set the [`FetchConfig::timeout`] to the given [`time::Duration`].
50    pub fn with_timeout(mut self, timeout: time::Duration) -> Self {
51        self.timeout = timeout;
52        self
53    }
54
55    /// Set the [`FetchConfig::fetch_config`] to the given [`radicle_fetch::Config`].
56    pub fn with_fetch_config(mut self, config: radicle_fetch::Config) -> Self {
57        self.protocol = config;
58        self
59    }
60
61    /// Set the minimum feature level, within the [`FetchConfig::fetch_config`],
62    /// to the given [`FeatureLevel`].
63    pub fn with_minimum_feature_level(mut self, feature_level: FeatureLevel) -> Self {
64        self.protocol.level_min = feature_level;
65        self
66    }
67
68    /// Return the timeout duration configured for this fetch.
69    pub fn timeout(&self) -> time::Duration {
70        self.timeout
71    }
72
73    /// Return the [`radicle_fetch::Config`] configured for this fetch.
74    pub fn fetch_config(&self) -> radicle_fetch::Config {
75        self.protocol
76    }
77
78    /// Merge another [`FetchConfig`] with the current one.
79    /// For each field, the following semantics occur:
80    /// - `timeout`: the maximum timeout is taken
81    /// - `protocol.limit.refs`: the maximum limit is taken
82    /// - `protocol.limit.special`: the maximum limit is taken
83    /// - `protocol.level_min`: the minimum level is taken
84    fn merge(&mut self, other: FetchConfig) {
85        self.timeout = self.timeout.max(other.timeout);
86        self.protocol.limit.refs = self.protocol.limit.refs.max(other.protocol.limit.refs);
87        self.protocol.limit.special = self
88            .protocol
89            .limit
90            .special
91            .max(other.protocol.limit.special);
92        self.protocol.level_min = self.protocol.level_min.min(other.protocol.level_min);
93    }
94}
95
96impl Default for FetchConfig {
97    fn default() -> Self {
98        Self::new()
99    }
100}
101
102/// Logical state for Git fetches happening in the node.
103///
104/// A fetch can either be:
105///   - [`ActiveFetch`]: meaning it is currently being fetched from another node on the network
106///   - [`QueuedFetch`]: meaning it is expected to be fetched from a given node, but the
107///     repository is already being fetched, or the node is at capacity.
108///
109/// For any given repository, identified by its [`RepoId`], there can only be
110/// one fetch occurring for it at a given time. This prevents any concurrent
111/// fetches from clobbering overlapping references.
112///
113/// If the repository is actively being fetched, then that fetch will be queued
114/// for a later attempt.
115///
116/// For any given node, there is a configurable capacity so that only `N` number
117/// of fetches can happen with it concurrently. This does not guarantee that the
118/// node will actually allow this node to fetch from it – since it will maintain
119/// its own capacity for connections and load.
120#[derive(Clone, Debug, PartialEq, Eq)]
121pub struct FetcherState {
122    /// The active fetches that are occurring, ensuring only one fetch per repository.
123    active: BTreeMap<RepoId, ActiveFetch>,
124    /// The queued fetches, waiting to happen, where each node maintains its own queue.
125    queues: BTreeMap<NodeId, Queue>,
126    /// Configuration for maintaining the fetch state.
127    config: Config,
128}
129
130impl Default for FetcherState {
131    fn default() -> Self {
132        Self::new(Config::default())
133    }
134}
135
136impl FetcherState {
137    /// Initialize the [`FetcherState`] with the given [`Config`].
138    pub fn new(config: Config) -> Self {
139        Self {
140            active: BTreeMap::new(),
141            queues: BTreeMap::new(),
142            config,
143        }
144    }
145}
146
147impl FetcherState {
148    /// Process the handling of a [`Command`], delegating to its corresponding
149    /// method, and returning the corresponding [`Event`].
150    ///
151    /// This method is useful if the [`FetcherState`] is used in batch
152    /// processing and does need to be explicit about the underlying method.
153    pub fn handle(&mut self, command: Command) -> Event {
154        match command {
155            Command::Fetch(fetch) => self.fetch(fetch).into(),
156            Command::Fetched(fetched) => self.fetched(fetched).into(),
157            Command::Cancel(cancel) => self.cancel(cancel).into(),
158        }
159    }
160
161    /// Process a [`Fetch`] command, which transitions the given fetch to
162    /// active, if possible.
163    ///
164    /// The fetch will only transition to being active if:
165    ///
166    ///   - A fetch is not already happening for that repository, in which case it gets queued.
167    ///   - The node to be fetched from is not already at capacity, again it will be queued.
168    ///
169    /// [`Fetch`]: command::Fetch
170    pub fn fetch(
171        &mut self,
172        command::Fetch {
173            from,
174            rid,
175            refs,
176            config,
177        }: command::Fetch,
178    ) -> event::Fetch {
179        if let Some(active) = self.active.get(&rid) {
180            if active.refs == refs && active.from == from {
181                return event::Fetch::AlreadyFetching { rid, from };
182            } else {
183                return self.enqueue(rid, from, refs, config);
184            }
185        }
186
187        if self.is_at_node_capacity(&from) {
188            self.enqueue(rid, from, refs, config)
189        } else {
190            self.active.insert(
191                rid,
192                ActiveFetch {
193                    from,
194                    refs: refs.clone(),
195                },
196            );
197            event::Fetch::Started {
198                rid,
199                from,
200                refs,
201                config,
202            }
203        }
204    }
205
206    /// Process a [`Fetched`] command, which removes the given fetch from the
207    /// set of active fetches, but only if the active fetch was from the given
208    /// node.
209    ///
210    /// Note that this is agnostic of whether the fetch succeeded or failed.
211    ///
212    /// The caller will be notified if the completed fetch did not exist in the active set.
213    ///
214    /// [`Fetched`]: command::Fetched
215    pub fn fetched(&mut self, command::Fetched { from, rid }: command::Fetched) -> event::Fetched {
216        // Only the node that started the active fetch may complete it. A result from any
217        // other node is stale — e.g. a late completion delivered after the peer disconnected
218        // and the repo was re-fetched from elsewhere. Clearing the entry on a mismatch would
219        // drop the wrong (newer) fetch, so we leave it and report `NotFound`.
220        match self.active.get(&rid) {
221            Some(ActiveFetch {
222                from: active_from, ..
223            }) if *active_from == from => match self.active.remove(&rid) {
224                Some(ActiveFetch { from, refs }) => event::Fetched::Completed { from, rid, refs },
225                None => event::Fetched::NotFound { from, rid },
226            },
227            _ => event::Fetched::NotFound { from, rid },
228        }
229    }
230
231    /// Attempt to dequeue a [`QueuedFetch`] for the given node.
232    ///
233    /// This will only dequeue the fetch if it is not active, and the given node
234    /// is not at capacity.
235    pub fn dequeue(&mut self, from: &NodeId) -> Option<QueuedFetch> {
236        let is_at_capacity = self.is_at_node_capacity(from);
237        let queue = self.queues.get_mut(from)?;
238        let active = &self.active;
239        queue.try_dequeue(|QueuedFetch { rid, .. }| !is_at_capacity && !active.contains_key(rid))
240    }
241
242    /// Process a [`Cancel`] command, which cancels any active and/or queued
243    /// fetches for that given node.
244    ///
245    /// [`Cancel`]: command::Cancel
246    pub fn cancel(&mut self, command::Cancel { from }: command::Cancel) -> event::Cancel {
247        let cancelled: Vec<_> = self
248            .active
249            .iter()
250            .filter_map(|(rid, f)| (f.from == from).then_some(*rid))
251            .collect();
252        let ongoing: BTreeMap<_, _> = cancelled
253            .iter()
254            .filter_map(|rid| self.active.remove(rid).map(|f| (*rid, f)))
255            .collect();
256        let ongoing = (!ongoing.is_empty()).then_some(ongoing);
257        let queued = self.queues.remove(&from).filter(|queue| !queue.is_empty());
258
259        match (ongoing, queued) {
260            (None, None) => event::Cancel::Unexpected { from },
261            (ongoing, queued) => event::Cancel::Canceled {
262                from,
263                active: ongoing.unwrap_or_default(),
264                queued: queued.map(|q| q.queue).unwrap_or_default(),
265            },
266        }
267    }
268
269    fn enqueue(
270        &mut self,
271        rid: RepoId,
272        from: NodeId,
273        refs: RefsToFetch,
274        config: FetchConfig,
275    ) -> event::Fetch {
276        let queue = self
277            .queues
278            .entry(from)
279            .or_insert(Queue::new(self.config.maximum_queue_size));
280        match queue.enqueue(QueuedFetch { rid, refs, config }) {
281            Enqueue::CapacityReached(QueuedFetch { rid, refs, config }) => {
282                event::Fetch::QueueAtCapacity {
283                    rid,
284                    from,
285                    refs,
286                    config,
287                    capacity: queue.len(),
288                }
289            }
290            Enqueue::Queued => event::Fetch::Queued { rid, from },
291            Enqueue::Merged => event::Fetch::Queued { rid, from },
292        }
293    }
294}
295
296impl FetcherState {
297    /// Get the set of queued fetches.
298    pub fn queued_fetches(&self) -> &BTreeMap<NodeId, Queue> {
299        &self.queues
300    }
301
302    /// Get the set of active fetches.
303    pub fn active_fetches(&self) -> &BTreeMap<RepoId, ActiveFetch> {
304        &self.active
305    }
306
307    /// Get the [`ActiveFetch`] for the provided [`RepoId`], returning `None` if
308    /// it does not exist.
309    pub fn get_active_fetch(&self, rid: &RepoId) -> Option<&ActiveFetch> {
310        self.active.get(rid)
311    }
312
313    /// Check if the number of fetches exceeds the maximum number of concurrent
314    /// fetches for a given [`NodeId`].
315    ///
316    /// Returns `true` if the fetcher is fetching the maximum number of
317    /// repositories, for that node.
318    fn is_at_node_capacity(&self, node: &NodeId) -> bool {
319        let count = self.active.values().filter(|f| &f.from == node).count();
320        count >= self.config.maximum_concurrency.into()
321    }
322}
323
324/// Configuration for the [`FetcherState`].
325#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
326pub struct Config {
327    /// Maximum number of concurrent fetches per peer connection.
328    maximum_concurrency: NonZeroUsize,
329    /// Maximum fetching queue size for a single node.
330    maximum_queue_size: MaxQueueSize,
331}
332
333impl Config {
334    pub fn new() -> Self {
335        Self::default()
336    }
337
338    /// Maximum fetching queue size for a single node.
339    pub fn with_max_capacity(mut self, capacity: MaxQueueSize) -> Self {
340        self.maximum_queue_size = capacity;
341        self
342    }
343
344    /// Maximum number of concurrent fetches per peer connection.
345    pub fn with_max_concurrency(mut self, concurrency: NonZeroUsize) -> Self {
346        self.maximum_concurrency = concurrency;
347        self
348    }
349}
350
351impl Default for Config {
352    fn default() -> Self {
353        Self {
354            maximum_concurrency: MAX_CONCURRENCY,
355            maximum_queue_size: MaxQueueSize::default(),
356        }
357    }
358}
359
360/// An active fetch represents a repository being fetched by a particular node.
361#[derive(Clone, Debug, PartialEq, Eq)]
362pub struct ActiveFetch {
363    pub from: NodeId,
364    pub refs: RefsToFetch,
365}
366
367impl ActiveFetch {
368    /// The node from which the repository is being fetched.
369    pub fn from(&self) -> &NodeId {
370        &self.from
371    }
372
373    /// The set of references that fetch is being performed for.
374    pub fn refs(&self) -> &RefsToFetch {
375        &self.refs
376    }
377}
378
379/// A fetch that is waiting to be processed, in the fetch queue.
380#[derive(Debug, Clone, PartialEq, Eq, Hash)]
381pub struct QueuedFetch {
382    /// The repository that will be fetched.
383    pub rid: RepoId,
384    /// The references that the fetch is being performed for.
385    pub refs: RefsToFetch,
386    /// The configuration options to pass to the fetch process.
387    pub config: FetchConfig,
388}
389
390/// A queue for keeping track of fetches.
391///
392/// It ensures that the queue contains unique items for fetching, and does not
393/// exceed the provided maximum capacity.
394#[derive(Clone, Debug, PartialEq, Eq)]
395pub struct Queue {
396    queue: VecDeque<QueuedFetch>,
397    max_queue_size: MaxQueueSize,
398}
399
400/// The maximum number of fetches that can be queued for a single node.
401#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
402pub struct MaxQueueSize(usize);
403
404impl MaxQueueSize {
405    /// Minimum queue size is `1`.
406    pub const MIN: Self = MaxQueueSize(1);
407
408    /// Create a queue size, that must be larger than `0`.
409    pub fn new(size: NonZeroUsize) -> Self {
410        Self(size.into())
411    }
412
413    pub fn as_usize(&self) -> usize {
414        self.0
415    }
416
417    /// Checks if the `n` provided exceeds the maximum queue size.
418    fn is_exceeded_by(&self, n: usize) -> bool {
419        n >= self.0
420    }
421}
422
423impl Default for MaxQueueSize {
424    fn default() -> Self {
425        Self(MAX_FETCH_QUEUE_SIZE)
426    }
427}
428
429/// The result of [`Queue::enqueue`].
430#[must_use]
431#[derive(Debug, PartialEq, Eq)]
432pub(super) enum Enqueue {
433    /// The capacity of the queue has been exceeded, and the [`QueuedFetch`] is
434    /// returned.
435    CapacityReached(QueuedFetch),
436    /// The [`QueuedFetch`] was successfully queued.
437    Queued,
438    Merged,
439}
440
441impl Queue {
442    /// Create the [`Queue`] with the given [`MaxQueueSize`].
443    pub(super) fn new(max_queue_size: MaxQueueSize) -> Self {
444        Self {
445            queue: VecDeque::with_capacity(max_queue_size.0),
446            max_queue_size,
447        }
448    }
449
450    /// The current number of items in the queue.
451    pub(super) fn len(&self) -> usize {
452        self.queue.len()
453    }
454
455    /// Returns `true` if the [`Queue`] is empty.
456    pub(super) fn is_empty(&self) -> bool {
457        self.queue.is_empty()
458    }
459
460    /// Enqueues a fetch onto the back of the queue, and will only succeed if
461    /// the queue has not reached capacity and if the item is unique.
462    pub(super) fn enqueue(&mut self, fetch: QueuedFetch) -> Enqueue {
463        if let Some(existing) = self.queue.iter_mut().find(|qf| qf.rid == fetch.rid) {
464            existing.refs = existing.refs.clone().merge(fetch.refs);
465            existing.config.merge(fetch.config);
466            return Enqueue::Merged;
467        }
468
469        if self.max_queue_size.is_exceeded_by(self.queue.len()) {
470            Enqueue::CapacityReached(fetch)
471        } else {
472            self.queue.push_back(fetch);
473            Enqueue::Queued
474        }
475    }
476
477    /// Try to dequeue the next [`QueuedFetch`], but only if the `predicate`
478    /// holds; otherwise, it will be pushed back to the front of the queue.
479    pub(super) fn try_dequeue<P>(&mut self, predicate: P) -> Option<QueuedFetch>
480    where
481        P: FnOnce(&QueuedFetch) -> bool,
482    {
483        let fetch = self.dequeue()?;
484        if predicate(&fetch) {
485            Some(fetch)
486        } else {
487            self.queue.push_front(fetch);
488            None
489        }
490    }
491
492    /// Dequeues a fetch from the front of the queue.
493    pub(super) fn dequeue(&mut self) -> Option<QueuedFetch> {
494        self.queue.pop_front()
495    }
496
497    /// Return an iterator over the queued fetches.
498    pub fn iter<'a>(&'a self) -> QueueIter<'a> {
499        QueueIter {
500            inner: self.queue.iter(),
501        }
502    }
503}
504
505/// Iterator of the [`QueuedFetch`]'s
506pub struct QueueIter<'a> {
507    inner: std::collections::vec_deque::Iter<'a, QueuedFetch>,
508}
509
510impl<'a> Iterator for QueueIter<'a> {
511    type Item = &'a QueuedFetch;
512
513    fn next(&mut self) -> Option<Self::Item> {
514        self.inner.next()
515    }
516}
517
518impl<'a> IntoIterator for &'a Queue {
519    type Item = &'a QueuedFetch;
520    type IntoIter = QueueIter<'a>;
521
522    fn into_iter(self) -> Self::IntoIter {
523        self.iter()
524    }
525}