Skip to main content

diskann/error/
ranked.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! Multi-Modal Error Handling
7//!
8//! # Background
9//!
10//! Sometime, data providers can experience spurious failures during operations such as vector
11//! retrieval.
12//!
13//! A primary culprit is access to deleted vectors (if vectors are eagerly deleted rather
14//! than deferred until graph clean up), which may returns an error but is more or
15//! less expected and should not take down the DiskANN algorithm.
16//!
17//! However, not all classes of errors can be ignored in this way, such as errors indicating
18//! that DiskANN should abort processing.
19//!
20//! Furthermore, DiskANN should not be in the habit of ignoring all errors from providers as
21//! many users may want such errors to be propagated.
22//!
23//! This means we need a multi-level error handling mechanism that achieves several design
24//! goals:
25//!
26//! 1. Disambiguate between "transient" errors (those which are expected to resolve eventually
27//!    or can be ignored) and "hard" errors (those that should stop the current task and be
28//!    propagated) from user error types.
29//!
30//! 2. Require algorithmic code to explicitly acknowledge or escalate "transient" errors with
31//!    minimal probability of silently dropping an error or implicitly escalating.
32//!
33//!    In this context:
34//!
35//!    * "acknowledge": Notify the error that it has been observed and that the algorithm
36//!      is choosing to continue.
37//!
38//!    * "escalate": Notify the error that is has been observed and is being escalated by
39//!      the algorithm into a hard error.
40//!
41//! 3. Provide light-weight callback mechanisms for error acknowledgement or escalation
42//!    to enable telemetry in user types.
43//!
44//!    Practically, this means that user error types can use diagnostic logging to record
45//!    acknowledgements or escalations to assist in debugging efforts.
46//!
47//! 4. Ideally be minimal or no overhead for error types that are never transient.
48//!
49//! # How Does This Work?
50//!
51//! User error types (those implemented by the [`DataProvider`] and (eventually)
52//! [`NeighborProviderAsync`]) are expected to implement [`ToRanked`], which converts the
53//! error type to a [`RankedError`] consisting of a `Transient` and full `Error` alternatives.
54//! The [`RankedError::Transient`] alternative must implement [`TransientError`], which
55//! provides callback mechanisms for transient error acknowledgement or escalation.
56//! These callback are provided with a concrete reason for the decision to aid in debugging
57//! or program telemetry.
58//!
59//! The trait [`ErrorExt`] is then defined which takes `Result`s of such [`ToRanked`] types
60//! and provides the `acknowledge`/`escalate` interface to these `Result`s.
61//!
62//! So, how does this satisfy our goals:
63//!
64//! 1. User types explicitly mark themselves as either "transient" or "hard" when constructing
65//!    the [`RankedError`] enum through [`ToRanked`].
66//!
67//! 2. Generic algorithmic code should constrain error types to be just [`ToRanked`]. This
68//!    means that the `?` operator will not apply by default, requiring explicit handling
69//!    through either [`ErrorExt::acknowledge`] or [`ErrorExt::escalate`].
70//!
71//! 3. The methods in [`ErrorExt`] require reasons for the decision, which are forwarded
72//!    the implementation of [`TransientError`] and therefore made available to user types.
73//!
74//! Now, what about low-overhead?
75//!
76//! User types `T` that wish to **always** escalate can use the [`diskann::always_escalate`]
77//! macro. This works be defining the `Transient` alternative of [`ToRanked`] to be the
78//! unconstructable enum [`diskann::error::NeverTransient`]. This ensures at compile time
79//! that the [`RankedError::Transient`] alternative is unreachable and therefore the layout of
80//! `RankedError<NeverTransient, T>` is the same as `T` and all the transient error handling
81//! goes away.
82
83use std::fmt::{Debug, Display};
84
85use crate::{ANNError, ANNResult};
86
87pub trait TransientError<T>: Sized + std::fmt::Debug + Send + Sync {
88    /// Consume self, acknowledging the transient error but proceeding with program logic.
89    ///
90    /// This method accepts a parameter describing the reason for the acknowledgement.
91    fn acknowledge<D>(self, why: D)
92    where
93        D: Display;
94
95    /// Consume self, acknowledging the transient error but proceeding with program logic.
96    ///
97    /// The closure `why` provides a deferred method for evaluating the acknowledgement
98    /// reason and can be used by error types that do not log acknowledgements to avoid
99    /// evaluating the reason all-together.
100    ///
101    /// # Track Caller
102    ///
103    /// The provided implementation is annotated with `track_caller`.
104    #[track_caller]
105    fn acknowledge_with<F, D>(self, why: F)
106    where
107        F: FnOnce() -> D,
108        D: Display,
109    {
110        self.acknowledge(why())
111    }
112
113    /// Report to `self` that transient errors are not acceptable in the current context
114    /// and that the transient error is being upgraded to a full error.
115    ///
116    /// This method accepts a parameter describing the reason for the escalation.
117    fn escalate<D>(self, why: D) -> T
118    where
119        D: Display;
120
121    /// Report to `self` that transient errors are not acceptable in the current context
122    /// and that the transient error is being upgraded to a full error.
123    ///
124    /// The closure `why` provides a deferred method for evaluating the acknowledgement
125    /// reason and can be used by error types that do not log acknowledgements to avoid
126    /// evaluating the reason all-together.
127    ///
128    /// # Track Caller
129    ///
130    /// The provided implementation is annotated with `track_caller`.
131    #[track_caller]
132    fn escalate_with<F, D>(self, why: F) -> T
133    where
134        F: FnOnce() -> D,
135        D: Display,
136    {
137        self.escalate(why())
138    }
139}
140
141/// Allow conversion from an error type to a [`RankedError`].
142///
143/// This trait bound is applied to providers that may return transient or non-critical
144/// failures. The [`RankedError`] allows DiskANN algorithmic logic to determine whether
145/// an error can be suppressed safely or needs to be escalated.
146///
147/// * See also: [`always_escalate!`], [`ErrorExt`].
148pub trait ToRanked {
149    type Transient: TransientError<Self::Error>;
150    type Error: Into<ANNError> + std::fmt::Debug + Send + Sync;
151
152    /// Convert `self` into a `RankedError`.
153    fn to_ranked(self) -> RankedError<Self::Transient, Self::Error>;
154
155    /// Construct `Self` from its transient variant.
156    fn from_transient(transient: Self::Transient) -> Self;
157
158    /// Construct `Self` from its error variant.
159    fn from_error(error: Self::Error) -> Self;
160}
161
162/// An error type consisting of a transient (non-critical) error and an unignorable error.
163#[must_use]
164#[derive(Debug)]
165pub enum RankedError<R, E>
166where
167    R: TransientError<E>,
168{
169    /// This error can be ignored if the allowed by the caller code. Such errors should
170    /// be acknowledged (see [`TransientError::acknowledge`] before being dropped, but
171    /// aren't necessarily required to be.
172    Transient(R),
173
174    /// This error cannot be ignored and should be propagated. The caller should **never**
175    /// suppress values in this alternative.
176    Error(E),
177}
178
179impl<R, E> ToRanked for RankedError<R, E>
180where
181    R: TransientError<E>,
182    E: Into<ANNError> + std::fmt::Debug + Send + Sync,
183{
184    type Transient = R;
185    type Error = E;
186
187    fn to_ranked(self) -> Self {
188        self
189    }
190
191    fn from_transient(transient: <Self as ToRanked>::Transient) -> Self {
192        Self::Transient(transient)
193    }
194
195    fn from_error(error: <Self as ToRanked>::Error) -> Self {
196        Self::Error(error)
197    }
198}
199
200/// A zero-sized type that is unconstructable.
201///
202/// This is used as the [`TransientError`] type for error types using the
203/// [`always_escalate!`] macro to opt-out of transient error handling.
204#[derive(Debug)]
205pub enum NeverTransient {}
206
207/// Mark the type `T` as "Always Escalating". This will implement `ToRanked for $T` in a
208/// way that:
209///
210/// 1. Ensures `to_ranked` **always** returns [`RankedError::Error`].
211///
212/// 2. Defines the "transient" portion of `ToRanked` in such a way that it is impossible to
213///    instantiate. This means that the layout of [`RankedError`] is identical to the layout
214///    of `$T` and allows the methods in [`ErrorExt`] to optimize out all transient error
215///    handling.
216#[macro_export]
217macro_rules! always_escalate {
218    ($T:ty) => {
219        impl $crate::error::TransientError<$T> for $crate::error::NeverTransient {
220            fn acknowledge<D>(self, _: D)
221            where
222                D: std::fmt::Display,
223            {
224                unreachable!("NeverTransient is an unconstructable type");
225            }
226            fn acknowledge_with<F, D>(self, _: F)
227            where
228                F: FnOnce() -> D,
229                D: std::fmt::Display,
230            {
231                unreachable!("NeverTransient is an unconstructable type");
232            }
233            fn escalate<D>(self, _: D) -> $T
234            where
235                D: std::fmt::Display,
236            {
237                unreachable!("NeverTransient is an unconstructable type");
238            }
239            fn escalate_with<F, D>(self, _: F) -> $T
240            where
241                F: FnOnce() -> D,
242                D: std::fmt::Display,
243            {
244                unreachable!("NeverTransient is an unconstructable type");
245            }
246        }
247
248        impl $crate::error::ToRanked for $T {
249            type Transient = $crate::error::NeverTransient;
250            type Error = Self;
251
252            fn to_ranked(self) -> $crate::error::RankedError<Self::Transient, Self::Error> {
253                $crate::error::RankedError::Error(self)
254            }
255
256            fn from_transient(_: $crate::error::NeverTransient) -> Self {
257                unreachable!("NeverTransient is an unconstructable type");
258            }
259
260            fn from_error(error: Self) -> Self {
261                error
262            }
263        }
264    };
265}
266
267/// An infallible error type so that the compiler knows this error can
268/// never be thrown.
269#[derive(Debug, Clone, PartialEq, Eq)]
270pub enum Infallible {}
271
272impl From<Infallible> for ANNError {
273    fn from(_: Infallible) -> Self {
274        // This is unreachable but we need to implement it to keep the compiler happy when
275        // the error type for an associated type must implement `Into<ANNError>` and is
276        // `Infallible`. E.g. `VectorRepr` impl for f32.
277        unreachable!()
278    }
279}
280
281impl std::fmt::Display for Infallible {
282    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283        unreachable!()
284    }
285}
286
287impl std::error::Error for Infallible {}
288
289impl Infallible {
290    /// Match result containing `Infallible` error type.
291    ///
292    /// This function is a utility for unwrapping a `Result` that has an `Infallible` error type.
293    /// Since `Infallible` represents a type that cannot be instantiated, any `Result<T, Infallible>`
294    /// is guaranteed to be `Ok(T)`. This function safely extracts the value from such a result.
295    ///
296    /// # Arguments
297    ///
298    /// * `x` - A result with an infallible error type.
299    ///
300    /// # Returns
301    ///
302    /// The inner value `T` from the `Ok` variant.
303    ///
304    /// # Example
305    ///
306    /// ```
307    /// use diskann::error::Infallible;
308    ///
309    /// let result: Result<i32, Infallible> = Ok(42);
310    /// let value = Infallible::match_infallible(result);
311    /// assert_eq!(value, 42);
312    /// ```
313    pub fn match_infallible<T>(x: Result<T, Infallible>) -> T {
314        x.unwrap_or_else(|inf| match inf {})
315    }
316}
317
318always_escalate!(Infallible);
319
320/// Provide explicit error handling for compatible result types.
321///
322/// This trait requires the caller to differentiate between callsites that can handle
323/// transient errors and those that cannot.
324///
325/// Explicit acknowledgement or escalation of transient errors is accompanied by a reason
326/// why to assist with telementry.
327pub trait ErrorExt<T> {
328    /// Acknowledge and drop transient errors while propagating critical errors.
329    ///
330    /// * If `self` is not an error, return `Ok(Some(v))`.
331    /// * If `self` is a transient error, acknowledge it with the provided reason and return
332    ///   `Ok(None)`.
333    /// * Otherwise, return an error.
334    fn allow_transient<D>(self, why: D) -> ANNResult<Option<T>>
335    where
336        D: Display;
337
338    /// Acknowledge and drop transient errors while propagating critical errors.
339    ///
340    /// * If `self` is not an error, return `Ok(Some(v))`.
341    /// * If `self` is a transient error, acknowledge it with the provided reason and return
342    ///   `Ok(None)`.
343    /// * Otherwise, return an error.
344    fn allow_transient_with<F, D>(self, why: F) -> ANNResult<Option<T>>
345    where
346        F: FnOnce() -> D,
347        D: Display;
348
349    /// Escalate transient errors into full errors.
350    ///
351    /// * If `self` is not an error, return `Ok(v)`.
352    /// * If `self` is a transient error, escalate with the provided reason and return the
353    ///   escalated error.
354    /// * Otherwise, return the critical error.
355    fn escalate<D>(self, why: D) -> ANNResult<T>
356    where
357        D: Display;
358
359    /// Escalate transient errors into full errors.
360    ///
361    /// * If `self` is not an error, return `Ok(v)`.
362    /// * If `self` is a transient error, escalate with the provided reason and return the
363    ///   escalated error.
364    /// * Otherwise, return the critical error.
365    fn escalate_with<F, D>(self, why: F) -> ANNResult<T>
366    where
367        F: FnOnce() -> D,
368        D: Display;
369}
370
371impl<T, E> ErrorExt<T> for Result<T, E>
372where
373    E: ToRanked,
374{
375    #[track_caller]
376    fn allow_transient<D>(self, why: D) -> ANNResult<Option<T>>
377    where
378        D: Display,
379    {
380        match self {
381            Ok(v) => Ok(Some(v)),
382            Err(err) => match err.to_ranked() {
383                RankedError::Transient(transient) => {
384                    transient.acknowledge(why);
385                    Ok(None)
386                }
387                RankedError::Error(err) => Err(err.into()),
388            },
389        }
390    }
391
392    #[track_caller]
393    fn allow_transient_with<F, D>(self, why: F) -> ANNResult<Option<T>>
394    where
395        F: FnOnce() -> D,
396        D: Display,
397    {
398        match self {
399            Ok(v) => Ok(Some(v)),
400            Err(err) => match err.to_ranked() {
401                RankedError::Transient(transient) => {
402                    transient.acknowledge_with(why);
403                    Ok(None)
404                }
405                RankedError::Error(err) => Err(err.into()),
406            },
407        }
408    }
409
410    #[track_caller]
411    fn escalate<D>(self, why: D) -> ANNResult<T>
412    where
413        D: Display,
414    {
415        match self {
416            Ok(v) => Ok(v),
417            Err(err) => match err.to_ranked() {
418                RankedError::Transient(transient) => Err(transient.escalate(why).into()),
419                RankedError::Error(err) => Err(err.into()),
420            },
421        }
422    }
423
424    #[track_caller]
425    fn escalate_with<F, D>(self, why: F) -> ANNResult<T>
426    where
427        F: FnOnce() -> D,
428        D: Display,
429    {
430        match self {
431            Ok(v) => Ok(v),
432            Err(err) => match err.to_ranked() {
433                RankedError::Transient(transient) => Err(transient.escalate_with(why).into()),
434                RankedError::Error(err) => Err(err.into()),
435            },
436        }
437    }
438}
439
440///////////
441// Tests //
442///////////
443
444#[cfg(test)]
445mod tests {
446    use std::sync::Mutex;
447
448    use thiserror::Error;
449
450    use super::*;
451
452    // Check that the layout of Ranked "Always Escalate" types `T` is the same as the layout
453    // for `T`
454    #[derive(Debug, Clone, Copy, Error)]
455    #[error("generic error message: {0}")]
456    struct AlwaysEscalate(usize);
457
458    impl From<AlwaysEscalate> for ANNError {
459        fn from(value: AlwaysEscalate) -> ANNError {
460            ANNError::log_index_error(value)
461        }
462    }
463
464    always_escalate!(AlwaysEscalate);
465
466    #[test]
467    fn test_always_escalate() {
468        // The goal here is to ensure that the ranked version of a type that has opted in
469        // to `always_escalate!` has the same layout (i.e., same size) as the error type
470        // itself.
471        //
472        // The goal is to make ranking always-escalate errors
473        assert_eq!(
474            std::mem::size_of::<RankedError<NeverTransient, AlwaysEscalate>>(),
475            std::mem::size_of::<AlwaysEscalate>()
476        );
477
478        let r = AlwaysEscalate(10).to_ranked();
479        assert!(matches!(r, RankedError::Error(AlwaysEscalate(10))));
480    }
481
482    #[derive(Debug, Error)]
483    #[error(
484        "Bomb: value = {}, ack = {}, escalated = {}",
485        value,
486        acknowledged,
487        escalated
488    )]
489    struct Bomb<'a> {
490        messages: &'a Mutex<Vec<(String, u32)>>,
491        acknowledged: bool,
492        escalated: bool,
493        value: u64,
494    }
495
496    impl<'a> Bomb<'a> {
497        fn new(messages: &'a Mutex<Vec<(String, u32)>>, value: u64) -> Self {
498            Self {
499                messages,
500                acknowledged: false,
501                escalated: false,
502                value,
503            }
504        }
505    }
506
507    impl Drop for Bomb<'_> {
508        fn drop(&mut self) {
509            if !self.acknowledged && !self.escalated {
510                panic!("Bomb error was neither acknowledged nor escalated");
511            }
512            if self.acknowledged && self.escalated {
513                panic!("Bomb error was both acknowledged and escalated");
514            }
515        }
516    }
517
518    #[derive(Debug, Error)]
519    #[error("Disarmed: value = {}", value)]
520    struct Disarmed<'a> {
521        messages: &'a Mutex<Vec<(String, u32)>>,
522        value: u64,
523    }
524
525    impl<'a> Disarmed<'a> {
526        fn new(messages: &'a Mutex<Vec<(String, u32)>>, value: u64) -> Self {
527            Self { messages, value }
528        }
529    }
530
531    impl<'a> TransientError<Disarmed<'a>> for Bomb<'a> {
532        #[track_caller]
533        fn acknowledge<D>(mut self, why: D)
534        where
535            D: Display,
536        {
537            self.acknowledged = true;
538            let mut v = self.messages.lock().unwrap();
539            let location = std::panic::Location::caller();
540            v.push((format!("acknowledged: {}", why), location.line()))
541        }
542
543        #[track_caller]
544        fn escalate<D>(mut self, why: D) -> Disarmed<'a>
545        where
546            D: Display,
547        {
548            self.escalated = true;
549            let mut v = self.messages.lock().unwrap();
550            let location = std::panic::Location::caller();
551            v.push((format!("escalated: {}", why), location.line()));
552
553            Disarmed {
554                messages: self.messages,
555                value: self.value,
556            }
557        }
558    }
559
560    impl From<Disarmed<'_>> for ANNError {
561        #[track_caller]
562        fn from(value: Disarmed<'_>) -> ANNError {
563            ANNError::log_index_error(&value)
564        }
565    }
566
567    struct MaybeTransient<'a> {
568        messages: &'a Mutex<Vec<(String, u32)>>,
569        value: u64,
570        transient: bool,
571    }
572
573    impl<'a> MaybeTransient<'a> {
574        fn new(messages: &'a Mutex<Vec<(String, u32)>>, value: u64, transient: bool) -> Self {
575            Self {
576                messages,
577                value,
578                transient,
579            }
580        }
581    }
582
583    impl<'a> ToRanked for MaybeTransient<'a> {
584        type Transient = Bomb<'a>;
585        type Error = Disarmed<'a>;
586
587        fn to_ranked(self) -> RankedError<Self::Transient, Self::Error> {
588            if self.transient {
589                RankedError::Transient(Bomb::new(self.messages, self.value))
590            } else {
591                RankedError::Error(Disarmed::new(self.messages, self.value))
592            }
593        }
594
595        fn from_transient(mut transient: Bomb<'a>) -> Self {
596            transient.acknowledged = true;
597            Self::new(transient.messages, transient.value, true)
598        }
599
600        fn from_error(error: Disarmed<'a>) -> Self {
601            Self::new(error.messages, error.value, false)
602        }
603    }
604
605    #[test]
606    fn to_ranked_idempotent() {
607        let messages = Mutex::new(Vec::<(String, u32)>::new());
608
609        let v = MaybeTransient::new(&messages, 10, true).to_ranked();
610        assert!(matches!(v, RankedError::Transient(..)));
611
612        // Disarm the bomb
613        match v.to_ranked() {
614            RankedError::Transient(v) => v.acknowledge(""),
615            _ => panic!("wrong variant"),
616        }
617
618        let v = MaybeTransient::new(&messages, 10, false).to_ranked();
619        assert!(matches!(v, RankedError::Error(..)));
620        let v = v.to_ranked();
621        assert!(matches!(v, RankedError::Error(..)));
622    }
623
624    #[test]
625    fn error_ext_allow_transient_ok() {
626        let messages = Mutex::new(Vec::<(String, u32)>::new());
627
628        // Non-Error Path.
629        let v: usize = Result::<usize, MaybeTransient<'_>>::Ok(10)
630            .allow_transient("hello")
631            .unwrap()
632            .unwrap();
633        assert_eq!(v, 10);
634        assert!(messages.lock().unwrap().is_empty());
635    }
636
637    #[test]
638    fn error_ext_allow_transient_with_ok() {
639        let messages = Mutex::new(Vec::<(String, u32)>::new());
640
641        // Non-Error Path.
642        let v: usize = Result::<usize, MaybeTransient<'_>>::Ok(10)
643            .allow_transient_with(|| -> &str {
644                panic!("this should not be called!");
645            })
646            .unwrap()
647            .unwrap();
648        assert_eq!(v, 10);
649        assert!(messages.lock().unwrap().is_empty());
650    }
651
652    #[test]
653    fn error_ext_escalate_ok() {
654        let messages = Mutex::new(Vec::<(String, u32)>::new());
655
656        // Non-Error Path.
657        let v: usize = Result::<usize, MaybeTransient<'_>>::Ok(10)
658            .escalate("hello")
659            .unwrap();
660        assert_eq!(v, 10);
661        assert!(messages.lock().unwrap().is_empty());
662    }
663
664    #[test]
665    fn error_ext_escalate_with_ok() {
666        let messages = Mutex::new(Vec::<(String, u32)>::new());
667
668        // Non-Error Path.
669        let v: usize = Result::<usize, MaybeTransient<'_>>::Ok(10)
670            .escalate_with(|| -> &str {
671                panic!("this should not be called");
672            })
673            .unwrap();
674
675        assert_eq!(v, 10);
676        assert!(messages.lock().unwrap().is_empty());
677    }
678
679    #[test]
680    fn error_ext_allow_transient_transient() {
681        let messages = Mutex::new(Vec::<(String, u32)>::new());
682
683        // Error path - transient
684        let why = "foo";
685        let line = line!();
686        assert!(
687            Result::<usize, MaybeTransient<'_>>::Err(MaybeTransient::new(&messages, 10, true))
688                .allow_transient(why)
689                .unwrap()
690                .is_none()
691        );
692
693        let m = messages.lock().unwrap();
694        assert_eq!(m.len(), 1);
695        assert_eq!(m[0].1, line + 3);
696        assert_eq!(m[0].0, format!("acknowledged: {}", why));
697    }
698
699    #[test]
700    fn error_ext_allow_transient_with_transient() {
701        let messages = Mutex::new(Vec::<(String, u32)>::new());
702
703        // Error path - transient
704        let why = "foo";
705        let mut called: bool = false;
706        let line = line!();
707        assert!(
708            Result::<usize, MaybeTransient<'_>>::Err(MaybeTransient::new(&messages, 10, true))
709                .allow_transient_with(|| {
710                    called = true;
711                    why
712                })
713                .unwrap()
714                .is_none()
715        );
716
717        assert!(called);
718        let m = messages.lock().unwrap();
719        assert_eq!(m.len(), 1);
720        assert_eq!(m[0].1, line + 3);
721        assert_eq!(m[0].0, format!("acknowledged: {}", why));
722    }
723
724    #[test]
725    fn error_ext_escalate_transient() {
726        let messages = Mutex::new(Vec::<(String, u32)>::new());
727
728        // Error path - transient
729        let why = "foo";
730        let line = line!();
731        assert!(
732            Result::<usize, MaybeTransient<'_>>::Err(MaybeTransient::new(&messages, 10, true))
733                .escalate(why)
734                .is_err()
735        );
736
737        let m = messages.lock().unwrap();
738        assert_eq!(m.len(), 1);
739        assert_eq!(m[0].1, line + 3);
740        assert_eq!(m[0].0, format!("escalated: {}", why));
741    }
742
743    #[test]
744    fn error_ext_escalate_with_transient() {
745        let messages = Mutex::new(Vec::<(String, u32)>::new());
746
747        // Error path - transient
748        let why = "foo";
749        let mut called: bool = false;
750        let line = line!();
751        assert!(
752            Result::<usize, MaybeTransient<'_>>::Err(MaybeTransient::new(&messages, 10, true))
753                .escalate_with(|| {
754                    called = true;
755                    why
756                })
757                .is_err()
758        );
759
760        assert!(called);
761        let m = messages.lock().unwrap();
762        assert_eq!(m.len(), 1);
763        assert_eq!(m[0].1, line + 3);
764        assert_eq!(m[0].0, format!("escalated: {}", why));
765    }
766
767    #[test]
768    fn error_ext_allow_transient_error() {
769        let messages = Mutex::new(Vec::<(String, u32)>::new());
770
771        // Error path - error
772        let why = "foo";
773        assert!(
774            Result::<usize, MaybeTransient<'_>>::Err(MaybeTransient::new(&messages, 10, false))
775                .allow_transient(why)
776                .is_err()
777        );
778
779        assert!(messages.lock().unwrap().is_empty());
780    }
781
782    #[test]
783    fn error_ext_allow_transient_with_error() {
784        let messages = Mutex::new(Vec::<(String, u32)>::new());
785
786        // Error path - error
787        assert!(
788            Result::<usize, MaybeTransient<'_>>::Err(MaybeTransient::new(&messages, 10, false))
789                .allow_transient_with(|| -> &str {
790                    panic!("should not be called");
791                })
792                .is_err()
793        );
794
795        assert!(messages.lock().unwrap().is_empty());
796    }
797
798    #[test]
799    fn error_ext_escalate_error() {
800        let messages = Mutex::new(Vec::<(String, u32)>::new());
801
802        // Error path - error
803        let why = "foo";
804        assert!(
805            Result::<usize, MaybeTransient<'_>>::Err(MaybeTransient::new(&messages, 10, false))
806                .escalate(why)
807                .is_err()
808        );
809
810        assert!(messages.lock().unwrap().is_empty());
811    }
812
813    #[test]
814    fn error_ext_escalate_with_error() {
815        let messages = Mutex::new(Vec::<(String, u32)>::new());
816
817        // Error path - error
818        assert!(
819            Result::<usize, MaybeTransient<'_>>::Err(MaybeTransient::new(&messages, 10, false))
820                .escalate_with(|| -> &str {
821                    panic!("should not be called");
822                })
823                .is_err()
824        );
825
826        assert!(messages.lock().unwrap().is_empty());
827    }
828
829    #[test]
830    fn test_infallible() {
831        // Test that match_infallible can extract values from Ok results
832        let result: Result<i32, Infallible> = Ok(42);
833        let value = Infallible::match_infallible(result);
834        assert_eq!(value, 42);
835
836        // Test with different types
837        let result: Result<String, Infallible> = Ok("hello".to_string());
838        let value = Infallible::match_infallible(result);
839        assert_eq!(value, "hello");
840
841        // Test that Infallible converts to ANNError (though this should never happen in practice)
842        // We can't actually construct an Infallible value to test this directly since it's unconstructable
843        // But we can verify the From implementation exists by checking the type constraint
844        fn _test_infallible_into_ann_error(_: Infallible) -> ANNError {
845            ANNError::log_index_error("This should never be called")
846        }
847    }
848}