melin_server_runtime/process.rs
1//! Process-level setup for latency-sensitive servers: signal handling
2//! and memory locking. Generic OS plumbing — no application coupling.
3
4use std::sync::Arc;
5use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
6
7/// Pointer to the shared shutdown flag, set once before signals can fire.
8/// `AtomicUsize` stores the raw pointer as an integer — signal-safe.
9static SHUTDOWN_PTR: AtomicUsize = AtomicUsize::new(0);
10
11/// Signal handler for SIGINT/SIGTERM. Sets the shutdown flag.
12/// Second signal force-exits (user is impatient).
13extern "C" fn signal_handler(_sig: libc::c_int) {
14 let ptr = SHUTDOWN_PTR.load(Ordering::Relaxed);
15 if ptr != 0 {
16 let flag = unsafe { &*(ptr as *const AtomicBool) };
17 if flag.swap(true, Ordering::Relaxed) {
18 unsafe { libc::_exit(1) };
19 }
20 }
21}
22
23/// Install SIGINT/SIGTERM handlers that flip `shutdown` on first signal
24/// and force-exit on the second. The caller must keep the `Arc` alive
25/// for the program's lifetime — we publish its pointer to a signal-safe
26/// static so the handler can reach the flag.
27pub fn install_shutdown_handler(shutdown: &Arc<AtomicBool>) {
28 SHUTDOWN_PTR.store(Arc::as_ptr(shutdown) as usize, Ordering::Relaxed);
29 unsafe {
30 libc::signal(
31 libc::SIGINT,
32 signal_handler as *const () as libc::sighandler_t,
33 );
34 libc::signal(
35 libc::SIGTERM,
36 signal_handler as *const () as libc::sighandler_t,
37 );
38 }
39}
40
41/// Pin engine pages into RAM with `mlockall`, lock-on-fault.
42///
43/// Goal: prevent the kernel from faulting *out* engine pages under memory
44/// pressure, which otherwise surfaces as 100µs–10ms tail spikes.
45///
46/// We pass `MCL_ONFAULT` alongside `MCL_CURRENT | MCL_FUTURE` deliberately.
47/// This runs early — before the application's state and the rest of the
48/// pipeline are built — so `MCL_FUTURE` is required to cover those later
49/// allocations.
50/// But plain `MCL_FUTURE` marks every future mapping `VM_LOCKED` and makes
51/// the kernel *eagerly populate* the whole mapping at `mmap` time, inside an
52/// uninterruptible `__mm_populate` walk. Any sizeable post-init allocation
53/// (a large malloc glibc serves via `mmap`, a snapshot/clone) then stalls
54/// the allocating thread until every page is faulted in — long enough to
55/// even outlast SIGKILL. `MCL_ONFAULT` switches future mappings to
56/// lock-on-fault: pages are locked as they are touched, not populated up
57/// front. The fault-out protection we actually want is preserved (a page,
58/// once touched on the hot path, stays resident), without the populate stall.
59///
60/// Best-effort: requires `CAP_IPC_LOCK` (or root) and a sufficient
61/// `RLIMIT_MEMLOCK`. Logs a warning and continues on failure.
62pub fn try_lock_memory() {
63 let unlim = libc::rlimit {
64 rlim_cur: libc::RLIM_INFINITY,
65 rlim_max: libc::RLIM_INFINITY,
66 };
67 let setrlimit_rc = unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &unlim) };
68 if setrlimit_rc != 0 {
69 let err = std::io::Error::last_os_error();
70 tracing::warn!(
71 error = %err,
72 "could not raise RLIMIT_MEMLOCK; mlockall may fail or be capped"
73 );
74 }
75
76 let flags = libc::MCL_CURRENT | libc::MCL_FUTURE | libc::MCL_ONFAULT;
77 let rc = unsafe { libc::mlockall(flags) };
78 if rc == 0 {
79 tracing::info!("mlockall(MCL_CURRENT | MCL_FUTURE | MCL_ONFAULT) succeeded");
80 } else {
81 let err = std::io::Error::last_os_error();
82 tracing::warn!(
83 error = %err,
84 "mlockall failed; running without memory lock — tail latency may be affected"
85 );
86 }
87}