Skip to main content

detcore/
finalization.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Consuming finalization preserves the execution's primary error.
10
11use std::fmt;
12
13/// A typed replay cursor refused its terminal state. This marker is constructed
14/// only at terminal cursor validation, not for recording I/O or summary errors.
15#[derive(Debug)]
16pub struct TerminalReplayRefusal(pub(crate) detcore_model::schedule::boundary::Error);
17
18impl fmt::Display for TerminalReplayRefusal {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        write!(f, "terminal replay refusal: {}", self.0)
21    }
22}
23
24impl std::error::Error for TerminalReplayRefusal {
25    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
26        Some(&self.0)
27    }
28}
29
30/// Priority-history replay of a callback-annotated record ended with recorded
31/// checkpoints unconsumed (`Config::annotated_priority_replay`). Constructed
32/// only by the scheduler's terminal summary, after the record it re-records
33/// is flushed; the run ends as a policy refusal.
34#[derive(Debug)]
35pub struct PriorityHistoryRefusal(pub(crate) String);
36
37impl fmt::Display for PriorityHistoryRefusal {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        write!(f, "terminal replay refusal: {}", self.0)
40    }
41}
42
43impl std::error::Error for PriorityHistoryRefusal {}
44
45/// Accepted faithful replay findings or a refused finding publication. These
46/// remain distinct: a refused payload contributes no accepted finding or marker.
47/// Constructed only after recording and summary finalization are attempted.
48#[derive(Clone, Debug, Eq, PartialEq)]
49pub struct ReplayDepartureError {
50    pub(crate) summary: Option<detcore_model::replay_departure::Summary>,
51    pub(crate) publication_refusal: Option<(
52        crate::types::DetTid,
53        crate::tool_global::replay_departure::Refusal,
54    )>,
55}
56
57impl ReplayDepartureError {
58    /// The bounded observations retained by the coordinator.
59    pub fn summary(&self) -> Option<&detcore_model::replay_departure::Summary> {
60        self.summary.as_ref()
61    }
62
63    /// Find the authoritative departure cause, including Reverie's transparent
64    /// Tool wrapper. Secondary finalization observations never reclassify the
65    /// operation's primary failure.
66    pub fn in_error(error: &anyhow::Error) -> Option<&Self> {
67        if let Some(pair) = error.downcast_ref::<FinalizationFailure>() {
68            return Self::in_error(pair.primary());
69        }
70        if let Some(departure) = error.downcast_ref::<Self>() {
71            return Some(departure);
72        }
73        error
74            .chain()
75            .find_map(|cause| match cause.downcast_ref::<reverie::Error>() {
76                Some(reverie::Error::Tool(inner)) => Self::in_error(inner),
77                _ => None,
78            })
79    }
80}
81
82impl fmt::Display for ReplayDepartureError {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        f.write_str("HERMIT_REPLAY_DEPARTURE: ")?;
85        if let Some(summary) = &self.summary {
86            let first = &summary.first;
87            write!(
88                f,
89                "{} nonadvancing change-point detection(s); first {:?} in thread {} at {}, preceding {}; ",
90                summary.count, first.source, first.thread, first.observed, first.previous
91            )?;
92        }
93        if let Some((thread, reason)) = &self.publication_refusal {
94            write!(
95                f,
96                "finding publication from thread {thread} refused ({reason:?}); "
97            )?;
98        }
99        f.write_str("refusing faithful replay completion")
100    }
101}
102
103impl std::error::Error for ReplayDepartureError {}
104
105/// An earlier error plus a later finalization failure. Only the primary is in
106/// the cause chain: a secondary policy refusal must not reclassify an I/O or
107/// backend failure. Both full diagnostics remain available through Display.
108#[derive(Debug)]
109pub struct FinalizationFailure {
110    primary: anyhow::Error,
111    secondary: anyhow::Error,
112}
113
114impl FinalizationFailure {
115    /// The original error, before finalization was attempted.
116    pub fn primary(&self) -> &anyhow::Error {
117        &self.primary
118    }
119
120    /// The additional failure encountered while releasing the original owner.
121    pub fn secondary(&self) -> &anyhow::Error {
122        &self.secondary
123    }
124}
125
126impl fmt::Display for FinalizationFailure {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        write!(
129            f,
130            "{}; additionally, finalization failed: {:#}",
131            self.primary, self.secondary
132        )
133    }
134}
135
136impl std::error::Error for FinalizationFailure {
137    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
138        Some(self.primary.as_ref())
139    }
140}
141
142/// Combine an already evaluated operation with its already evaluated cleanup.
143/// Cleanup is never skipped, and failure of either operation prevents success.
144pub fn combine_finalization<T>(
145    primary: anyhow::Result<T>,
146    finalization: anyhow::Result<()>,
147) -> anyhow::Result<T> {
148    match (primary, finalization) {
149        (Ok(value), Ok(())) => Ok(value),
150        (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error),
151        (Err(primary), Err(secondary)) => Err(FinalizationFailure { primary, secondary }.into()),
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[derive(Debug)]
160    struct Primary;
161    impl fmt::Display for Primary {
162        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163            f.write_str("primary identity")
164        }
165    }
166    impl std::error::Error for Primary {}
167
168    #[derive(Debug)]
169    struct Secondary;
170    impl fmt::Display for Secondary {
171        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172            f.write_str("secondary identity")
173        }
174    }
175    impl std::error::Error for Secondary {}
176
177    #[test]
178    fn both_failures_remain_owned_but_only_primary_classifies() {
179        let error =
180            combine_finalization::<()>(Err(Primary.into()), Err(Secondary.into())).unwrap_err();
181        let pair = error.downcast_ref::<FinalizationFailure>().unwrap();
182        assert!(pair.primary().is::<Primary>());
183        assert!(pair.secondary().is::<Secondary>());
184        assert!(error.chain().any(|cause| cause.is::<Primary>()));
185        assert!(!error.chain().any(|cause| cause.is::<Secondary>()));
186        assert!(format!("{error:#}").contains("secondary identity"));
187    }
188
189    #[test]
190    fn either_failure_refuses_and_success_keeps_value() {
191        assert_eq!(combine_finalization(Ok(7), Ok(())).unwrap(), 7);
192        assert!(
193            combine_finalization::<()>(Err(Primary.into()), Ok(()))
194                .unwrap_err()
195                .is::<Primary>()
196        );
197        assert!(
198            combine_finalization(Ok(7), Err(Secondary.into()))
199                .unwrap_err()
200                .is::<Secondary>()
201        );
202    }
203}