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}