use persistant_relational::{
Dialect, Isolation, RelError, RelationalBackend, RelationalTx, Row, Value,
};
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct Bounded<B> {
inner: B,
bound: Duration,
}
impl<B> Bounded<B> {
pub fn new(inner: B, bound: Duration) -> Self {
Bounded { inner, bound }
}
pub fn inner(&self) -> &B {
&self.inner
}
pub fn bound(&self) -> Duration {
self.bound
}
}
pub fn call_bound_from_env(var: &str) -> Option<Duration> {
parse_bound(&std::env::var(var).ok()?)
}
pub fn parse_bound(text: &str) -> Option<Duration> {
let ms: u64 = text.trim().parse().ok()?;
(ms > 0).then(|| Duration::from_millis(ms))
}
async fn within<T>(
bound: Duration,
call: impl Future<Output = Result<T, RelError>>,
) -> Result<T, RelError> {
match tokio::time::timeout(bound, call).await {
Ok(r) => r,
Err(_) => Err(RelError::Timeout { after: bound }),
}
}
impl<B: RelationalBackend> RelationalBackend for Bounded<B> {
type Tx<'a>
= BoundedTx<B::Tx<'a>>
where
Self: 'a;
fn backend(&self) -> &str {
self.inner.backend()
}
fn version(&self) -> &str {
self.inner.version()
}
fn dialect(&self) -> Dialect {
self.inner.dialect()
}
async fn execute(&self, sql: &str, params: &[Value]) -> Result<u64, RelError> {
within(self.bound, self.inner.execute(sql, params)).await
}
async fn query(&self, sql: &str, params: &[Value]) -> Result<Vec<Row>, RelError> {
within(self.bound, self.inner.query(sql, params)).await
}
async fn begin(&self, isolation: Isolation) -> Result<Self::Tx<'_>, RelError> {
let tx = within(self.bound, self.inner.begin(isolation)).await?;
Ok(BoundedTx {
inner: tx,
bound: self.bound,
poisoned: false,
})
}
fn call_bound(&self) -> Option<Duration> {
Some(self.bound)
}
async fn with_lock_held<'a>(
&'a self,
lock_sql: &'a str,
at_most: Duration,
during: Pin<Box<dyn Future<Output = ()> + Send + 'a>>,
) -> Result<(), RelError> {
let mut tx = self.begin(Isolation::ReadCommitted).await?;
RelationalTx::query(&mut tx, lock_sql, &[]).await?;
let _ = tokio::time::timeout(at_most, during).await;
RelationalTx::rollback(tx).await
}
}
#[derive(Debug)]
pub struct BoundedTx<T> {
inner: T,
bound: Duration,
poisoned: bool,
}
impl<T> BoundedTx<T> {
fn refuse(&self) -> RelError {
RelError::Backend(
"transaction poisoned by an earlier timeout; it can only be rolled back".to_string(),
)
}
}
impl<T: RelationalTx> RelationalTx for BoundedTx<T> {
async fn execute(&mut self, sql: &str, params: &[Value]) -> Result<u64, RelError> {
if self.poisoned {
return Err(self.refuse());
}
let r = within(self.bound, self.inner.execute(sql, params)).await;
self.poisoned |= matches!(r, Err(RelError::Timeout { .. }));
r
}
async fn query(&mut self, sql: &str, params: &[Value]) -> Result<Vec<Row>, RelError> {
if self.poisoned {
return Err(self.refuse());
}
let r = within(self.bound, self.inner.query(sql, params)).await;
self.poisoned |= matches!(r, Err(RelError::Timeout { .. }));
r
}
async fn commit(self) -> Result<(), RelError> {
if self.poisoned {
return Err(self.refuse());
}
within(self.bound, self.inner.commit()).await
}
async fn rollback(self) -> Result<(), RelError> {
within(self.bound, self.inner.rollback()).await
}
}