1
//! Proof-of-concept for applied operation use.
2
//!
3
//! These functions should obviously be moved to [`super`] at one point.
4

            
5
use r2d2::Pool;
6
use r2d2_sqlite::SqliteConnectionManager;
7
use rand::{Rng, seq::SliceRandom};
8
use strum::IntoEnumIterator;
9
use tor_basic_utils::retry::RetryDelay;
10
use tor_dircommon::{authority::AuthorityContacts, config::DirTolerance};
11
use tor_rtcompat::PreferredRuntime;
12

            
13
use crate::{
14
    database::{ContentEncoding, Timestamp},
15
    err::IsFatal,
16
    mirror::operation::{ConsensusBoundData, StaticEngine},
17
    types::FlavoredConsensusUnverified,
18
};
19

            
20
/// Proof-of-concept main execution function for this module
21
///
22
/// Right now, this is a proof-of-concept that just panics in the case of a
23
/// fatal error, but does proper retry handling for non-fatal errors.
24
// TODO DIRMIRROR: Make this not a poc.
25
// TODO DIRMIRROR: Add logging.
26
// TODO DIRMIRROR: Diziet thinks the endpoint selection/retry logic is broken
27
//   eg that it could reach `expect("attempted all authorities")`.
28
//   At the very least it is confusing.  See
29
//   https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/3664#note_3352738
30
async fn serve<T: FlavoredConsensusUnverified, R: Rng, F: Fn() -> Timestamp>(
31
    pool: &Pool<SqliteConnectionManager>,
32
    authorities: AuthorityContacts,
33
    tolerance: DirTolerance,
34
    rng: &mut R,
35
    now_fn: F,
36
) {
37
    let mut data = ConsensusBoundData::<T>::None;
38
    let engine = StaticEngine {
39
        authorities,
40
        tolerance,
41
        rt: PreferredRuntime::current().expect("unable to get runtime"),
42
        encodings: ContentEncoding::iter().collect(),
43
        _phantom: Default::default(),
44
    };
45

            
46
    // Shuffle the list of download endpoints.
47
    let mut downloads = engine.authorities.downloads().clone();
48
    downloads.shuffle(rng);
49
    // Keeps track of the authority we currently use, i.e. preferred authority.
50
    let mut current = 0;
51

            
52
    let mut retry = RetryDelay::default();
53
    loop {
54
        let endpoint = downloads.get(current).expect("attempted all authorities");
55

            
56
        // Perform the FSM execution.
57
        let res = engine
58
            .execute(pool, &mut data, endpoint, now_fn(), rng)
59
            .await;
60

            
61
        match res {
62
            Ok(()) => {
63
                retry.reset();
64

            
65
                // Swap the currently used authority with the front and reset
66
                // current to zero.
67
                //
68
                // With this design, we will loose track on which authorities
69
                // were successful and which were not on every successful
70
                // return.  At one point, we have to do this.  Probably after
71
                // every consensus, but not after every Ok.  However, for this
72
                // we would need a way to learn when we got a new consensus.
73
                // It would probably make most sense to modify the return type
74
                // of execute() to return something like the next state plus
75
                // previous state or maybe an even simpler bool that returns
76
                // true when the consensus got replaced.
77
                downloads.swap(0, current);
78
            }
79
            Err(e) => {
80
                // Check whether the error is fatal.
81
                if e.is_fatal() {
82
                    panic!("fatal error: {e}");
83
                }
84

            
85
                // Non-fatal error means we should wait and try again.
86
                current += 1;
87
                let delay = retry.next_delay(rng);
88
                tokio::time::sleep(delay).await;
89
            }
90
        }
91
    }
92
}