persistant_sqlx/
bounded.rs1use persistant_relational::{
19 Dialect, Isolation, RelError, RelationalBackend, RelationalTx, Row, Value,
20};
21use std::future::Future;
22use std::pin::Pin;
23use std::time::Duration;
24
25#[derive(Debug, Clone)]
27pub struct Bounded<B> {
28 inner: B,
29 bound: Duration,
30}
31
32impl<B> Bounded<B> {
33 pub fn new(inner: B, bound: Duration) -> Self {
35 Bounded { inner, bound }
36 }
37
38 pub fn inner(&self) -> &B {
40 &self.inner
41 }
42
43 pub fn bound(&self) -> Duration {
45 self.bound
46 }
47}
48
49pub fn call_bound_from_env(var: &str) -> Option<Duration> {
52 parse_bound(&std::env::var(var).ok()?)
53}
54
55pub fn parse_bound(text: &str) -> Option<Duration> {
57 let ms: u64 = text.trim().parse().ok()?;
58 (ms > 0).then(|| Duration::from_millis(ms))
59}
60
61async fn within<T>(
62 bound: Duration,
63 call: impl Future<Output = Result<T, RelError>>,
64) -> Result<T, RelError> {
65 match tokio::time::timeout(bound, call).await {
66 Ok(r) => r,
67 Err(_) => Err(RelError::Timeout { after: bound }),
68 }
69}
70
71impl<B: RelationalBackend> RelationalBackend for Bounded<B> {
72 type Tx<'a>
73 = BoundedTx<B::Tx<'a>>
74 where
75 Self: 'a;
76
77 fn backend(&self) -> &str {
78 self.inner.backend()
79 }
80
81 fn version(&self) -> &str {
82 self.inner.version()
83 }
84
85 fn dialect(&self) -> Dialect {
86 self.inner.dialect()
87 }
88
89 async fn execute(&self, sql: &str, params: &[Value]) -> Result<u64, RelError> {
90 within(self.bound, self.inner.execute(sql, params)).await
91 }
92
93 async fn query(&self, sql: &str, params: &[Value]) -> Result<Vec<Row>, RelError> {
94 within(self.bound, self.inner.query(sql, params)).await
95 }
96
97 async fn begin(&self, isolation: Isolation) -> Result<Self::Tx<'_>, RelError> {
98 let tx = within(self.bound, self.inner.begin(isolation)).await?;
99 Ok(BoundedTx {
100 inner: tx,
101 bound: self.bound,
102 poisoned: false,
103 })
104 }
105
106 fn call_bound(&self) -> Option<Duration> {
107 Some(self.bound)
108 }
109
110 async fn with_lock_held<'a>(
111 &'a self,
112 lock_sql: &'a str,
113 at_most: Duration,
114 during: Pin<Box<dyn Future<Output = ()> + Send + 'a>>,
115 ) -> Result<(), RelError> {
116 let mut tx = self.begin(Isolation::ReadCommitted).await?;
117 RelationalTx::query(&mut tx, lock_sql, &[]).await?;
119 let _ = tokio::time::timeout(at_most, during).await;
122 RelationalTx::rollback(tx).await
123 }
124}
125
126#[derive(Debug)]
130pub struct BoundedTx<T> {
131 inner: T,
132 bound: Duration,
133 poisoned: bool,
134}
135
136impl<T> BoundedTx<T> {
137 fn refuse(&self) -> RelError {
138 RelError::Backend(
139 "transaction poisoned by an earlier timeout; it can only be rolled back".to_string(),
140 )
141 }
142}
143
144impl<T: RelationalTx> RelationalTx for BoundedTx<T> {
145 async fn execute(&mut self, sql: &str, params: &[Value]) -> Result<u64, RelError> {
146 if self.poisoned {
147 return Err(self.refuse());
148 }
149 let r = within(self.bound, self.inner.execute(sql, params)).await;
150 self.poisoned |= matches!(r, Err(RelError::Timeout { .. }));
151 r
152 }
153
154 async fn query(&mut self, sql: &str, params: &[Value]) -> Result<Vec<Row>, RelError> {
155 if self.poisoned {
156 return Err(self.refuse());
157 }
158 let r = within(self.bound, self.inner.query(sql, params)).await;
159 self.poisoned |= matches!(r, Err(RelError::Timeout { .. }));
160 r
161 }
162
163 async fn commit(self) -> Result<(), RelError> {
164 if self.poisoned {
165 return Err(self.refuse());
166 }
167 within(self.bound, self.inner.commit()).await
168 }
169
170 async fn rollback(self) -> Result<(), RelError> {
171 within(self.bound, self.inner.rollback()).await
172 }
173}