Skip to main content

rocketmq_runtime/
critical.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Durable failure records for tasks a component registered as critical.
16//!
17//! The record is the authority. It is written when the failure is observed and
18//! stays until a handler takes it, so a full notification channel or a subscriber
19//! that stopped reading cannot lose the fact that a critical task failed. The
20//! monitor that handles records runs under an owner outside the group it
21//! monitors, because a poisoned group can neither spawn nor run its own monitor.
22
23use std::sync::atomic::AtomicU64;
24use std::sync::atomic::Ordering;
25use std::sync::Arc;
26use std::time::Instant;
27
28use parking_lot::Mutex;
29use tokio::sync::mpsc;
30use tokio::sync::Notify;
31
32use crate::error::RuntimeResult;
33use crate::task_group::TaskId;
34use crate::task_group::TaskKind;
35use crate::task_spawner::TaskSpawner;
36use crate::RuntimeContractPolicy;
37use crate::RuntimeContractViolation;
38
39/// How a critical task stopped being healthy.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum CriticalFailureKind {
42    /// The task panicked.
43    Panicked,
44    /// A service returned before its owner started shutting down.
45    ExitedUnexpectedly,
46    /// The task reported an unrecoverable error through the state's record API.
47    Unrecoverable,
48}
49
50impl CriticalFailureKind {
51    /// Returns the stable identifier used in bounded log fields.
52    #[must_use]
53    pub const fn as_str(self) -> &'static str {
54        match self {
55            Self::Panicked => "panicked",
56            Self::ExitedUnexpectedly => "exited-unexpectedly",
57            Self::Unrecoverable => "unrecoverable",
58        }
59    }
60}
61
62/// A durable record of one critical task failure.
63///
64/// Deliberately bounded: it carries the failure kind, the task category, and a
65/// sequence, and never the task's name, group name, or arguments.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct CriticalFailure {
68    kind: CriticalFailureKind,
69    task_kind: TaskKind,
70    sequence: u64,
71    observed_at: Instant,
72}
73
74impl CriticalFailure {
75    /// Returns the bounded failure kind.
76    #[must_use]
77    pub const fn kind(self) -> CriticalFailureKind {
78        self.kind
79    }
80
81    /// Returns the bounded category of the failed task.
82    #[must_use]
83    pub const fn task_kind(self) -> TaskKind {
84        self.task_kind
85    }
86
87    /// Returns the process-lifetime sequence of this failure.
88    #[must_use]
89    pub const fn sequence(self) -> u64 {
90        self.sequence
91    }
92
93    /// Returns the instant at which the failure was observed.
94    #[must_use]
95    pub const fn observed_at(self) -> Instant {
96        self.observed_at
97    }
98}
99
100/// The registration data a critical spawn carries into task execution.
101#[derive(Debug, Clone)]
102pub(crate) struct CriticalRegistration {
103    pub(crate) failures: CriticalFailureState,
104    pub(crate) task_kind: TaskKind,
105    /// Whether returning before owner cancellation is itself a failure.
106    pub(crate) expects_until_cancelled: bool,
107}
108
109#[derive(Debug)]
110struct CriticalFailureStateInner {
111    pending: Mutex<Option<CriticalFailure>>,
112    subscribers: Mutex<Vec<mpsc::Sender<CriticalFailure>>>,
113    sequence: AtomicU64,
114    occurrences: AtomicU64,
115    changed: Notify,
116}
117
118/// The owner-scoped authority for critical task failures.
119///
120/// One state is shared by the critical tasks of a component and by the monitor
121/// that handles them. Clones share the same record.
122#[derive(Debug, Clone)]
123pub struct CriticalFailureState {
124    inner: Arc<CriticalFailureStateInner>,
125}
126
127impl Default for CriticalFailureState {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl CriticalFailureState {
134    /// Creates a state with no recorded failure.
135    #[must_use]
136    pub fn new() -> Self {
137        Self {
138            inner: Arc::new(CriticalFailureStateInner {
139                pending: Mutex::new(None),
140                subscribers: Mutex::new(Vec::new()),
141                sequence: AtomicU64::new(1),
142                occurrences: AtomicU64::new(0),
143                changed: Notify::new(),
144            }),
145        }
146    }
147
148    /// Records one failure and returns it.
149    ///
150    /// The first unhandled failure stays pending, because it is the evidence a
151    /// handler takes with [`Self::handle`]. Later failures still advance the
152    /// sequence and the occurrence count, so a handler can tell that more than
153    /// one occurred, and they never replace the pending record.
154    pub fn record(&self, kind: CriticalFailureKind, task_kind: TaskKind) -> CriticalFailure {
155        let failure = CriticalFailure {
156            kind,
157            task_kind,
158            sequence: self.inner.sequence.fetch_add(1, Ordering::AcqRel),
159            observed_at: Instant::now(),
160        };
161        self.inner.occurrences.fetch_add(1, Ordering::AcqRel);
162        {
163            let mut pending = self.inner.pending.lock();
164            if pending.is_none() {
165                *pending = Some(failure);
166            }
167        }
168        self.inner.changed.notify_waiters();
169        self.notify_subscribers(failure);
170        failure
171    }
172
173    fn notify_subscribers(&self, failure: CriticalFailure) {
174        let mut subscribers = self.inner.subscribers.lock();
175        // A full channel drops this notification and keeps the subscriber: the
176        // pending record is what a handler must rely on.
177        subscribers
178            .retain(|subscriber| !matches!(subscriber.try_send(failure), Err(mpsc::error::TrySendError::Closed(_))));
179    }
180
181    /// Returns the pending failure without handling it.
182    #[must_use]
183    pub fn pending(&self) -> Option<CriticalFailure> {
184        *self.inner.pending.lock()
185    }
186
187    /// Takes the pending failure, marking it handled.
188    #[must_use]
189    pub fn handle(&self) -> Option<CriticalFailure> {
190        self.inner.pending.lock().take()
191    }
192
193    /// Returns how many failures were recorded in this process.
194    #[must_use]
195    pub fn occurrences(&self) -> u64 {
196        self.inner.occurrences.load(Ordering::Acquire)
197    }
198
199    /// Waits until a failure is pending and returns it without handling it.
200    pub async fn wait(&self) -> CriticalFailure {
201        loop {
202            let changed = self.inner.changed.notified();
203            if let Some(failure) = self.pending() {
204                return failure;
205            }
206            changed.await;
207        }
208    }
209
210    /// Subscribes to bounded failure notifications.
211    ///
212    /// The subscription is a signal, not the record: when the channel is full the
213    /// notification is dropped and [`Self::pending`] still reports the failure.
214    ///
215    /// # Errors
216    ///
217    /// Returns a contract violation when `capacity` is zero.
218    pub fn subscribe(&self, capacity: usize) -> Result<CriticalFailureSubscription, RuntimeContractViolation> {
219        if capacity == 0 {
220            return Err(RuntimeContractViolation::InvalidConfiguration {
221                policy: RuntimeContractPolicy::CriticalFailureSubscriptionCapacityPositive,
222            });
223        }
224        let (sender, receiver) = mpsc::channel(capacity);
225        self.inner.subscribers.lock().push(sender);
226        Ok(CriticalFailureSubscription { receiver })
227    }
228
229    /// Spawns a monitor under `owner` that takes each pending failure and hands
230    /// it to `on_failure`.
231    ///
232    /// `owner` must be an owner outside the group of the monitored tasks. A group
233    /// that became poisoned cannot spawn work, so a monitor inside it could never
234    /// run.
235    ///
236    /// # Errors
237    ///
238    /// Returns an error when `owner` is shutting down or closed.
239    pub fn spawn_monitor<F>(
240        &self,
241        owner: &TaskSpawner,
242        name: impl Into<crate::TaskName>,
243        on_failure: F,
244    ) -> RuntimeResult<TaskId>
245    where
246        F: Fn(CriticalFailure) + Send + 'static,
247    {
248        let failures = self.clone();
249        let cancellation = owner.cancellation_token();
250        owner.spawn(name, TaskKind::Worker, async move {
251            loop {
252                tokio::select! {
253                    biased;
254                    _ = cancellation.cancelled() => break,
255                    _ = failures.wait() => {
256                        // Take the current record atomically. Another handler
257                        // may have consumed the notification's older record.
258                        if let Some(failure) = failures.handle() {
259                            on_failure(failure);
260                        }
261                    }
262                }
263            }
264        })
265    }
266}
267
268/// A bounded subscription to critical failure notifications.
269#[derive(Debug)]
270pub struct CriticalFailureSubscription {
271    receiver: mpsc::Receiver<CriticalFailure>,
272}
273
274impl CriticalFailureSubscription {
275    /// Waits for the next failure notification.
276    ///
277    /// Returns `None` once the state is dropped.
278    pub async fn recv(&mut self) -> Option<CriticalFailure> {
279        self.receiver.recv().await
280    }
281
282    /// Returns the next notification without waiting.
283    #[must_use]
284    pub fn try_recv(&mut self) -> Option<CriticalFailure> {
285        self.receiver.try_recv().ok()
286    }
287}