solana_core/repair/
outstanding_requests.rs

1use {
2    crate::repair::request_response::RequestResponse,
3    lru::LruCache,
4    rand::{thread_rng, Rng},
5    solana_ledger::shred::Nonce,
6};
7
8pub const DEFAULT_REQUEST_EXPIRATION_MS: u64 = 60_000;
9
10pub struct OutstandingRequests<T> {
11    requests: LruCache<Nonce, RequestStatus<T>>,
12}
13
14impl<T, S: ?Sized> OutstandingRequests<T>
15where
16    T: RequestResponse<Response = S>,
17{
18    // Returns boolean indicating whether sufficient time has passed for a request with
19    // the given timestamp to be made
20    pub fn add_request(&mut self, request: T, now: u64) -> Nonce {
21        let num_expected_responses = request.num_expected_responses();
22        let nonce = thread_rng().gen_range(0..Nonce::MAX);
23        self.requests.put(
24            nonce,
25            RequestStatus {
26                expire_timestamp: now + DEFAULT_REQUEST_EXPIRATION_MS,
27                num_expected_responses,
28                request,
29            },
30        );
31        nonce
32    }
33
34    pub fn register_response<R>(
35        &mut self,
36        nonce: u32,
37        response: &S,
38        now: u64,
39        // runs if the response was valid
40        success_fn: impl Fn(&T) -> R,
41    ) -> Option<R> {
42        let (response, should_delete) = self
43            .requests
44            .get_mut(&nonce)
45            .map(|status| {
46                if status.num_expected_responses > 0
47                    && now < status.expire_timestamp
48                    && status.request.verify_response(response)
49                {
50                    status.num_expected_responses -= 1;
51                    (
52                        Some(success_fn(&status.request)),
53                        status.num_expected_responses == 0,
54                    )
55                } else {
56                    (None, true)
57                }
58            })
59            .unwrap_or((None, false));
60
61        if should_delete {
62            self.requests
63                .pop(&nonce)
64                .expect("Delete must delete existing object");
65        }
66
67        response
68    }
69}
70
71impl<T> Default for OutstandingRequests<T> {
72    fn default() -> Self {
73        Self {
74            requests: LruCache::new(16 * 1024),
75        }
76    }
77}
78
79pub struct RequestStatus<T> {
80    expire_timestamp: u64,
81    num_expected_responses: u32,
82    request: T,
83}
84
85#[cfg(test)]
86pub(crate) mod tests {
87    use {
88        super::*, crate::repair::serve_repair::ShredRepairType, solana_keypair::Keypair,
89        solana_ledger::shred::Shredder, solana_time_utils::timestamp,
90    };
91
92    #[test]
93    fn test_add_request() {
94        let repair_type = ShredRepairType::Orphan(9);
95        let mut outstanding_requests = OutstandingRequests::default();
96        let nonce = outstanding_requests.add_request(repair_type, timestamp());
97        let request_status = outstanding_requests.requests.get(&nonce).unwrap();
98        assert_eq!(request_status.request, repair_type);
99        assert_eq!(
100            request_status.num_expected_responses,
101            repair_type.num_expected_responses()
102        );
103    }
104
105    #[test]
106    fn test_timeout_expired_remove() {
107        let repair_type = ShredRepairType::Orphan(9);
108        let mut outstanding_requests = OutstandingRequests::default();
109        let nonce = outstanding_requests.add_request(repair_type, timestamp());
110        let keypair = Keypair::new();
111        let shred = Shredder::single_shred_for_tests(0, &keypair);
112
113        let expire_timestamp = outstanding_requests
114            .requests
115            .get(&nonce)
116            .unwrap()
117            .expire_timestamp;
118
119        assert!(outstanding_requests
120            .register_response(nonce, shred.payload(), expire_timestamp + 1, |_| ())
121            .is_none());
122        assert!(outstanding_requests.requests.get(&nonce).is_none());
123    }
124
125    #[test]
126    fn test_register_response() {
127        let repair_type = ShredRepairType::Orphan(9);
128        let mut outstanding_requests = OutstandingRequests::default();
129        let nonce = outstanding_requests.add_request(repair_type, timestamp());
130        let keypair = Keypair::new();
131        let shred = Shredder::single_shred_for_tests(0, &keypair);
132        let mut expire_timestamp = outstanding_requests
133            .requests
134            .get(&nonce)
135            .unwrap()
136            .expire_timestamp;
137        let mut num_expected_responses = outstanding_requests
138            .requests
139            .get(&nonce)
140            .unwrap()
141            .num_expected_responses;
142        assert!(num_expected_responses > 1);
143
144        // Response that passes all checks should decrease num_expected_responses
145        assert!(outstanding_requests
146            .register_response(nonce, shred.payload(), expire_timestamp - 1, |_| ())
147            .is_some());
148        num_expected_responses -= 1;
149        assert_eq!(
150            outstanding_requests
151                .requests
152                .get(&nonce)
153                .unwrap()
154                .num_expected_responses,
155            num_expected_responses
156        );
157
158        // Response with incorrect nonce is ignored
159        assert!(outstanding_requests
160            .register_response(nonce + 1, shred.payload(), expire_timestamp - 1, |_| ())
161            .is_none());
162        assert!(outstanding_requests
163            .register_response(nonce + 1, shred.payload(), expire_timestamp, |_| ())
164            .is_none());
165        assert_eq!(
166            outstanding_requests
167                .requests
168                .get(&nonce)
169                .unwrap()
170                .num_expected_responses,
171            num_expected_responses
172        );
173
174        // Response with timestamp over limit should remove status, preventing late
175        // responses from being accepted
176        assert!(outstanding_requests
177            .register_response(nonce, shred.payload(), expire_timestamp, |_| ())
178            .is_none());
179        assert!(outstanding_requests.requests.get(&nonce).is_none());
180
181        // If number of outstanding requests hits zero, should also remove the entry
182        let nonce = outstanding_requests.add_request(repair_type, timestamp());
183        expire_timestamp = outstanding_requests
184            .requests
185            .get(&nonce)
186            .unwrap()
187            .expire_timestamp;
188        num_expected_responses = outstanding_requests
189            .requests
190            .get(&nonce)
191            .unwrap()
192            .num_expected_responses;
193        assert!(num_expected_responses > 1);
194        for _ in 0..num_expected_responses {
195            assert!(outstanding_requests.requests.get(&nonce).is_some());
196            assert!(outstanding_requests
197                .register_response(nonce, shred.payload(), expire_timestamp - 1, |_| ())
198                .is_some());
199        }
200        assert!(outstanding_requests.requests.get(&nonce).is_none());
201    }
202}