use crate::{Error, Result};
use arti_client::{TorClient as ArtiClient, TorClientConfig};
use parking_lot::Mutex;
use std::collections::VecDeque;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Semaphore;
use tor_rtcompat::PreferredRuntime;
use tracing::{debug, warn};
#[derive(Debug, Clone)]
pub struct PrewarmConfig {
pub min_circuits: usize,
pub max_circuits: usize,
pub refill_interval: Duration,
pub max_age: Duration,
}
impl Default for PrewarmConfig {
fn default() -> Self {
Self {
min_circuits: 2,
max_circuits: 5,
refill_interval: Duration::from_secs(30),
max_age: Duration::from_secs(300), }
}
}
#[derive(Debug)]
struct PrewarmedCircuit {
created_at: Instant,
isolation_token: u64,
}
pub struct CircuitPrewarmer {
config: PrewarmConfig,
client: Arc<ArtiClient<PreferredRuntime>>,
circuits: Arc<Mutex<VecDeque<PrewarmedCircuit>>>,
semaphore: Arc<Semaphore>,
shutdown: Arc<tokio::sync::Notify>,
}
impl CircuitPrewarmer {
pub async fn new(config: PrewarmConfig) -> Result<Self> {
let arti_config = TorClientConfig::default();
let client = ArtiClient::create_bootstrapped(arti_config)
.await
.map_err(|e| Error::bootstrap(e.to_string()))?;
Ok(Self {
config,
client: Arc::new(client),
circuits: Arc::new(Mutex::new(VecDeque::new())),
semaphore: Arc::new(Semaphore::new(1)),
shutdown: Arc::new(tokio::sync::Notify::new()),
})
}
pub fn start(&self) -> tokio::task::JoinHandle<()> {
let config = self.config.clone();
let circuits = Arc::clone(&self.circuits);
let client = Arc::clone(&self.client);
let semaphore = Arc::clone(&self.semaphore);
let shutdown = Arc::clone(&self.shutdown);
tokio::spawn(async move {
loop {
tokio::select! {
_ = tokio::time::sleep(config.refill_interval) => {
{
let mut lock = circuits.lock();
lock.retain(|c| c.created_at.elapsed() < config.max_age);
}
let current_count = circuits.lock().len();
if current_count < config.min_circuits {
let to_create = config.max_circuits - current_count;
for _ in 0..to_create {
if let Ok(_permit) = semaphore.clone().try_acquire_owned() {
let circuits = Arc::clone(&circuits);
let client = Arc::clone(&client);
let max_circuits = config.max_circuits;
tokio::spawn(async move {
if let Err(e) = prewarm_one(&client, &circuits, max_circuits).await {
warn!(error = %e, "Failed to prewarm circuit");
}
});
}
}
}
}
_ = shutdown.notified() => {
debug!("Circuit prewarmer shutting down");
break;
}
}
}
})
}
pub fn get(&self) -> Option<u64> {
let mut lock = self.circuits.lock();
while let Some(circuit) = lock.pop_front() {
if circuit.created_at.elapsed() < self.config.max_age {
return Some(circuit.isolation_token);
}
}
None
}
pub fn available(&self) -> usize {
self.circuits.lock().len()
}
pub fn shutdown(&self) {
self.shutdown.notify_one();
}
}
async fn prewarm_one(
client: &ArtiClient<PreferredRuntime>,
circuits: &Mutex<VecDeque<PrewarmedCircuit>>,
max_circuits: usize,
) -> Result<()> {
let isolation_token = rand::random::<u64>();
let _stream = client
.connect(("example.com", 80u16))
.await
.map_err(|e| Error::Circuit {
message: format!("Failed to prewarm circuit: {}", e),
source: Some(Box::new(e)),
})?;
let circuit = PrewarmedCircuit {
created_at: Instant::now(),
isolation_token,
};
let mut lock = circuits.lock();
if lock.len() < max_circuits {
lock.push_back(circuit);
debug!(count = lock.len(), "Prewarmed circuit added");
}
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
#[test]
fn test_prewarm_config_default() {
let config = PrewarmConfig::default();
assert_eq!(config.min_circuits, 2);
assert_eq!(config.max_circuits, 5);
}
}