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}