Skip to main content

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}