1use crate::sqlx;
23
24use std::future::Future;
25
26use cratestack_core::{CratestackError, TransactionIsolation};
27
28use crate::error::cratestack_error_from_sqlx;
29
30pub(crate) const MAX_RETRIES_DEFAULT: u32 = 3;
31
32pub async fn run_in_isolated_tx<F, Fut, T>(
45 pool: &sqlx::PgPool,
46 isolation: TransactionIsolation,
47 body: F,
48) -> Result<T, CratestackError>
49where
50 F: FnMut(sqlx::Transaction<'static, sqlx::Postgres>) -> Fut,
51 Fut: Future<Output = Result<(T, sqlx::Transaction<'static, sqlx::Postgres>), CratestackError>>,
52{
53 run_in_isolated_tx_with_retries(pool, isolation, MAX_RETRIES_DEFAULT, body).await
54}
55
56pub async fn run_in_isolated_tx_with_retries<F, Fut, T>(
60 pool: &sqlx::PgPool,
61 isolation: TransactionIsolation,
62 max_retries: u32,
63 mut body: F,
64) -> Result<T, CratestackError>
65where
66 F: FnMut(sqlx::Transaction<'static, sqlx::Postgres>) -> Fut,
67 Fut: Future<Output = Result<(T, sqlx::Transaction<'static, sqlx::Postgres>), CratestackError>>,
68{
69 let mut attempts = 0u32;
70 loop {
71 attempts += 1;
72 let mut tx = pool.begin().await.map_err(cratestack_error_from_sqlx)?;
73 let set_stmt = format!("SET TRANSACTION ISOLATION LEVEL {}", isolation.as_sql());
74 sqlx::query(sqlx::AssertSqlSafe(set_stmt))
78 .execute(&mut *tx)
79 .await
80 .map_err(cratestack_error_from_sqlx)?;
81
82 match body(tx).await {
83 Ok((value, tx)) => match tx.commit().await {
84 Ok(()) => return Ok(value),
85 Err(commit_error) => {
86 let promoted = cratestack_error_from_sqlx(commit_error);
96 if attempts <= max_retries && is_retriable(&promoted) {
97 tokio::task::yield_now().await;
98 continue;
99 }
100 return Err(promoted);
101 }
102 },
103 Err(error) => {
104 if attempts <= max_retries && is_retriable(&error) {
105 tokio::task::yield_now().await;
110 continue;
111 }
112 return Err(error);
113 }
114 }
115 }
116}
117
118fn is_retriable(error: &CratestackError) -> bool {
128 crate::retriable::retriable_sqlstate(error).is_some()
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn parses_all_isolation_levels() {
137 assert_eq!(
138 TransactionIsolation::parse("serializable").unwrap(),
139 TransactionIsolation::Serializable,
140 );
141 assert_eq!(
142 TransactionIsolation::parse("Repeatable_Read").unwrap(),
143 TransactionIsolation::RepeatableRead,
144 );
145 assert_eq!(
146 TransactionIsolation::parse("read committed").unwrap(),
147 TransactionIsolation::ReadCommitted,
148 );
149 assert!(TransactionIsolation::parse("snapshot").is_err());
150 }
151
152 #[test]
153 fn sql_strings_match_pg_grammar() {
154 assert_eq!(TransactionIsolation::Serializable.as_sql(), "SERIALIZABLE");
155 assert_eq!(
156 TransactionIsolation::RepeatableRead.as_sql(),
157 "REPEATABLE READ",
158 );
159 assert_eq!(
160 TransactionIsolation::ReadCommitted.as_sql(),
161 "READ COMMITTED",
162 );
163 }
164
165 #[test]
166 fn retriable_on_serialization_failure_sqlstate() {
167 let err = CratestackError::Database(
168 "Database(PgDatabaseError { severity: ERROR, code: \"40001\", \
169 message: \"could not serialize access due to concurrent update\" })"
170 .to_owned(),
171 );
172 assert!(is_retriable(&err));
173 }
174
175 #[test]
176 fn retriable_on_deadlock_sqlstate() {
177 let err = CratestackError::Database(
178 "Database(PgDatabaseError { code: \"40P01\", \
179 message: \"deadlock detected\" })"
180 .to_owned(),
181 );
182 assert!(is_retriable(&err));
183 }
184
185 #[test]
186 fn not_retriable_on_an_application_error_that_mentions_40001() {
187 for err in [
188 CratestackError::Validation("field 'memo' length 40001 exceeds maximum 100".to_owned()),
189 CratestackError::Internal("deadlock detected in my own lock manager".to_owned()),
190 ] {
191 assert!(!is_retriable(&err), "{err:?}");
192 }
193 }
194
195 #[test]
196 fn not_retriable_on_unique_violation() {
197 let err = CratestackError::Database(
198 "duplicate key value violates unique constraint \"accounts_pkey\"".to_owned(),
199 );
200 assert!(!is_retriable(&err));
201 }
202
203 #[test]
204 fn retriable_when_serialization_failure_is_raised_at_commit_time() {
205 let err = CratestackError::Database(
211 "Database(PgDatabaseError { severity: ERROR, code: \"40001\", \
212 message: \"could not serialize access due to read/write dependencies among transactions\" })"
213 .to_owned(),
214 );
215 assert!(is_retriable(&err));
216 }
217
218 #[test]
221 fn retriable_typed_serialization_failure() {
222 use cratestack_core::DbErrorInfo;
223 let err = CratestackError::DatabaseTyped(DbErrorInfo {
224 detail: "could not serialize access due to concurrent update".to_owned(),
225 sqlstate: Some("40001".to_owned()),
226 constraint: None,
227 });
228 assert!(
229 is_retriable(&err),
230 "DatabaseTyped with 40001 sqlstate must be retriable via the fast path",
231 );
232 }
233
234 #[test]
235 fn retriable_typed_deadlock() {
236 use cratestack_core::DbErrorInfo;
237 let err = CratestackError::DatabaseTyped(DbErrorInfo {
238 detail: "deadlock detected".to_owned(),
239 sqlstate: Some("40P01".to_owned()),
240 constraint: None,
241 });
242 assert!(
243 is_retriable(&err),
244 "DatabaseTyped with 40P01 sqlstate must be retriable via the fast path",
245 );
246 }
247
248 #[test]
249 fn not_retriable_typed_unique_violation() {
250 use cratestack_core::DbErrorInfo;
251 let err = CratestackError::DatabaseTyped(DbErrorInfo {
252 detail: "duplicate key value violates unique constraint \"accounts_pkey\"".to_owned(),
253 sqlstate: Some("23505".to_owned()),
254 constraint: Some("accounts_pkey".to_owned()),
255 });
256 assert!(
257 !is_retriable(&err),
258 "unique_violation (23505) must not be retried",
259 );
260 }
261
262 #[test]
263 fn typed_variant_sqlstate_is_authoritative_over_its_detail() {
264 use cratestack_core::DbErrorInfo;
272 for (sqlstate, detail) in [
273 (
274 "XX999",
275 "could not serialize access due to read/write dependencies",
276 ),
277 ("P0001", "insufficient funds: requested 40001"),
278 ("22P02", "invalid input syntax for type bigint: \"40001x\""),
279 ("55P03", "deadlock detected while waiting for 40P01"),
280 ] {
281 let err = CratestackError::DatabaseTyped(DbErrorInfo {
282 detail: detail.to_owned(),
283 sqlstate: Some(sqlstate.to_owned()),
284 constraint: None,
285 });
286 assert!(
287 !is_retriable(&err),
288 "typed {sqlstate} must not be retried because of its text",
289 );
290 }
291 }
292
293 #[test]
294 fn typed_variant_exposes_constraint_for_unique_violation() {
295 use cratestack_core::DbErrorInfo;
296 let err = CratestackError::DatabaseTyped(DbErrorInfo {
297 detail: "duplicate key value violates unique constraint \"wallets_owner_key\""
298 .to_owned(),
299 sqlstate: Some("23505".to_owned()),
300 constraint: Some("wallets_owner_key".to_owned()),
301 });
302 assert_eq!(err.db_sqlstate(), Some("23505"));
303 assert_eq!(err.db_constraint(), Some("wallets_owner_key"));
304 assert_eq!(err.public_message(), "internal error");
306 }
307}