Skip to main content

conmonrs/child_reaper/
watcher.rs

1//! Watching a registered child until it exited, and what follows.
2
3use super::{Child, ChildKind, EXIT_CODE_TIMED_OUT, ExitChannelData, ReapableChild};
4use crate::{
5    attach::SharedContainerAttach,
6    container_io::{DRAIN_MAX_LOG_WAIT, Drain, Message},
7    container_log::{ContainerLog, SharedContainerLog},
8    fs_util,
9    oom_watcher::{OOMEvent, OOMWatcher},
10    process::{self, Process},
11};
12use anyhow::{Context, Result};
13use nix::sys::signal::Signal;
14use std::{
15    ffi::OsString,
16    io,
17    path::{Path, PathBuf},
18    pin::{Pin, pin},
19    sync::{
20        Arc,
21        atomic::{AtomicU64, Ordering},
22    },
23};
24use tokio::{
25    fs,
26    io::AsyncWriteExt,
27    sync::broadcast::{self, Receiver},
28    task,
29    time::{self, Instant},
30};
31use tracing::{Instrument, debug, debug_span, error, info, trace, warn};
32
33/// Runs a closure once dropped, at the end of a task however it ends.
34struct OnDrop<F: FnOnce()>(Option<F>);
35
36impl<F: FnOnce()> Drop for OnDrop<F> {
37    fn drop(&mut self) {
38        if let Some(f) = self.0.take() {
39            f();
40        }
41    }
42}
43
44/// What only the watcher of a child needs, which moves into it, see
45/// [`ReapableChild::watch`].
46#[derive(Debug)]
47pub(crate) struct ExitHandling {
48    exit_paths: Vec<PathBuf>,
49    oom_exit_paths: Vec<PathBuf>,
50
51    /// When the process gets killed, only an exec_sync has one.
52    timeout: Option<Instant>,
53}
54
55impl ExitHandling {
56    /// Write neither exit nor OOM exit files, for a child whose call fails.
57    pub(super) fn drop_exit_paths(&mut self) {
58        self.exit_paths.clear();
59        self.oom_exit_paths.clear();
60    }
61}
62
63impl ReapableChild {
64    /// Whether the process which exited with `exit_code` got OOM killed. The
65    /// watcher writes the files for the OOMs it could tell on its own, this
66    /// the ones which only the exit status tells.
67    pub(super) async fn handle_oom(
68        event: Option<OOMEvent>,
69        exit_code: i32,
70        oom_exit_paths: &[PathBuf],
71    ) -> bool {
72        let Some(event) = event else {
73            return false;
74        };
75        let oomed = event.is_oom(exit_code);
76        if oomed && !event.oom {
77            debug!("Taking the kill of the process for an OOM, its cgroup is gone");
78            OOMWatcher::write_oom_files(oom_exit_paths).await;
79        }
80        oomed
81    }
82
83    /// Wait until the output ended, or until the [`Drain`] gives up. A
84    /// process which inherited the pipes and keeps them open must not delay
85    /// the exit file. Output on its way into the log is no pause, a slow log
86    /// driver must not cut off the end of the log, up to the `deadline`.
87    /// False if the deadline passed, which got logged here.
88    pub(super) async fn wait_for_output(attach: SharedContainerAttach, deadline: Instant) -> bool {
89        let (mut rx, ended) = attach.subscribe();
90        if ended {
91            return true;
92        }
93        let mut drain = Drain::default();
94        loop {
95            if time::timeout_at(deadline, attach.wait_logged())
96                .await
97                .is_err()
98            {
99                warn!("Log drivers did not take the output in time, not waiting for them");
100                return false;
101            }
102            // A read loop which got output while the drain waited gives it
103            // another chance.
104            let Ok(output) = time::timeout_at(deadline, drain.recv_output(&mut rx, None)).await
105            else {
106                warn!("Output did not end in time, not waiting for it");
107                return false;
108            };
109            if output.is_none() && (drain.exhausted() || !attach.is_logging()) {
110                return true;
111            }
112        }
113    }
114
115    /// Wait for the output like [`ReapableChild::wait_for_output`], then
116    /// stop the read loops and wait for them to end, which writes the lines
117    /// still waiting for their ending into the log, and sync the log to
118    /// disk. Nothing reaches the log after the exit file this way. A log
119    /// write or sync which hangs must not hold the exit file back, all of it
120    /// takes [`DRAIN_MAX_LOG_WAIT`] at most.
121    pub(super) async fn finish_output(attach: SharedContainerAttach, logger: &SharedContainerLog) {
122        let deadline = Instant::now() + DRAIN_MAX_LOG_WAIT;
123        let logged = Self::wait_for_output(attach.clone(), deadline).await;
124        attach.stop_output();
125        let (mut rx, ended) = attach.subscribe();
126        if !ended {
127            // Without read loops, the output never ends and nothing got
128            // logged.
129            if !attach.has_output() {
130                return;
131            }
132            let end = async {
133                while !matches!(
134                    rx.recv().await,
135                    Ok(Message::Done) | Err(broadcast::error::RecvError::Closed)
136                ) {}
137            };
138            // The read loops get until the deadline to end, the sync would
139            // get no time after it. A wait for the output which ran into the
140            // deadline, because the output did not end or the log drivers
141            // hang, logged that already, the read loops hardly end then.
142            if time::timeout_at(deadline, end).await.is_err() {
143                if logged {
144                    warn!("Read loops did not end in time, not waiting for them");
145                }
146                return;
147            }
148        }
149        match time::timeout_at(deadline, ContainerLog::sync(logger)).await {
150            Ok(Ok(())) => {}
151            Ok(Err(e)) => warn!("Unable to sync the log: {e:#}"),
152            Err(_) => warn!("Log did not sync in time, not waiting for it"),
153        }
154    }
155
156    /// Split the child into the registered part and what only its watcher
157    /// needs.
158    pub(crate) fn from_child(child: Child) -> (Box<str>, Self, ExitHandling) {
159        static NEXT_SEQ: AtomicU64 = AtomicU64::new(0);
160        let Child {
161            id,
162            kind,
163            pid,
164            exit_paths,
165            oom_exit_paths,
166            timeout,
167            io,
168            token,
169            process,
170        } = child;
171        (
172            id,
173            Self {
174                seq: NEXT_SEQ.fetch_add(1, Ordering::Relaxed),
175                pid,
176                // Not reaped before its watcher runs, the PID is its own.
177                process: process.unwrap_or_else(|| Arc::new(Process::open(pid))),
178                reaped: Arc::default(),
179                io: (kind == ChildKind::Container).then_some(io),
180                token,
181            },
182            ExitHandling {
183                exit_paths,
184                oom_exit_paths,
185                timeout,
186            },
187        )
188    }
189
190    /// Wait for the exit of the process with `wait`, or kill it at the
191    /// `timeout`, and return its exit code and whether it timed out. The
192    /// wait stays pending on the timeout, the watcher reaps the process with
193    /// it once the exit went out, see [`ReapableChild::reap_killed`].
194    async fn wait_or_kill(
195        process: &Process,
196        mut wait: Pin<&mut impl Future<Output = i32>>,
197        timeout: Option<Instant>,
198    ) -> (i32, bool) {
199        let Some(timeout) = timeout else {
200            return (wait.await, false);
201        };
202        if let Ok(code) = time::timeout_at(timeout, wait.as_mut()).await {
203            return (code, false);
204        }
205        match process.signal_group(Signal::SIGKILL) {
206            Ok(()) => info!(
207                pid = %process.pid(),
208                "Killed the process, it did not exit before its timeout"
209            ),
210            Err(e) => debug!("Not killing timed out process, it is gone: {e}"),
211        }
212        (EXIT_CODE_TIMED_OUT, true)
213    }
214
215    /// Reap the process which got killed at its timeout through the `wait`
216    /// which timed out, once the SIGKILL took effect. A second wait would
217    /// register the pidfd again, or race this one, whose blocking wait still
218    /// runs without a pidfd, and could take the exit of another child which
219    /// reused the PID. A process stuck in the kernel, like on a hung network
220    /// mount, may take long, it keeps only a pidfd busy meanwhile, not a
221    /// thread. The process stays registered until then, so that the orphan
222    /// sweep leaves it to this wait and the shutdown waits for it and
223    /// reports it if it does not exit in time.
224    async fn reap_killed(wait: Pin<&mut impl Future<Output = i32>>) {
225        let code = wait.await;
226        debug!(code, "Timed out process got reaped");
227    }
228
229    /// Watch the process and return the receiver for its exit, which exists
230    /// before the task starts, a receiver subscribed after the exit would
231    /// miss it. `forget` runs once the exit got sent and the process got
232    /// reaped, which comes later for one killed at its timeout, or the task
233    /// ended otherwise, like with a panic. A task which never ran, which
234    /// tokio drops right in `spawn` while the runtime shuts down, does not
235    /// run it, so the caller may hold a lock which `forget` takes.
236    pub(super) fn watch(
237        &self,
238        exit: ExitHandling,
239        forget: impl FnOnce() + Send + 'static,
240    ) -> Receiver<ExitChannelData> {
241        let ExitHandling {
242            exit_paths,
243            oom_exit_paths,
244            timeout,
245        } = exit;
246        let pid = self.pid();
247        let process = self.process.clone();
248        let reaped = self.reaped.clone();
249        // Only one exit code will be written.
250        let (exit_tx, exit_rx) = broadcast::channel(1);
251        let token = self.token.clone();
252        // None for an exec, see `io`.
253        let io = self.io.clone();
254
255        task::spawn(
256            async move {
257                // Only created on the first poll, see above.
258                let _forget = OnDrop(Some(forget));
259                // Nobody learns about an OOM without the files, like an exec.
260                let oom = async {
261                    if oom_exit_paths.is_empty() {
262                        return None;
263                    }
264                    OOMWatcher::watch(&token, pid, &oom_exit_paths)
265                        .instrument(debug_span!("cgroup_handling", %pid))
266                        .await
267                };
268                let mut reap =
269                    pin!(process::wait_for_exit(&process).instrument(debug_span!("wait_for_exit")));
270                let wait = async {
271                    let (exit_code, timed_out) =
272                        Self::wait_or_kill(&process, reap.as_mut(), timeout).await;
273                    // One killed at its timeout gets reaped at the end of
274                    // the watcher, which forgets it right after.
275                    reaped.store(!timed_out, Ordering::Release);
276                    // Tells the users of the token that the process is gone,
277                    // which ends the OOM watcher.
278                    token.cancel();
279                    (exit_code, timed_out)
280                };
281                let ((exit_code, timed_out), event) = tokio::join!(wait, oom);
282                let oomed = Self::handle_oom(event, exit_code, &oom_exit_paths).await;
283
284                // The exit file tells that the log is complete, so let the read
285                // loops write what the process left in the pipes, and stop
286                // them, see `stop_output`. The output of an exec gets stopped
287                // by its consumer, see `StopOutput`.
288                if let Some(io) = &io {
289                    Self::finish_output(io.attach().clone(), io.logger()).await;
290                }
291
292                let exit_channel_data = ExitChannelData {
293                    exit_code,
294                    timed_out,
295                };
296                // Only of a container: a node runs thousands of execs, like
297                // the ones of probes. With the PID, which the lines of the
298                // shutdown name for the processes which did not exit.
299                if io.is_some() {
300                    info!(%pid, exit_code, oomed, "Container exited");
301                } else {
302                    debug!(exit_code, timed_out, "Exec process exited");
303                }
304                Self::write_to_exit_paths(exit_code, &exit_paths).await;
305
306                // Only the consumer of an exec receives it, nobody of a
307                // container.
308                if exit_tx.send(exit_channel_data).is_err() {
309                    trace!("Nobody receives the exit status");
310                }
311                // Only after the exit went out, so that the client gets its
312                // timeout right away.
313                if timed_out {
314                    Self::reap_killed(reap).await;
315                }
316            }
317            .instrument(debug_span!("watch", %pid))
318            // The span of the request or the session which started the
319            // process names its container, see `enter_request` of the RPC
320            // interface. Below the debug level, the span above is disabled
321            // and would not hand it on.
322            .in_current_span(),
323        );
324        exit_rx
325    }
326
327    /// Write the exit code to every exit path. Failures are logged per path,
328    /// one unwritable path must not prevent the others.
329    async fn write_to_exit_paths(code: i32, paths: &[PathBuf]) {
330        let code_str = format!("{code}");
331        for path_buf in paths {
332            let span = debug_span!("write_exit_path", path = %path_buf.display());
333            async {
334                debug!(code, "Writing exit code to file");
335                if let Err(e) = Self::write_exit_file(path_buf, &code_str).await {
336                    error!("Unable to write exit file: {e:#}");
337                }
338            }
339            .instrument(span)
340            .await;
341        }
342    }
343
344    /// Write the exit file at once, like conmon does. CRI-O reads it as soon
345    /// as it gets created, which must not find it empty. CRI-O ignores the
346    /// temporary file, which is not named like a container. A symlink in its
347    /// place does not get followed, like for the log files, the file gets
348    /// created new.
349    pub(super) async fn write_exit_file(path: &Path, contents: &str) -> Result<()> {
350        let name = path.file_name().context("exit path has no file name")?;
351        let mut tmp_name = OsString::from(".");
352        tmp_name.push(name);
353        tmp_name.push(".tmp");
354        let tmp = path.with_file_name(tmp_name);
355        let result = match Self::write_new_file(&tmp, contents).await {
356            Ok(()) => fs::rename(&tmp, path)
357                .await
358                .with_context(|| format!("rename to {}", path.display())),
359            Err(e) => Err(e).with_context(|| format!("write {}", tmp.display())),
360        };
361        if result.is_err()
362            && let Err(e) = fs::remove_file(&tmp).await
363        {
364            debug!("Unable to remove {}: {e}", tmp.display());
365        }
366        result
367    }
368
369    /// Create the file new with the contents. One left behind at the path,
370    /// like by a crash, gets removed first, a symlink is not followed.
371    async fn write_new_file(path: &Path, contents: &str) -> io::Result<()> {
372        fs_util::remove_file(path)?;
373        let mut file = fs::OpenOptions::new()
374            .write(true)
375            .create_new(true)
376            .custom_flags(libc::O_NOFOLLOW)
377            .open(path)
378            .await?;
379        file.write_all(contents.as_bytes()).await?;
380        // The write runs in the background until flushed, the rename must
381        // not come first.
382        file.flush().await
383    }
384}