Skip to main content

conmonrs/container_io/
drain.rs

1//! Bounding the output which gets forwarded after the process exited.
2
3use super::Message;
4use crate::{
5    attach::SharedContainerAttach,
6    channel::{Receiver, RecvError},
7};
8use std::ops::ControlFlow;
9use tokio::{
10    select,
11    sync::broadcast,
12    time::{self, Duration, Instant},
13};
14use tracing::{debug, warn};
15
16/// What [`Drain::recv_logging`] ended with.
17enum Logging {
18    /// A result of the receiver, output which came meanwhile.
19    Output(Result<Message, broadcast::error::RecvError>),
20
21    /// No read loop holds output on its way into the log anymore.
22    Logged,
23
24    /// The log drivers did not take the output within the budget.
25    GaveUp,
26}
27
28/// Upper bound for waiting on the next output after the process exited. The
29/// read loops may still be draining the pipes when the exit is observed, they
30/// signal the end with `Message::Done`. The timeout only kicks in if that
31/// never arrives, for example because a grandchild keeps the pipe open.
32pub(crate) const DRAIN_TIMEOUT: Duration = Duration::from_millis(100);
33
34/// Upper bound for the messages forwarded per drain after the process exited.
35/// The output written before the exit is at most what the channels, the
36/// [`STDIO_CHANNEL_SIZE`](super::STDIO_CHANNEL_SIZE) of the stdio ones or
37/// the [`OUTPUT_CHANNEL_SIZE`](crate::attach::OUTPUT_CHANNEL_SIZE) of the
38/// attach broadcast, and the pipe buffers hold. With the default pipe-max-size
39/// of 1 MiB in 8 KiB reads, that is 128 messages per pipe, and small writes
40/// read one by one do not fill a pipe of that size either, so this is well
41/// above. It is a heuristic though, a process with CAP_SYS_RESOURCE can grow
42/// its pipes further. More output stems from a grandchild which keeps the
43/// pipe open and writing, which would otherwise keep the drain going forever.
44pub(super) const DRAIN_MAX_MESSAGES: usize = 1024;
45
46/// Upper bound for the time a drain waits for output after the process
47/// exited in total. A grandchild which keeps writing just often enough for
48/// [`DRAIN_TIMEOUT`] would keep it going for minutes otherwise. Only the
49/// waiting counts, not the forwarding, so a slow client still gets the
50/// output which was there already.
51pub(crate) const DRAIN_MAX_WAIT: Duration = Duration::from_secs(1);
52
53/// Upper bound for the time the exit files and the attach drains wait for
54/// the log drivers to take the output after the process exited, which does
55/// not count as a pause of the output, see `SharedContainerAttach::logging`
56/// and [`Drain::recv_attach`], and for the read loops to end once stopped
57/// and the log to sync, see `ReapableChild::finish_output`. A log driver
58/// which is slow, like a journal which takes only part of every chunk in
59/// time, or which hangs, like a log file on a stuck disk, must not keep the
60/// exit from being reported for minutes.
61pub(crate) const DRAIN_MAX_LOG_WAIT: Duration = Duration::from_secs(5);
62
63/// Bounds forwarding the output after the process exited.
64pub(crate) struct Drain {
65    remaining: usize,
66    waited: Duration,
67
68    /// How long the drain waited for the log drivers so far, see
69    /// [`Drain::recv_attach`].
70    log_waited: Duration,
71}
72
73impl Default for Drain {
74    fn default() -> Self {
75        Self {
76            remaining: DRAIN_MAX_MESSAGES,
77            waited: Duration::ZERO,
78            log_waited: Duration::ZERO,
79        }
80    }
81}
82
83impl Drain {
84    /// Wait for the next message, giving up once the channel stays quiet for
85    /// [`DRAIN_TIMEOUT`], [`DRAIN_MAX_MESSAGES`] were received or the waits
86    /// took [`DRAIN_MAX_WAIT`].
87    pub(crate) async fn recv<F: Future>(&mut self, recv: F) -> Option<F::Output> {
88        if !self.take() {
89            return None;
90        }
91        let budget = DRAIN_MAX_WAIT.saturating_sub(self.waited);
92        if budget.is_zero() {
93            warn!("Dropping remaining output after waiting {DRAIN_MAX_WAIT:?} since the exit");
94            return None;
95        }
96        let start = Instant::now();
97        let res = time::timeout(DRAIN_TIMEOUT.min(budget), recv).await.ok();
98        self.waited += start.elapsed();
99        res
100    }
101
102    /// Account for the next message, false once [`DRAIN_MAX_MESSAGES`] got
103    /// forwarded, which gets logged here.
104    fn take(&mut self) -> bool {
105        match self.remaining.checked_sub(1) {
106            Some(remaining) => {
107                self.remaining = remaining;
108                true
109            }
110            None => {
111                warn!(
112                    "Dropping remaining output after forwarding {DRAIN_MAX_MESSAGES} messages since the exit"
113                );
114                false
115            }
116        }
117    }
118
119    /// Whether the drain forwarded or waited as much as it may.
120    pub(crate) fn exhausted(&self) -> bool {
121        self.remaining == 0 || self.waited >= DRAIN_MAX_WAIT
122    }
123
124    /// The next message of the attach output with [`Drain::recv`]. A
125    /// receiver which fell behind skips messages, which gets logged, for
126    /// the receiver of a client with its [`Lag`], the one it kept before
127    /// the exit. None once the output ended or the drain gave up.
128    pub(crate) async fn recv_output(
129        &mut self,
130        rx: &mut broadcast::Receiver<Message>,
131        mut lag: Option<&mut Lag>,
132    ) -> Option<Message> {
133        loop {
134            let res = self.recv(rx.recv()).await?;
135            if let ControlFlow::Break(message) = Self::output(res, rx, lag.as_deref_mut()) {
136                return message;
137            }
138        }
139    }
140
141    /// What a result of the attach receiver `rx` means for the client, see
142    /// [`Drain::recv_output`]: the message, none once the output ended, or
143    /// skipped messages, which `lag` notes and the caller goes on after.
144    fn output(
145        res: Result<Message, broadcast::error::RecvError>,
146        rx: &broadcast::Receiver<Message>,
147        lag: Option<&mut Lag>,
148    ) -> ControlFlow<Option<Message>> {
149        match res {
150            Ok(Message::Done) | Err(broadcast::error::RecvError::Closed) => {
151                ControlFlow::Break(None)
152            }
153            Ok(message) => {
154                if let Some(lag) = lag {
155                    lag.received(rx);
156                }
157                ControlFlow::Break(Some(message))
158            }
159            Err(broadcast::error::RecvError::Lagged(n)) => {
160                match lag {
161                    Some(lag) => lag.skipped(n),
162                    None => debug!("Skipped {n} attach output messages"),
163                }
164                ControlFlow::Continue(())
165            }
166        }
167    }
168
169    /// The next message of the attach output of `attach` like
170    /// [`Drain::recv_output`], for a drain after the exit. A read loop which
171    /// holds output on its way into the log, see
172    /// [`SharedContainerAttach::logging`], gets until [`DRAIN_MAX_LOG_WAIT`]
173    /// in total to broadcast it, which is no pause of the output: the client
174    /// must not lose the last output to a log write which takes longer than
175    /// [`DRAIN_TIMEOUT`]. The output which got broadcast already, or gets
176    /// broadcast meanwhile by another read loop, comes right away, a log
177    /// which hangs must not keep it from the client. None once the output
178    /// ended or the drain gave up.
179    pub(crate) async fn recv_attach(
180        &mut self,
181        attach: &SharedContainerAttach,
182        rx: &mut broadcast::Receiver<Message>,
183        mut lag: Option<&mut Lag>,
184    ) -> Option<Message> {
185        loop {
186            let res = if attach.is_logging() {
187                match self.recv_logging(attach, rx).await {
188                    Logging::Output(res) => res,
189                    // Taken by the log, the output gets broadcast now.
190                    Logging::Logged => continue,
191                    Logging::GaveUp => return None,
192                }
193            } else {
194                let Some(res) = self.recv(rx.recv()).await else {
195                    // A read loop which got output while the drain waited
196                    // gives it another chance.
197                    if self.exhausted() || !attach.is_logging() {
198                        return None;
199                    }
200                    continue;
201                };
202                res
203            };
204            if let ControlFlow::Break(message) = Self::output(res, rx, lag.as_deref_mut()) {
205                return message;
206            }
207        }
208    }
209
210    /// Receive from `rx` while a read loop holds output on its way into the
211    /// log, until it took it or what is left of [`DRAIN_MAX_LOG_WAIT`]
212    /// passed, which gets logged here once. The output which is queued or
213    /// gets broadcast meanwhile comes first.
214    async fn recv_logging(
215        &mut self,
216        attach: &SharedContainerAttach,
217        rx: &mut broadcast::Receiver<Message>,
218    ) -> Logging {
219        if !self.take() {
220            return Logging::GaveUp;
221        }
222        let budget = DRAIN_MAX_LOG_WAIT.saturating_sub(self.log_waited);
223        let start = Instant::now();
224        let res = select! {
225            biased;
226            res = rx.recv() => Logging::Output(res),
227            logged = time::timeout(budget, attach.wait_logged()) => match logged {
228                Ok(()) => {
229                    // No message got taken, the slot goes back.
230                    self.remaining += 1;
231                    Logging::Logged
232                }
233                Err(_) => {
234                    // Logged once, when the budget ran out.
235                    if !budget.is_zero() {
236                        warn!("Log drivers did not take the output in time, not waiting for them");
237                    }
238                    Logging::GaveUp
239                }
240            },
241        };
242        self.log_waited += start.elapsed();
243        res
244    }
245
246    /// The next message of a stdio channel with [`Drain::recv`]. None once
247    /// the output ended, the channel got closed or the drain gave up.
248    pub(crate) async fn recv_stdio(&mut self, rx: &Receiver<Message>) -> Option<Message> {
249        match self.recv(rx.recv()).await? {
250            Ok(Message::Done) | Err(RecvError) => None,
251            Ok(message) => Some(message),
252        }
253    }
254}
255
256/// Logs an attach client which falls behind the output once until it
257/// caught up again, like [`LogErrors`]. A client which stays too slow skips
258/// messages again every time the channel filled up, which would log a
259/// warning for every one of its capacity of messages otherwise.
260///
261/// [`LogErrors`]: super::read_loop::LogErrors
262#[derive(Debug, Default)]
263pub(crate) struct Lag {
264    /// The messages skipped since the client fell behind, none while it
265    /// keeps up.
266    pub(super) skipped: Option<u64>,
267}
268
269impl Lag {
270    /// The client skipped `n` messages.
271    pub(crate) fn skipped(&mut self, n: u64) {
272        match &mut self.skipped {
273            Some(skipped) => {
274                *skipped = skipped.saturating_add(n);
275                debug!("Attach client still too slow, skipped {skipped} messages so far");
276            }
277            None => {
278                self.skipped = Some(n);
279                warn!("Attach client too slow, skipped {n} messages, more until it catches up");
280            }
281        }
282    }
283
284    /// The client received a message from `rx`. It caught up once nothing
285    /// is queued for it anymore.
286    pub(crate) fn received<T>(&mut self, rx: &broadcast::Receiver<T>) {
287        if self.skipped.is_some()
288            && rx.is_empty()
289            && let Some(skipped) = self.skipped.take()
290        {
291            debug!("Attach client caught up after skipping {skipped} messages");
292        }
293    }
294}