Skip to main content

mz_ore/
process.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
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 in the LICENSE file at the
6// root of this repository, or online at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Process utilities.
17
18/// The exit code of [`halt!`](crate::halt).
19pub const HALT_EXIT_CODE: i32 = 166;
20
21/// The exit code of [`restart_in_place!`](crate::restart_in_place).
22///
23/// This is `EX_TEMPFAIL` from `sysexits.h`. Shells, tini and Kubernetes report
24/// a process killed by signal N as exit code 128 + N, so a code below 128 can
25/// only come from an explicit exit.
26pub const RESTART_IN_PLACE_EXIT_CODE: i32 = 75;
27
28/// Halts the process.
29///
30/// `halt` forwards the provided arguments to the [`tracing::warn`] macro, then
31/// terminates the process with [`HALT_EXIT_CODE`].
32///
33/// Halting a process is a middle ground between a graceful shutdown and a
34/// panic. Use halts for errors that are severe enough to require shutting down
35/// the process, but not severe enough to be considered a crash. Halts are not
36/// intended to be reported to error tracking tools (e.g., Sentry).
37///
38/// There are two common classes of errors that trigger a halt:
39///
40///   * An error that indicates a new process has taken over (e.g., an error
41///     indicating that the process's leader epoch has expired).
42///
43///   * A retriable error where granular retry logic would be tricky enough to
44///     write that it has not yet been worth it. Since restarting from a fresh
45///     process must *always* be correct, forcing a restart is an easy way to
46///     tap into the existing whole-process retry logic. Use halt judiciously in
47///     these cases, as restarting a process can be inefficient (e.g., mandatory
48///     restart backoff, rehydrating expensive state).
49#[cfg_attr(nightly_doc_features, doc(cfg(feature = "tracing")))]
50#[cfg(feature = "tracing")]
51#[macro_export]
52macro_rules! halt {
53    ($($arg:expr),* $(,)?) => {{
54        $crate::__private::tracing::warn!("halting process: {}", format!($($arg),*));
55        $crate::process::exit_thread_safe($crate::process::HALT_EXIT_CODE);
56    }}
57}
58
59/// Halts the process and asks its supervisor to restart it in place.
60///
61/// `restart_in_place` behaves like [`halt!`](crate::halt), but terminates the
62/// process with [`RESTART_IN_PLACE_EXIT_CODE`]. The environmentd container
63/// entrypoint restarts the process right away on this exit code instead of
64/// exiting the container, which avoids Kubernetes crash backoff when a process
65/// halts repeatedly.
66///
67/// The restarted process runs even if Kubernetes has replaced the process's pod
68/// in the meantime, for example after a node failure. Only use this macro while
69/// the deployment is read-only, where the entrypoint's restart marker keeps the
70/// restarted process from booting writable next to the replacement. Use
71/// [`halt!`](crate::halt) everywhere else, in particular after the process has
72/// been fenced out.
73#[cfg_attr(nightly_doc_features, doc(cfg(feature = "tracing")))]
74#[cfg(feature = "tracing")]
75#[macro_export]
76macro_rules! restart_in_place {
77    ($($arg:expr),* $(,)?) => {{
78        $crate::__private::tracing::warn!("halting process for restart in place: {}", format!($($arg),*));
79        $crate::process::exit_thread_safe($crate::process::RESTART_IN_PLACE_EXIT_CODE);
80    }}
81}
82
83/// Prints the given message and exits the process.
84///
85/// `exit!` forwards the provided arguments to the [`tracing::info`] macro, then
86/// terminates the process with given exit code.
87#[cfg_attr(nightly_doc_features, doc(cfg(feature = "tracing")))]
88#[cfg(feature = "tracing")]
89#[macro_export]
90macro_rules! exit {
91    ($exit_code:literal, $($arg:expr),* $(,)?) => {{
92        $crate::__private::tracing::info!("exiting process (0): {}", format!($($arg),*));
93        $crate::process::exit_thread_safe($exit_code);
94    }};
95}
96
97/// Like [`halt!`], but exits with the given code, so that a restart with a
98/// specific cause can be told apart from a routine halt.
99#[cfg_attr(nightly_doc_features, doc(cfg(feature = "tracing")))]
100#[cfg(feature = "tracing")]
101pub fn halt_with_code(exit_code: i32, message: std::fmt::Arguments<'_>) -> ! {
102    crate::__private::tracing::warn!("halting process: {message}");
103    exit_thread_safe(exit_code)
104}
105
106/// Helper for exiting macros.
107///
108/// This function exists to avoid that all callers of `halt!` have to explicitly depend on `libc`.
109pub fn exit_thread_safe(error_code: i32) -> ! {
110    // Using `std::process::exit` here would be unsound as that function invokes libc's `exit`
111    // function, which is not thread-safe [1]. There are two viable work arounds for this:
112    //
113    //  * Calling libc's `_exit` function instead, which is thread-safe by virtue of not performing
114    //    any cleanup work.
115    //  * Introducing a global lock to ensure only a single thread gets to call `exit` at a time.
116    //
117    // We chose the former approach because it's simpler and has the additional benefit of
118    // protecting us from third-party code that installs thread-unsafe exit handlers.
119    //
120    // Note that we are fine with not running any cleanup code here. This behaves just like an
121    // abort we'd do in response to a panic. Any code that relies on cleanup code to run before
122    // process termination is already unsound in the face of panics and hardware failure.
123    //
124    // [1]: https://github.com/rust-lang/rust/issues/126600
125    unsafe { libc::_exit(error_code) };
126}
127
128/// Sets the OS-level name of the calling thread, visible to tools like `top -H`, `perf`,
129/// `samply`, and `/proc/<pid>/task/*/comm`.
130///
131/// Linux truncates thread names to 15 bytes (16 with the NUL terminator); `name` is truncated to
132/// fit. Best-effort: a no-op on non-Linux platforms, and silently does nothing if the underlying
133/// syscall fails.
134pub fn set_current_thread_name(name: &str) {
135    #[cfg(target_os = "linux")]
136    {
137        let mut truncated = name.to_string();
138        while truncated.len() > 15 {
139            truncated.pop();
140        }
141        if let Ok(name) = std::ffi::CString::new(truncated) {
142            unsafe {
143                libc::pthread_setname_np(libc::pthread_self(), name.as_ptr());
144            }
145        }
146    }
147    #[cfg(not(target_os = "linux"))]
148    {
149        let _ = name;
150    }
151}