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}