Skip to main content

conmonrs/oom_watcher/
inotify.rs

1//! A single inotify instance which watches the memory events files of all
2//! containers.
3
4use crate::{
5    async_fd::{self, Fd},
6    mutex::lock,
7};
8use anyhow::{Context, Result};
9use nix::{
10    errno::Errno,
11    sys::inotify::{AddWatchFlags, InitFlags, Inotify, InotifyEvent, WatchDescriptor},
12};
13use std::{
14    collections::HashMap,
15    io,
16    path::Path,
17    sync::{
18        Arc, Mutex, Weak,
19        atomic::{AtomicBool, AtomicU64, Ordering},
20    },
21};
22use tokio::{
23    io::{ErrorKind, Interest, unix::AsyncFd},
24    runtime::Handle,
25    sync::mpsc::Sender,
26    task::{self, JoinHandle},
27    time::{self, Duration},
28};
29use tracing::{error, trace};
30
31/// A watch event as delivered to subscribers.
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub(super) struct WatchEvent {
34    /// The file which changed. None if the kernel dropped events, every
35    /// watched file may have changed then.
36    pub(super) path: Option<Arc<Path>>,
37}
38
39impl WatchEvent {
40    /// Events got lost, by the kernel or a full channel.
41    pub(super) const LOST: Self = Self { path: None };
42}
43
44/// What a watch reports. cgroupfs notifies a change of the memory events
45/// with `IN_MODIFY` and the removal of a cgroup with `IN_IGNORED` only,
46/// which every watch gets anyway.
47const WATCH_FLAGS: AddWatchFlags = AddWatchFlags::IN_MODIFY;
48
49/// How often the events get dispatched at most. The kernel notifies a change
50/// of the memory events of a cgroup up to every 10 ms, and one of the
51/// hierarchical ones of every ancestor, which alternate in the queue, so
52/// that inotify does not merge them. Under memory pressure, like at
53/// `memory.high` or `memory.max`, a single container would wake the server
54/// up to a hundred times a second otherwise, and every one of its watchers
55/// would read its counters as often. The queue of the kernel holds 16384
56/// events by default, which takes more than 1600 watched files changing in
57/// one interval, far more than the containers of a pod. An overflow gets
58/// dispatched to every watcher, see [`Watches::dispatch`].
59const MIN_DISPATCH_INTERVAL: Duration = Duration::from_millis(100);
60
61/// The watches by descriptor. A watch belongs to a file rather than to its
62/// path: a file which got recreated gets a watch of its own, and the events
63/// of the old one, like its removal, must not reach the subscribers of the
64/// new one.
65#[derive(Default)]
66struct Watches(HashMap<WatchDescriptor, Watch>);
67
68/// A watch of a file and its subscribers.
69struct Watch {
70    path: Arc<Path>,
71    subscribers: Vec<(u64, Sender<WatchEvent>)>,
72}
73
74impl Watches {
75    /// Hand an event of the kernel to the subscribers of its watch.
76    fn dispatch(&mut self, event: &InotifyEvent) {
77        if event.mask.contains(AddWatchFlags::IN_Q_OVERFLOW) {
78            self.lost();
79            return;
80        }
81        let Some(watch) = self.0.get(&event.wd) else {
82            trace!("Watch event with unknown descriptor: {event:?}");
83            return;
84        };
85        // A removal of a cgroup only removes the watch, which tells nothing
86        // about the file, see [`OOMWatcher::watch_cgroup_v2`].
87        if event.mask.intersects(WATCH_FLAGS) {
88            let watch_event = WatchEvent {
89                path: Some(watch.path.clone()),
90            };
91            for (_, tx) in &watch.subscribers {
92                Self::deliver(tx, &watch_event);
93            }
94        }
95        // The last event of a watch, the kernel may reuse the descriptor.
96        if event.mask.contains(AddWatchFlags::IN_IGNORED) {
97            self.0.remove(&event.wd);
98        }
99    }
100
101    /// Tell every subscriber that events got lost, which may concern every
102    /// file.
103    fn lost(&self) {
104        for (_, tx) in self.0.values().flat_map(|watch| &watch.subscribers) {
105            Self::deliver(tx, &WatchEvent::LOST);
106        }
107    }
108
109    /// Deliver an event without ever blocking the reader of the events. The
110    /// last free slot of a channel takes a lost event instead, so that a
111    /// full channel drops events without its receiver missing that it did:
112    /// a dropped event of the pod cgroup must still refresh its baseline.
113    fn deliver(tx: &Sender<WatchEvent>, event: &WatchEvent) {
114        let event = if tx.capacity() > 1 {
115            event.clone()
116        } else {
117            WatchEvent::LOST
118        };
119        if tx.try_send(event).is_err() {
120            trace!("Dropping watch event, subscriber not ready");
121        }
122    }
123}
124
125/// A single inotify instance shared by all OOM watchers.
126///
127/// Creating an inotify instance per container does not scale with the number
128/// of containers in a pod. All memory event files are watched through this
129/// instance instead, a task reads its events and dispatches them to the
130/// subscribers of their path. Adding and removing a watch does not block, the
131/// subscribers do that right away.
132pub(super) struct SharedWatcher {
133    inotify: Arc<Inotify>,
134
135    /// The subscribers and the watches. The reader of the events takes this
136    /// lock as well, it never waits while holding it.
137    watches: Arc<Mutex<Watches>>,
138
139    /// Set once the reader of the events stopped, the next subscription gets
140    /// a new watcher then.
141    failed: Arc<AtomicBool>,
142
143    /// Set once a subscriber gave up watching after the reader stopped, see
144    /// [`Subscription::give_up`].
145    given_up: AtomicBool,
146
147    /// The task which reads the events, it ends with the watcher.
148    reader: JoinHandle<()>,
149
150    /// Source of unique subscription identifiers.
151    next_id: AtomicU64,
152}
153
154/// Where the shared watcher is kept, see [`SharedWatcher::get`].
155type Slot = Mutex<Weak<SharedWatcher>>;
156
157/// The shared watcher is kept alive by its subscriptions. It gets created on
158/// the first subscription and dropped with the last one, which frees the
159/// inotify instance while no container is watched. A failed creation is
160/// retried on the next subscription instead of being cached, and so is a
161/// watcher whose reader stopped. The server has a single runtime, which reads
162/// the events.
163#[cfg(not(test))]
164static SHARED_WATCHER: Slot = Mutex::new(Weak::new());
165
166/// The unit tests run a runtime per test, each one gets a watcher of its own,
167/// which its runtime reads.
168#[cfg(test)]
169static SHARED_WATCHERS: Mutex<Vec<(tokio::runtime::Id, &'static Slot)>> = Mutex::new(Vec::new());
170
171impl SharedWatcher {
172    /// Retrieve the shared watcher, creating it if necessary.
173    pub(super) fn get() -> Result<Arc<Self>> {
174        // The reader of the events is a task of the runtime.
175        let runtime = Handle::try_current().context("get runtime")?;
176        Self::get_in(Self::slot(&runtime))
177    }
178
179    /// Retrieve the watcher kept in the slot, or create one there.
180    fn get_in(slot: &Slot) -> Result<Arc<Self>> {
181        let mut slot = lock(slot);
182        if let Some(watcher) = slot
183            .upgrade()
184            .filter(|watcher| !watcher.failed.load(Ordering::Acquire))
185        {
186            return Ok(watcher);
187        }
188        let watcher = Arc::new(Self::new().context("create shared file watcher")?);
189        *slot = Arc::downgrade(&watcher);
190        Ok(watcher)
191    }
192
193    #[cfg(not(test))]
194    fn slot(_: &Handle) -> &'static Slot {
195        &SHARED_WATCHER
196    }
197
198    #[cfg(test)]
199    fn slot(runtime: &Handle) -> &'static Slot {
200        let id = runtime.id();
201        let mut shared = lock(&SHARED_WATCHERS);
202        if let Some((_, slot)) = shared.iter().find(|(i, _)| *i == id) {
203            return slot;
204        }
205        // One per test, which lives as long as the static.
206        let slot = Box::leak(Box::default());
207        shared.push((id, slot));
208        slot
209    }
210
211    fn new() -> Result<Self> {
212        let inotify = Inotify::init(InitFlags::IN_NONBLOCK | InitFlags::IN_CLOEXEC)
213            .context("create inotify instance")?;
214        let inotify = Arc::new(inotify);
215        let registered = async_fd::register(inotify.clone(), Interest::READABLE)
216            .context("register inotify instance")?;
217        let watches = Arc::<Mutex<Watches>>::default();
218        let failed = Arc::<AtomicBool>::default();
219        let reader = task::spawn(Self::read_events(
220            registered,
221            watches.clone(),
222            failed.clone(),
223        ));
224        Ok(Self {
225            inotify,
226            watches,
227            failed,
228            given_up: AtomicBool::new(false),
229            reader,
230            next_id: AtomicU64::new(0),
231        })
232    }
233
234    /// Read the events of the kernel and dispatch them, until the watcher
235    /// got dropped or reading failed.
236    ///
237    /// After a dispatch, the events which come meanwhile get dispatched
238    /// together once [`MIN_DISPATCH_INTERVAL`] passed. The instance gets
239    /// deregistered until then: tokio registers it edge triggered, and
240    /// every new event would wake the runtime while it stays registered,
241    /// even if nothing reads it.
242    async fn read_events(
243        mut registered: AsyncFd<Fd<Arc<Inotify>>>,
244        watches: Arc<Mutex<Watches>>,
245        failed: Arc<AtomicBool>,
246    ) {
247        loop {
248            let events = match registered
249                .async_io(Interest::READABLE, |fd| Self::read_queued(fd))
250                .await
251            {
252                Ok(events) => events,
253                Err(e) if e.kind() == ErrorKind::Interrupted => continue,
254                Err(e) => {
255                    error!(
256                        "Unable to read watch events, watching again on a new instance a few times at most: {e}"
257                    );
258                    break;
259                }
260            };
261            {
262                let mut watches = lock(&watches);
263                for event in &events {
264                    watches.dispatch(event);
265                }
266            }
267
268            let inotify = Arc::clone(registered.get_ref());
269            drop(registered);
270            time::sleep(MIN_DISPATCH_INTERVAL).await;
271            // Events which are queued already get reported right away.
272            registered = match async_fd::register(inotify, Interest::READABLE) {
273                Ok(registered) => registered,
274                Err(e) => {
275                    error!(
276                        "Unable to register inotify instance, watching again on a new one a few times at most: {e}"
277                    );
278                    break;
279                }
280            };
281        }
282        Self::stopped(&watches, &failed);
283    }
284
285    /// Read all queued events, which a single read takes up to 4 KiB of.
286    /// Fails with `WouldBlock` only if there is none.
287    fn read_queued(inotify: &Inotify) -> io::Result<Vec<InotifyEvent>> {
288        let mut events = inotify.read_events()?;
289        loop {
290            match inotify.read_events() {
291                Ok(more) => events.extend(more),
292                Err(Errno::EAGAIN) => return Ok(events),
293                // Recurs with the next read, after the dispatch of these.
294                Err(e) => {
295                    trace!("Unable to read more watch events: {e}");
296                    return Ok(events);
297                }
298            }
299        }
300    }
301
302    /// The reader stopped. The next subscription gets a new watcher. The
303    /// watches get dropped with their senders, so that the channels of the
304    /// subscribers end once they took the events so far, they watch again
305    /// then.
306    fn stopped(watches: &Mutex<Watches>, failed: &AtomicBool) {
307        failed.store(true, Ordering::Release);
308        let watches = std::mem::take(&mut *lock(watches));
309        drop(watches);
310    }
311
312    /// Start watching `path` on behalf of `tx`. The watch gets removed when the
313    /// returned subscription is dropped and no other subscriber of the watch
314    /// is left.
315    pub(super) fn subscribe(
316        self: &Arc<Self>,
317        path: &Path,
318        tx: Sender<WatchEvent>,
319    ) -> Result<Subscription> {
320        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
321        let mut watches = lock(&self.watches);
322
323        // inotify returns the existing descriptor for a file which is watched
324        // already, the subscriber joins its watch then.
325        let wd = self
326            .inotify
327            .add_watch(path, WATCH_FLAGS)
328            .with_context(|| format!("watch {}", path.display()))?;
329        watches
330            .0
331            .entry(wd)
332            .or_insert_with(|| Watch {
333                path: path.into(),
334                subscribers: vec![],
335            })
336            .subscribers
337            .push((id, tx));
338
339        Ok(Subscription {
340            watcher: self.clone(),
341            id,
342            wd,
343        })
344    }
345
346    fn unsubscribe(&self, id: u64, wd: WatchDescriptor) {
347        let mut watches = lock(&self.watches);
348        // The kernel removed the watch already.
349        let Some(watch) = watches.0.get_mut(&wd) else {
350            return;
351        };
352        watch.subscribers.retain(|(i, _)| *i != id);
353        if !watch.subscribers.is_empty() {
354            return;
355        }
356        let Some(watch) = watches.0.remove(&wd) else {
357            return;
358        };
359        if let Err(e) = self.inotify.rm_watch(wd) {
360            // The watch is gone already if the cgroup got removed.
361            trace!("Unable to unwatch {}: {e:#}", watch.path.display());
362        }
363    }
364}
365
366impl Drop for SharedWatcher {
367    fn drop(&mut self) {
368        self.reader.abort();
369    }
370}
371
372/// A subscription to the [`SharedWatcher`] which unsubscribes on drop.
373pub(super) struct Subscription {
374    watcher: Arc<SharedWatcher>,
375    id: u64,
376    wd: WatchDescriptor,
377}
378
379impl Subscription {
380    /// Whether the reader of the watcher stopped, which ended the watch.
381    pub(super) fn watcher_failed(&self) -> bool {
382        self.watcher.failed.load(Ordering::Acquire)
383    }
384
385    /// Give up watching after the reader of the watcher stopped. Only the
386    /// first subscriber of the watcher which does gets true, so that a
387    /// failure gets one warning instead of one per container.
388    pub(super) fn give_up(&self) -> bool {
389        !self.watcher.given_up.swap(true, Ordering::Relaxed)
390    }
391}
392
393/// Ways for the tests of the watches to act like the kernel.
394#[cfg(test)]
395impl SharedWatcher {
396    /// Stop the reader, like a failure to read the events does.
397    pub(super) fn stop(&self) {
398        Self::stopped(&self.watches, &self.failed);
399    }
400
401    /// Remove the watch of a file, which the kernel reports with IN_IGNORED
402    /// only, like for the files of a cgroup which got removed.
403    pub(super) fn remove_watch(&self, path: &Path) -> Result<()> {
404        let wd = lock(&self.watches)
405            .0
406            .iter()
407            .find(|(_, watch)| *watch.path == *path)
408            .map(|(wd, _)| *wd)
409            .context("not watched")?;
410        Ok(self.inotify.rm_watch(wd)?)
411    }
412
413    /// Whether the file is watched.
414    pub(super) fn watches(&self, path: &Path) -> bool {
415        lock(&self.watches)
416            .0
417            .values()
418            .any(|watch| *watch.path == *path)
419    }
420}
421
422impl Drop for Subscription {
423    fn drop(&mut self) {
424        self.watcher.unsubscribe(self.id, self.wd);
425    }
426}
427
428#[cfg(test)]
429mod tests;