es_entity/operation/batch/transient.rs
1//! Classifying a probe failure as contention.
2
3/// Which probe failures are contention — not attributable to the range's
4/// contents — and how many re-probes they may buy.
5///
6/// The default classification is [`is_contention`]: a deadlock victim or a
7/// serialization failure, recognised anywhere in the error's source chain.
8/// Override it for error types of your own that describe the same thing: a
9/// failure that says nothing about the items, only about the interleaving.
10///
11/// Other transients are left to the search on purpose. An
12/// optimistic-concurrency conflict names one stale item, so splitting
13/// isolates it where re-probing the same range would only conflict again; a
14/// lost connection cannot be re-probed at all, and surfaces as the helper's
15/// outer `Err` on the next savepoint instead.
16#[derive(Debug, Clone, Copy)]
17pub struct TransientPolicy<P> {
18 /// Returns `true` when a probe failure carries no information about the
19 /// range's contents.
20 pub is_transient: P,
21 /// How many transient re-probes the whole search may take before it is
22 /// abandoned.
23 pub max_retries: usize,
24}
25
26impl<P> TransientPolicy<P> {
27 /// A policy with the default retry allowance.
28 pub fn new(is_transient: P) -> Self {
29 Self {
30 is_transient,
31 max_retries: super::DEFAULT_MAX_TRANSIENT_RETRIES,
32 }
33 }
34
35 /// Overrides the retry allowance.
36 #[must_use]
37 pub fn with_max_retries(self, max_retries: usize) -> Self {
38 Self {
39 max_retries,
40 ..self
41 }
42 }
43}
44
45/// The default classifier, as a plain function so it can be named in a
46/// [`TransientPolicy`] without boxing: [`Fault::classify`] over the whole
47/// [`source`](std::error::Error::source) chain, then
48/// [`Fault::is_contention`] — so a deadlock (`40P01`) or a serialization
49/// failure (`40001`) is recognised whether it arrives as a lane payload a
50/// repo op already classified or as a raw [`sqlx::Error`] several layers
51/// deep inside a caller's own error type.
52///
53/// [`Fault::classify`]: crate::errlanes::Fault::classify
54/// [`Fault::is_contention`]: crate::errlanes::Fault::is_contention
55pub(super) fn is_contention<E: std::error::Error + 'static>(error: &E) -> bool {
56 crate::errlanes::Fault::classify(error).is_contention()
57}
58
59#[cfg(test)]
60mod tests {
61 use super::is_contention;
62 use crate::errlanes::{Transient, TransientKind};
63
64 #[derive(Debug, thiserror::Error)]
65 #[error("wrapped")]
66 struct Wrapped(#[source] Transient);
67
68 #[test]
69 fn contention_is_found_through_a_callers_wrapper() {
70 assert!(is_contention(&Wrapped(Transient::new(
71 TransientKind::Deadlock
72 ))));
73 assert!(is_contention(&Wrapped(Transient::new(
74 TransientKind::SerializationFailure
75 ))));
76 }
77
78 #[test]
79 fn other_transients_are_left_to_the_search() {
80 assert!(!is_contention(&Wrapped(Transient::new(
81 TransientKind::OptimisticConflict
82 ))));
83 assert!(!is_contention(&Wrapped(Transient::new(
84 TransientKind::ConnectionLost
85 ))));
86 assert!(!is_contention(&std::io::Error::other(
87 "not transient at all"
88 )));
89 }
90}