use std::future::Future;
use std::panic::AssertUnwindSafe;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use tokio::sync::Semaphore;
#[cfg(feature = "sqlite")]
pub const DB_MAX_CONCURRENT: usize = 2;
#[cfg(feature = "sqlite")]
pub static DB_SLOTS: Semaphore = Semaphore::const_new(DB_MAX_CONCURRENT);
#[cfg(feature = "sqlite")]
pub const BLOCKING_DEADLINE_SLACK: Duration = Duration::from_secs(5);
pub async fn run_blocking_bounded<T: Send + 'static>(
slots: Option<&'static Semaphore>,
what: &str,
deadline: Duration,
f: impl FnOnce() -> Result<T, String> + Send + 'static,
) -> Result<T, String> {
let deadline_at = tokio::time::Instant::now() + deadline;
let permit = match slots {
Some(slots) => match tokio::time::timeout_at(deadline_at, slots.acquire()).await {
Ok(Ok(permit)) => Some(permit),
Ok(Err(_closed)) => return Err(format!("{what} is unavailable")),
Err(_) => {
return Err(format!(
"{what} is busy: earlier calls are still running (a single SQLite string \
operation such as LIKE, GLOB, replace() or instr() cannot be interrupted \
and may overrun its deadline). Retry shortly."
));
}
},
None => None,
};
let task = tokio::task::spawn_blocking(move || {
let _permit = permit;
f()
});
match tokio::time::timeout_at(deadline_at, task).await {
Ok(Ok(result)) => result,
Ok(Err(e)) if e.is_panic() => Err(format!(
"{what} panicked: {}",
panic_message(&*e.into_panic())
)),
Ok(Err(e)) => Err(format!("{what} failed: {e}")),
Err(_) => Err(format!(
"{what} did not finish within {} ms (it is left to finish in the background)",
deadline.as_millis()
)),
}
}
pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
payload
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "non-string panic payload".to_string())
}
pub struct CatchUnwind<F>(Pin<Box<F>>);
impl<F: Future> CatchUnwind<F> {
pub fn new(future: F) -> Self {
Self(Box::pin(future))
}
}
impl<F: Future> Future for CatchUnwind<F> {
type Output = Result<F::Output, String>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let inner = self.0.as_mut();
match std::panic::catch_unwind(AssertUnwindSafe(|| inner.poll(cx))) {
Ok(Poll::Pending) => Poll::Pending,
Ok(Poll::Ready(value)) => Poll::Ready(Ok(value)),
Err(payload) => Poll::Ready(Err(panic_message(&*payload))),
}
}
}
pub fn ms_ago(now: chrono::DateTime<chrono::Utc>, ms: u64) -> chrono::DateTime<chrono::Utc> {
let delta = i64::try_from(ms)
.ok()
.and_then(chrono::TimeDelta::try_milliseconds)
.unwrap_or(chrono::TimeDelta::MAX);
now.checked_sub_signed(delta)
.unwrap_or(chrono::DateTime::<chrono::Utc>::MIN_UTC)
}
pub fn secs_ago(now: chrono::DateTime<chrono::Utc>, secs: u64) -> chrono::DateTime<chrono::Utc> {
ms_ago(now, secs.saturating_mul(1000))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn look_back_saturates_instead_of_panicking_or_wrapping() {
let now = chrono::Utc::now();
for ms in [u64::MAX, i64::MAX as u64 + 1, 9_000_000_000_000_000] {
assert_eq!(
ms_ago(now, ms),
chrono::DateTime::<chrono::Utc>::MIN_UTC,
"{ms}"
);
}
for secs in [u64::MAX, 10_000_000_000_000] {
assert_eq!(
secs_ago(now, secs),
chrono::DateTime::<chrono::Utc>::MIN_UTC
);
}
assert_eq!(
ms_ago(now, 1500),
now - chrono::TimeDelta::milliseconds(1500)
);
assert_eq!(secs_ago(now, 30), now - chrono::TimeDelta::seconds(30));
assert!(ms_ago(now, 0) == now);
}
#[tokio::test]
async fn catch_unwind_reports_a_panic_as_an_error() {
let r: Result<(), String> = CatchUnwind::new(async {
tokio::task::yield_now().await;
panic!("boom in handler");
})
.await;
let err = r.unwrap_err();
assert!(err.contains("boom in handler"), "{err}");
assert_eq!(CatchUnwind::new(async { 7 }).await, Ok(7));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn blocking_calls_are_deadlined_capped_and_panic_safe() {
static ONE_SLOT: Semaphore = Semaphore::const_new(1);
let started = std::time::Instant::now();
let err = run_blocking_bounded(
Some(&ONE_SLOT),
"slow op",
Duration::from_millis(200),
|| {
std::thread::sleep(Duration::from_millis(1500));
Ok(())
},
)
.await
.unwrap_err();
assert!(err.contains("did not finish within 200 ms"), "{err}");
assert!(
started.elapsed() < Duration::from_millis(1000),
"{:?}",
started.elapsed()
);
let err = run_blocking_bounded(Some(&ONE_SLOT), "db", Duration::from_millis(200), || Ok(1))
.await
.unwrap_err();
assert!(err.contains("busy"), "{err}");
assert_eq!(ONE_SLOT.available_permits(), 0);
let ok = run_blocking_bounded(Some(&ONE_SLOT), "db", Duration::from_secs(5), || Ok(2))
.await
.unwrap();
assert_eq!(ok, 2);
let err = run_blocking_bounded::<()>(None, "probe", Duration::from_secs(5), || {
panic!("probe exploded")
})
.await
.unwrap_err();
assert!(err.contains("probe panicked: probe exploded"), "{err}");
}
}