use std::sync::mpsc::{self, RecvTimeoutError};
use std::sync::{Condvar, LazyLock, Mutex, PoisonError};
use std::thread;
use std::time::{Duration, Instant};
use crate::error::Error;
use crate::resolve::ProxyStep;
#[cfg_attr(not(pac_quickjs), allow(dead_code))]
pub(super) fn run_with_timeout<F>(
timeout: Duration,
slots: &'static Slots,
job: F,
) -> Result<Vec<ProxyStep>, Error>
where
F: FnOnce(Option<Instant>) -> Result<Vec<ProxyStep>, Error> + Send + 'static,
{
if timeout.is_zero() {
return Err(Error::PacTimeout { timeout });
}
let deadline = Instant::now().checked_add(timeout);
let slot = slots.acquire(deadline).ok_or(Error::PacSaturated {
timeout,
limit: slots.limit,
})?;
let (sender, receiver) = mpsc::sync_channel(1);
thread::Builder::new()
.name("proxy-watch-pac".to_owned())
.spawn(move || {
let _slot = slot;
let _ = sender.send(job(deadline));
})
.map_err(|source| Error::io("spawning the PAC evaluation thread", source))?;
let left = deadline.map_or(timeout, |deadline| {
deadline.saturating_duration_since(Instant::now())
});
match receiver.recv_timeout(left) {
Ok(result) => result,
Err(RecvTimeoutError::Timeout) => Err(Error::PacTimeout { timeout }),
Err(RecvTimeoutError::Disconnected) => Err(Error::pac_evaluation(
"the PAC evaluation thread ended without producing a result",
)),
}
}
pub(super) static SLOTS: LazyLock<Slots> = LazyLock::new(|| {
Slots::new(thread::available_parallelism().map_or(4, |cores| cores.get().max(4)))
});
pub(super) struct Slots {
limit: usize,
taken: Mutex<usize>,
freed: Condvar,
}
impl Slots {
pub(super) const fn new(limit: usize) -> Self {
Self {
limit,
taken: Mutex::new(0),
freed: Condvar::new(),
}
}
#[cfg(feature = "pac-subprocess")]
pub(super) fn limit(&self) -> usize {
self.limit
}
pub(super) fn acquire(&'static self, deadline: Option<Instant>) -> Option<Slot> {
let mut taken = self.taken.lock().unwrap_or_else(PoisonError::into_inner);
while *taken >= self.limit {
taken = match deadline {
None => self
.freed
.wait(taken)
.unwrap_or_else(PoisonError::into_inner),
Some(deadline) => {
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
return None;
}
self.freed
.wait_timeout(taken, left)
.unwrap_or_else(PoisonError::into_inner)
.0
}
};
}
*taken += 1;
Some(Slot(self))
}
}
pub(super) struct Slot(&'static Slots);
impl Drop for Slot {
fn drop(&mut self) {
*self.0.taken.lock().unwrap_or_else(PoisonError::into_inner) -= 1;
self.0.freed.notify_one();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_call_waits_for_a_slot_and_gives_up_when_none_comes_free() {
static ONE: Slots = Slots::new(1);
let slow = |_| {
thread::sleep(Duration::from_millis(300));
Ok(vec![ProxyStep::Direct])
};
let quick = |_| Ok(vec![ProxyStep::Direct]);
let first = run_with_timeout(Duration::from_millis(10), &ONE, slow).unwrap_err();
assert!(matches!(first, Error::PacTimeout { .. }), "{first:?}");
let second = run_with_timeout(Duration::from_millis(10), &ONE, quick).unwrap_err();
assert!(
matches!(second, Error::PacSaturated { limit: 1, .. }),
"{second:?}"
);
assert_eq!(
run_with_timeout(Duration::from_secs(120), &ONE, quick).unwrap(),
vec![ProxyStep::Direct]
);
}
}