use crate::{PathConfig, PreemptiveCircuitConfig, TargetPort, TargetTunnelUsage};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
use tracing::warn;
pub(crate) struct PreemptiveCircuitPredictor {
usages: HashMap<Option<TargetPort>, Instant>,
config: tor_config::MutCfg<PreemptiveCircuitConfig>,
}
impl PreemptiveCircuitPredictor {
pub(crate) fn new(config: PreemptiveCircuitConfig) -> Self {
let mut usages = HashMap::new();
for port in &config.initial_predicted_ports {
usages.insert(Some(TargetPort::ipv4(*port)), Instant::now());
}
usages.insert(None, Instant::now());
Self {
usages,
config: config.into(),
}
}
pub(crate) fn config(&self) -> Arc<PreemptiveCircuitConfig> {
self.config.get()
}
pub(crate) fn set_config(&self, mut new_config: PreemptiveCircuitConfig) {
self.config.map_and_replace(|cfg| {
new_config
.initial_predicted_ports
.clone_from(&cfg.initial_predicted_ports);
new_config
});
}
pub(crate) fn predict(&self, path_config: &PathConfig) -> Vec<TargetTunnelUsage> {
let config = self.config();
let now = Instant::now();
let circs = config.min_exit_circs_for_port;
self.usages
.iter()
.filter(|&(_, &time)| {
time.checked_add(config.prediction_lifetime)
.map(|t| t > now)
.unwrap_or_else(|| {
warn!("failed to represent preemptive circuit prediction lifetime as an Instant");
false
})
})
.map(|(&port, _)| {
let require_stability = port.is_some_and(|p| path_config.long_lived_ports.contains(&p.port));
TargetTunnelUsage::Preemptive {
port, circs, require_stability,
}
})
.collect()
}
pub(crate) fn note_usage(&mut self, port: Option<TargetPort>, time: Instant) {
self.usages.insert(port, time);
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use crate::{
PathConfig, PreemptiveCircuitConfig, PreemptiveCircuitPredictor, TargetPort,
TargetTunnelUsage,
};
use std::time::{Duration, Instant};
use crate::isolation::test::{IsolationTokenEq, assert_isoleq};
#[test]
fn predicts_starting_ports() {
let path_config = PathConfig::default();
let mut cfg = PreemptiveCircuitConfig::builder();
cfg.set_initial_predicted_ports(vec![]);
cfg.prediction_lifetime(Duration::from_secs(2));
let predictor = PreemptiveCircuitPredictor::new(cfg.build().unwrap());
assert_isoleq!(
predictor.predict(&path_config),
vec![TargetTunnelUsage::Preemptive {
port: None,
circs: 2,
require_stability: false,
}]
);
let mut cfg = PreemptiveCircuitConfig::builder();
cfg.set_initial_predicted_ports(vec![80]);
cfg.prediction_lifetime(Duration::from_secs(2));
let predictor = PreemptiveCircuitPredictor::new(cfg.build().unwrap());
let results = predictor.predict(&path_config);
assert_eq!(results.len(), 2);
assert!(
results
.iter()
.any(|r| r.isol_eq(&TargetTunnelUsage::Preemptive {
port: None,
circs: 2,
require_stability: false,
}))
);
assert!(
results
.iter()
.any(|r| r.isol_eq(&TargetTunnelUsage::Preemptive {
port: Some(TargetPort::ipv4(80)),
circs: 2,
require_stability: false,
}))
);
}
#[test]
fn predicts_used_ports() {
let path_config = PathConfig::default();
let mut cfg = PreemptiveCircuitConfig::builder();
cfg.set_initial_predicted_ports(vec![]);
cfg.prediction_lifetime(Duration::from_secs(2));
let mut predictor = PreemptiveCircuitPredictor::new(cfg.build().unwrap());
assert_isoleq!(
predictor.predict(&path_config),
vec![TargetTunnelUsage::Preemptive {
port: None,
circs: 2,
require_stability: false,
}]
);
predictor.note_usage(Some(TargetPort::ipv4(1234)), Instant::now());
let results = predictor.predict(&path_config);
assert_eq!(results.len(), 2);
assert!(
results
.iter()
.any(|r| r.isol_eq(&TargetTunnelUsage::Preemptive {
port: None,
circs: 2,
require_stability: false,
}))
);
assert!(
results
.iter()
.any(|r| r.isol_eq(&TargetTunnelUsage::Preemptive {
port: Some(TargetPort::ipv4(1234)),
circs: 2,
require_stability: false,
}))
);
}
#[test]
fn does_not_predict_old_ports() {
let path_config = PathConfig::default();
let mut cfg = PreemptiveCircuitConfig::builder();
cfg.set_initial_predicted_ports(vec![]);
cfg.prediction_lifetime(Duration::from_secs(2));
let mut predictor = PreemptiveCircuitPredictor::new(cfg.build().unwrap());
let now = Instant::now();
let three_seconds_ago = now - Duration::from_secs(2 + 1);
predictor.note_usage(Some(TargetPort::ipv4(2345)), three_seconds_ago);
assert_isoleq!(
predictor.predict(&path_config),
vec![TargetTunnelUsage::Preemptive {
port: None,
circs: 2,
require_stability: false,
}]
);
}
}