conmonrs/child_reaper/orphan_sweep.rs
1//! Reaping the zombies nobody claimed.
2
3use super::{Candidate, ChildReaper, ReapableChild};
4use crate::{
5 mutex::lock,
6 process::{has_zombie, reap_guard, wait_exit, zombie_children},
7};
8use nix::{
9 sys::wait::{Id, WaitPidFlag},
10 unistd::Pid,
11};
12use std::{
13 collections::{HashMap, HashSet},
14 sync::{Arc, atomic::Ordering},
15};
16use tokio::{
17 select,
18 signal::unix::{SignalKind, signal},
19 task,
20 time::{self, Duration, Instant},
21};
22use tracing::{Instrument, debug, debug_span, error};
23
24impl ChildReaper {
25 /// How long a zombie nobody claimed stays unreaped before the sweep takes
26 /// it, so that an owner which is about to wait for it still can.
27 const ORPHAN_GRACE: Duration = Duration::from_secs(1);
28
29 /// How long the sweep leaves a zombie nobody claimed alone at most, for
30 /// the runtime calls which were in progress when it was first seen. The
31 /// process of a call can exit before the call claims it, like an exec
32 /// which ends before its runtime exited, and its exit status belongs to
33 /// the call. Each of these calls defers the zombie only until its own
34 /// runtime deadline and the [`ChildReaper::CLAIM_MARGIN`]. This bound
35 /// counts from when the zombie was first seen, the earlier of both ends
36 /// the deferral. That is the one of the call for a zombie first seen
37 /// late in its call, like the process of a slow exec runtime which
38 /// exited right before it, and this one for a create, whose runtime gets
39 /// 10 minutes: a runtime which hangs must not keep the zombies an init
40 /// of a PID namespace waits for that long. An exec whose process exits
41 /// more than this before its runtime does loses its exit code.
42 pub(super) const ORPHAN_MAX_DEFER: Duration = Duration::from_secs(30);
43
44 /// How long after the deadline of its runtime a call may still claim
45 /// its process: a runtime which exits right before it may still take
46 /// the [`ChildReaper::RUNTIME_GRACE`] to connect the terminal,
47 /// the [`ChildReaper::ORPHAN_GRACE`] on top covers the scheduling.
48 pub(super) const CLAIM_MARGIN: Duration =
49 Self::RUNTIME_GRACE.saturating_add(Self::ORPHAN_GRACE);
50
51 /// Reap exited children which nobody waits for, in the background.
52 ///
53 /// A runtime which fails after entering the PID namespace of a container,
54 /// like an exec of a missing binary, leaves its helpers behind as children
55 /// of this subreaper. They exit at any later time. The init of the
56 /// namespace cannot exit until their zombies are reaped, so the container
57 /// could never be stopped.
58 ///
59 /// Every other child has an owner which reaps it: the runtimes are waited
60 /// for by tokio and the container processes by their watchers. They are
61 /// never zombies for long, so a zombie which is neither
62 /// registered nor pending for [`ChildReaper::ORPHAN_GRACE`] is an orphan,
63 /// once no runtime call which may still claim it is in progress, at most
64 /// for [`ChildReaper::ORPHAN_MAX_DEFER`]. Once the server shuts down,
65 /// those calls cannot register a process anymore and are not waited for.
66 pub(crate) fn start_orphan_sweep(self: Arc<Self>) {
67 task::spawn(
68 async move {
69 let mut sigchld = match signal(SignalKind::child()) {
70 Ok(signal) => signal,
71 Err(e) => {
72 error!("Unable to listen for SIGCHLD, orphans stay unreaped: {e}");
73 return;
74 }
75 };
76 let mut candidates = HashMap::new();
77 loop {
78 select! {
79 _ = sigchld.recv() => {}
80 () = self.sweep.notified() => {}
81 // Only candidates need a recheck once their grace is over.
82 () = time::sleep(Self::ORPHAN_GRACE), if !candidates.is_empty() => {}
83 }
84 self.sweep_orphans(&mut candidates, Self::ORPHAN_GRACE, Self::ORPHAN_MAX_DEFER)
85 .await;
86 }
87 }
88 .instrument(debug_span!("orphan_sweep")),
89 );
90 }
91
92 /// Reap the zombie children which have been unclaimed for `grace`, and
93 /// up to `max_defer` while a call which was in progress when the zombie
94 /// was first seen still is and may still claim it, unless the server
95 /// shuts down. `candidates` remembers when a zombie was first seen and
96 /// the calls which were started then.
97 pub(super) async fn sweep_orphans(
98 &self,
99 candidates: &mut HashMap<Pid, Candidate>,
100 grace: Duration,
101 max_defer: Duration,
102 ) {
103 // Most children get reaped by their owners right away, without a
104 // zombie left the scan is not needed. The SIGCHLD of an exit wakes
105 // this task together with the owner, which gets to reap it first.
106 task::yield_now().await;
107 if !has_zombie() {
108 candidates.clear();
109 return;
110 }
111 let (zombies, complete) = match task::spawn_blocking(zombie_children).await {
112 Ok(zombies) => zombies,
113 Err(e) => {
114 error!("Unable to scan for zombie children: {e}");
115 return;
116 }
117 };
118 // The children of a thread which exited meanwhile got another thread
119 // as parent, which may have been listed already. The next sweep finds
120 // them.
121 if !complete {
122 self.sweep.notify_one();
123 }
124
125 let now = Instant::now();
126 candidates.retain(|pid, _| zombies.contains(pid));
127 // The owner of a claimed zombie reaps it soon, it needs no recheck.
128 let claimed = self.claimed();
129 // After the scan, a call which started later did not start the zombie.
130 let calls_before = self.in_flight.borrow().next;
131 for pid in zombies {
132 if claimed.contains(&pid) {
133 candidates.remove(&pid);
134 continue;
135 }
136 let candidate = *candidates.entry(pid).or_insert(Candidate {
137 first_seen: now,
138 calls_before,
139 });
140 if now.duration_since(candidate.first_seen) < grace
141 || self.defers(candidate, now, max_defer)
142 {
143 continue;
144 }
145 // Checked right before reaping, the sets change while the scan
146 // runs. After the calls: a call which claimed the zombie
147 // meanwhile is either still in progress or registered it.
148 if self.is_claimed(pid) {
149 candidates.remove(&pid);
150 continue;
151 }
152 let reaping = reap_guard();
153 let code = wait_exit(Id::Pid(pid), WaitPidFlag::WNOHANG);
154 drop(reaping);
155 match code {
156 Ok(Some(code)) => debug!("Reaped orphaned child {pid}: exit code {code}"),
157 Ok(None) => debug!("Orphaned child {pid} still runs"),
158 Err(e) => debug!("Unable to reap orphaned child {pid}: {e}"),
159 }
160 candidates.remove(&pid);
161 }
162 }
163
164 /// Whether the sweep leaves the zombie `candidate` alone at `now` for a
165 /// call which may still claim it, at most for `max_defer` since it was
166 /// first seen, see [`ChildReaper::ORPHAN_MAX_DEFER`].
167 pub(super) fn defers(&self, candidate: Candidate, now: Instant, max_defer: Duration) -> bool {
168 !self.shutting_down.load(Ordering::SeqCst)
169 && now.duration_since(candidate.first_seen) < max_defer
170 && self
171 .in_flight
172 .borrow()
173 .may_claim_before(candidate.calls_before, now)
174 }
175
176 /// Whether runtime calls are in progress.
177 #[cfg(test)]
178 pub(super) fn calls_in_progress(&self) -> bool {
179 !self.in_flight.borrow().in_progress.is_empty()
180 }
181
182 /// Whether a watcher or a call in progress waits for the child. A
183 /// registered child which its watcher reaped already does not count,
184 /// another child may have its PID by now.
185 ///
186 /// Both sets get checked under the lock of the grandchildren, which
187 /// `watch_grandchild` holds while it moves a child from one to the other.
188 pub(super) fn is_claimed(&self, pid: Pid) -> bool {
189 let map = lock(&self.grandchildren);
190 Self::watched(&map).any(|watched| watched == pid) || lock(&self.pending).contains_key(&pid)
191 }
192
193 /// The children which a watcher or a call in progress waits for, see
194 /// [`ChildReaper::is_claimed`].
195 fn claimed(&self) -> HashSet<Pid> {
196 let map = lock(&self.grandchildren);
197 let mut claimed: HashSet<_> = Self::watched(&map).collect();
198 claimed.extend(lock(&self.pending).keys());
199 claimed
200 }
201
202 /// The registered children which their watchers did not reap yet.
203 fn watched(map: &HashMap<Box<str>, Vec<ReapableChild>>) -> impl Iterator<Item = Pid> {
204 map.values()
205 .flatten()
206 .filter_map(ReapableChild::claimed_pid)
207 }
208}