Skip to main content

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}