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 tor_basic_utils::retry::RetryDelay;
9
use tor_dircommon::{authority::AuthorityContacts, config::DirTolerance};
10
use tor_rtcompat::PreferredRuntime;
11

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

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

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

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

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

            
59
        match res {
60
            Ok(()) => {
61
                retry.reset();
62

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

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