1//! An internal pool object that we use to implement HsCircPool.
23use std::time::{Duration, Instant};
45use crate::{
6 hspool::{HsCircStem, HsCircStemKind},
7 AbstractCirc,
8};
9use rand::Rng;
10use tor_basic_utils::RngExt as _;
1112/// A collection of circuits used to fulfil onion-service-related requests.
13pub(super) struct Pool<C: AbstractCirc> {
14/// The collection of circuits themselves, in no particular order.
15circuits: Vec<HsCircStem<C>>,
1617/// The number of NAIVE elements that we would like to have in our pool.
18stem_target: usize,
1920/// The number of GUARDED elements that we would like to have in our pool.
21guarded_stem_target: usize,
2223/// True if we have exhausted our pool since the last time we decided
24 /// whether to change our target level.
25have_been_exhausted: bool,
2627/// True if we have been under 4/5 of our target since the last time we
28 /// decided whether to change it.
29have_been_under_highwater: bool,
3031/// Last time when we changed our target size.
32last_changed_target: Option<Instant>,
33}
3435/// Our default (and minimum) target NAIVE pool size.
36const DEFAULT_NAIVE_STEM_TARGET: usize = 3;
3738/// Our default (and minimum) target GUARDED pool size.
39const DEFAULT_GUARDED_STEM_TARGET: usize = 1;
4041/// Our maximum target NAIVE pool size. We will never let our NAIVE target grow above this
42/// value.
43const MAX_NAIVE_STEM_TARGET: usize = 384;
4445/// Our maximum target GUARDED pool size. We will never let our GUARDED target grow above this
46/// value.
47const MAX_GUARDED_STEM_TARGET: usize = 128;
4849/// A type of circuit we would like to launch.
50///
51/// [`ForLaunch::note_circ_launched`] should be called whenever a circuit
52/// of this [`HsCircStemKind`] is launched, to decrement the internal target `count`.
53pub(super) struct ForLaunch<'a> {
54/// The kind of circuit we want to launch.
55kind: HsCircStemKind,
56/// How many circuits of this kind do we need?
57 ///
58 /// This is a mutable reference to one of the target values from [`CircsToLaunch`];
59 /// we decrement it when we have launched a circuit of this type.
60count: &'a mut usize,
61}
6263impl<'a> ForLaunch<'a> {
64/// A circuit was launched, decrement the current target for its kind.
65pub(super) fn note_circ_launched(self) {
66*self.count -= 1;
67 }
6869/// The kind of circuit we want to launch.
70pub(super) fn kind(&self) -> HsCircStemKind {
71self.kind
72 }
73}
7475/// The circuits we need to launch.
76pub(super) struct CircsToLaunch {
77/// The number of NAIVE circuits we want to launch.
78stem_target: usize,
79/// The number of GUARDED circuits we want to launch.
80guarded_stem_target: usize,
81}
8283impl CircsToLaunch {
84/// Return a [`ForLaunch`] representing a circuit we would like to launch.
85pub(super) fn for_launch(&mut self) -> ForLaunch {
86// We start by launching NAIVE circuits.
87if self.stem_target > 0 {
88 ForLaunch {
89 kind: HsCircStemKind::Naive,
90 count: &mut self.stem_target,
91 }
92 } else {
93// If we have enough NAIVE circuits, we can start launching GUARDED ones too.
94ForLaunch {
95 kind: HsCircStemKind::Guarded,
96 count: &mut self.guarded_stem_target,
97 }
98 }
99 }
100101/// Return the number of NAIVE circuits we would like to launch.
102pub(super) fn stem(&self) -> usize {
103self.stem_target
104 }
105106/// Return the number of GUARDED circuits we would like to launch.
107pub(super) fn guarded_stem(&self) -> usize {
108self.guarded_stem_target
109 }
110111/// Return the total number of circuits we would currently like to launch.
112pub(super) fn n_to_launch(&self) -> usize {
113self.stem_target + self.guarded_stem_target
114 }
115}
116117impl<C: AbstractCirc> Default for Pool<C> {
118fn default() -> Self {
119Self {
120 circuits: Vec::new(),
121 stem_target: DEFAULT_NAIVE_STEM_TARGET,
122 guarded_stem_target: DEFAULT_GUARDED_STEM_TARGET,
123 have_been_exhausted: false,
124 have_been_under_highwater: false,
125 last_changed_target: None,
126 }
127 }
128}
129130impl<C: AbstractCirc> Pool<C> {
131/// Add `circ` to this pool
132pub(super) fn insert(&mut self, circ: HsCircStem<C>) {
133self.circuits.push(circ);
134 }
135136/// Remove every circuit from this pool for which `f` returns false.
137pub(super) fn retain<F>(&mut self, f: F)
138where
139F: FnMut(&HsCircStem<C>) -> bool,
140 {
141self.circuits.retain(f);
142 }
143144/// Return true if we are very low on circuits and should build more immediately.
145pub(super) fn very_low(&self) -> bool {
146self.circuits.len() <= self.target() / 3
147}
148149/// Return a [`CircsToLaunch`] describing the circuits we would currently like to launch.
150pub(super) fn circs_to_launch(&self) -> CircsToLaunch {
151 CircsToLaunch {
152 stem_target: self.stems_to_launch(),
153 guarded_stem_target: self.guarded_stems_to_launch(),
154 }
155 }
156157/// Return the number of NAIVE circuits we would currently like to launch.
158fn stems_to_launch(&self) -> usize {
159let circ_count = self
160.circuits
161 .iter()
162 .filter(|c| c.kind == HsCircStemKind::Naive)
163 .count();
164165self.stem_target.saturating_sub(circ_count)
166 }
167168/// Return the number of GUARDED circuits we would currently like to launch.
169fn guarded_stems_to_launch(&self) -> usize {
170let circ_count = self
171.circuits
172 .iter()
173 .filter(|c| c.kind == HsCircStemKind::Guarded)
174 .count();
175176self.guarded_stem_target.saturating_sub(circ_count)
177 }
178179/// Return the total number of circuits we would like to launch.
180 ///
181 /// We do not discard when we are _above_ this threshold, but we do
182 /// try to build when we are low.
183fn target(&self) -> usize {
184self.stem_target + self.guarded_stem_target
185 }
186187/// If there is any circuit in this pool for which `f` returns true and that satisfies
188 /// all of the specified [`HsCircPrefs`], return one such circuit at random, and remove
189 /// it from the pool.
190 ///
191 /// If none of the circuits satisfy `prefs`, return a randomly selected circuit for which `f`
192 /// returns true, and remove it from the pool.
193pub(super) fn take_one_where<R, F>(
194&mut self,
195 rng: &mut R,
196 f: F,
197 prefs: &HsCircPrefs,
198 ) -> Option<HsCircStem<C>>
199where
200R: Rng,
201 F: Fn(&HsCircStem<C>) -> bool,
202 {
203let rv = match random_idx_where(rng, &mut self.circuits[..], |circ_stem| {
204// First, check if any circuit matches _all_ the prefs
205circ_stem.satisfies_prefs(prefs) && f(circ_stem)
206 })
207 .or_else(|| {
208// Select a circuit satisfying `f` at random.
209random_idx_where(rng, &mut self.circuits[..], f)
210 }) {
211Some(idx) => Some(self.circuits.swap_remove(idx)),
212None => None,
213 };
214215if self.circuits.is_empty() {
216self.have_been_exhausted = true;
217self.have_been_under_highwater = true;
218 } else if self.circuits.len() < self.target() * 4 / 5 {
219self.have_been_under_highwater = true;
220 }
221222 rv
223 }
224225/// Update the target sizes for our pool.
226 ///
227 /// This updates our target numbers of NAIVE and GUARDED circuits.
228pub(super) fn update_target_size(&mut self, now: Instant) {
229/// Minimum amount of time that must elapse between a change and a
230 /// decision to grow our pool. We use this to control the rate of
231 /// growth and make sure that we are allowing enough time for circuits
232 /// to complete.
233const MIN_TIME_TO_GROW: Duration = Duration::from_secs(120);
234/// Minimum amount of time that must elapse between a target change and
235 /// a decisions to shrink our target. We use this to make sure that we
236 /// aren't shrinking too rapidly, and that we are allowing enough time
237 /// for the pool to actually get used.
238const MIN_TIME_TO_SHRINK: Duration = Duration::from_secs(600);
239240let last_changed = self.last_changed_target.get_or_insert(now);
241let time_since_last_change = now.saturating_duration_since(*last_changed);
242243// TODO: we may want to have separate have_been_exhausted/have_been_under_highwater
244 // flags for NAIVE and GUARDED circuits.
245 //
246 // TODO: stem_target and guarded_stem_target currently grow/shrink at the same rate,
247 // which is not ideal.
248 //
249 // Instead, we should switch to an adaptive strategy, where the two targets are updated
250 // based on how many NAIVE/GUARDED circuit requests we got.
251if self.have_been_exhausted {
252if time_since_last_change < MIN_TIME_TO_GROW {
253return;
254 }
255self.stem_target *= 2;
256self.guarded_stem_target *= 2;
257 } else if !self.have_been_under_highwater {
258if time_since_last_change < MIN_TIME_TO_SHRINK {
259return;
260 }
261262self.stem_target /= 2;
263self.guarded_stem_target /= 2;
264 }
265self.last_changed_target = Some(now);
266self.stem_target = self
267.stem_target
268 .clamp(DEFAULT_NAIVE_STEM_TARGET, MAX_NAIVE_STEM_TARGET);
269self.guarded_stem_target = self
270.guarded_stem_target
271 .clamp(DEFAULT_GUARDED_STEM_TARGET, MAX_GUARDED_STEM_TARGET);
272self.have_been_exhausted = false;
273self.have_been_under_highwater = false;
274 }
275276/// Purge all the circuits from the pool.
277#[allow(clippy::unnecessary_wraps)] // for consistency and future-proofing
278pub(super) fn retire_all_circuits(&mut self) -> Result<(), tor_config::ReconfigureError> {
279self.have_been_exhausted = true;
280281// Purge all circuits from this pool
282self.circuits.clear();
283284Ok(())
285 }
286}
287288/// Preferences for what kind of circuit to select from the pool.
289#[derive(Default, Debug, Clone)]
290pub(super) struct HsCircPrefs {
291/// If `Some`, specifies the [`HsCircStemKind`] we would like.
292pub(super) kind_prefs: Option<HsCircStemKind>,
293}
294295impl HsCircPrefs {
296/// Set the preferred [`HsCircStemKind`].
297pub(super) fn preferred_stem_kind(&mut self, kind: HsCircStemKind) -> &mut Self {
298self.kind_prefs = Some(kind);
299self
300}
301}
302303/// Helper: find a random item `elt` in `slice` such that `predicate(elt)` is
304/// true. Return the index of that item.
305///
306/// Can arbitrarily reorder `slice`. This allows us to visit the indices in uniform-at-random
307/// order, without having to do any O(N) operations or allocations.
308fn random_idx_where<R, T, P>(rng: &mut R, mut slice: &mut [T], predicate: P) -> Option<usize>
309where
310R: Rng,
311 P: Fn(&T) -> bool,
312{
313while !slice.is_empty() {
314let idx = rng
315 .gen_range_checked(0..slice.len())
316 .expect("slice was not empty but is now empty");
317if predicate(&slice[idx]) {
318return Some(idx);
319 }
320let last_idx = slice.len() - 1;
321// Move the one we just tried to the end,
322 // and eliminate it from consideration.
323slice.swap(idx, last_idx);
324 slice = &mut slice[..last_idx];
325 }
326// We didn't find any.
327None
328}
329330#[cfg(test)]
331mod test {
332// @@ begin test lint list maintained by maint/add_warning @@
333#![allow(clippy::bool_assert_comparison)]
334 #![allow(clippy::clone_on_copy)]
335 #![allow(clippy::dbg_macro)]
336 #![allow(clippy::mixed_attributes_style)]
337 #![allow(clippy::print_stderr)]
338 #![allow(clippy::print_stdout)]
339 #![allow(clippy::single_char_pattern)]
340 #![allow(clippy::unwrap_used)]
341 #![allow(clippy::unchecked_duration_subtraction)]
342 #![allow(clippy::useless_vec)]
343 #![allow(clippy::needless_pass_by_value)]
344//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
345346use super::*;
347use tor_basic_utils::test_rng::testing_rng;
348349#[test]
350fn random_idx() {
351let mut rng = testing_rng();
352let mut orig_numbers: Vec<i32> = vec![1, 3, 4, 8, 11, 19, 12, 6, 27];
353let mut numbers = orig_numbers.clone();
354355let mut found: std::collections::HashMap<i32, bool> =
356 numbers.iter().map(|n| (*n, false)).collect();
357358for _ in 0..1000 {
359let idx = random_idx_where(&mut rng, &mut numbers[..], |n| n & 1 == 1).unwrap();
360assert!(numbers[idx] & 1 == 1);
361 found.insert(numbers[idx], true);
362 }
363364for num in numbers.iter() {
365assert!(found[num] == (num & 1 == 1));
366 }
367368// Number may be reordered, but should still have the same elements.
369numbers.sort();
370 orig_numbers.sort();
371assert_eq!(numbers, orig_numbers);
372 }
373374#[test]
375fn random_idx_empty() {
376let mut rng = testing_rng();
377let idx = random_idx_where(&mut rng, &mut [], |_: &i32| panic!());
378assert_eq!(idx, None);
379 }
380381#[test]
382fn random_idx_none() {
383let mut rng = testing_rng();
384let mut numbers: Vec<i32> = vec![1, 3, 4, 8, 11, 19, 12, 6, 27];
385assert_eq!(
386 random_idx_where(&mut rng, &mut numbers[..], |_: &i32| false),
387None
388);
389 }
390}