Skip to main content

kunobi_daemon/
observation.rs

1//! Local observations. Consumers own logging, metrics, export and retention.
2//!
3//! Snapshots never take an I/O lock. Events have a fixed capacity and producers
4//! never wait for the reader: contention or a full queue increments `lost_events`.
5//! No callback, background worker, payload or credential is retained here.
6use std::{
7    collections::VecDeque,
8    io::{self, Read, Write},
9    sync::{
10        Arc, Mutex,
11        atomic::{AtomicU64, AtomicUsize, Ordering},
12    },
13    time::{Duration, Instant},
14};
15
16/// Direction of a transport operation.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum Direction {
19    /// Read from the peer.
20    Read,
21    /// Write or flush to the peer.
22    Write,
23}
24/// Bounded event values; no free-form message or client identity.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26#[non_exhaustive]
27pub enum Event {
28    /// An observed transport was wrapped.
29    Connected,
30    /// An observed transport was dropped.
31    Disconnected,
32    /// The peer ended its output stream.
33    ReadClosed,
34    /// An I/O operation failed.
35    IoFailed {
36        /// Failed direction.
37        direction: Direction,
38        /// OS-independent error category.
39        kind: io::ErrorKind,
40    },
41    /// Negotiation or authentication was refused by the caller.
42    Rejected,
43    /// Admission was closed.
44    DrainStarted,
45    /// All admitted work completed after drain began.
46    DrainCompleted,
47    /// A drain deadline expired, without cancelling active work.
48    DrainTimedOut,
49    /// The consumer began a cooperative handoff.
50    HandoffStarted,
51    /// The consumer committed a cooperative handoff.
52    HandoffCompleted,
53    /// The consumer abandoned a cooperative handoff.
54    HandoffAborted,
55}
56/// An event with monotonic time relative to this observation instance.
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58#[non_exhaustive]
59pub struct TimedEvent {
60    /// Time since construction.
61    pub elapsed: Duration,
62    /// Event category.
63    pub event: Event,
64}
65
66#[derive(Default)]
67struct Progress {
68    bytes: AtomicU64,
69    pending: AtomicUsize,
70    busy_since: AtomicU64,
71    last_progress: AtomicU64,
72    errors: AtomicU64,
73}
74/// Progress in one direction. Samples are approximate during concurrent I/O.
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76#[non_exhaustive]
77pub struct ProgressSnapshot {
78    /// Bytes accepted by successful transport calls, not application acknowledgements.
79    pub bytes: u64,
80    /// I/O calls currently in progress. This does not count application jobs.
81    pub pending: usize,
82    /// Time since this direction became continuously busy; not a job deadline.
83    pub busy_for: Option<Duration>,
84    /// Time since the last nonempty transfer; `None` until the first transfer.
85    pub since_progress: Option<Duration>,
86    /// Failed I/O calls, excluding Interrupted and WouldBlock.
87    pub errors: u64,
88}
89/// Transport observations aggregated over wrappers sharing this instance.
90#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91#[non_exhaustive]
92pub struct Snapshot {
93    /// Currently owned observed transports.
94    pub connections: usize,
95    /// Input progress.
96    pub read: ProgressSnapshot,
97    /// Output progress.
98    pub write: ProgressSnapshot,
99    /// Events discarded due to capacity or reader contention.
100    pub lost_events: u64,
101}
102/// Shared local data. Attach it only where the consumer needs observations.
103pub struct Observations {
104    started: Instant,
105    read: Progress,
106    write: Progress,
107    connections: AtomicUsize,
108    events: Mutex<VecDeque<TimedEvent>>,
109    event_capacity: usize,
110    lost_events: AtomicU64,
111}
112impl Observations {
113    /// Create a bounded event queue. Zero disables event recording; counters remain active.
114    pub fn new(event_capacity: usize) -> Self {
115        Self {
116            started: Instant::now(),
117            read: Progress::default(),
118            write: Progress::default(),
119            connections: AtomicUsize::new(0),
120            events: Mutex::new(VecDeque::with_capacity(event_capacity)),
121            event_capacity,
122            lost_events: AtomicU64::new(0),
123        }
124    }
125    /// Record a lifecycle event without waiting for the event consumer.
126    pub fn record(&self, event: Event) {
127        if self.event_capacity == 0 {
128            return;
129        }
130        if let Ok(mut events) = self.events.try_lock()
131            && events.len() < self.event_capacity
132        {
133            events.push_back(TimedEvent {
134                elapsed: self.started.elapsed(),
135                event,
136            });
137            return;
138        }
139        self.lost_events.fetch_add(1, Ordering::Relaxed);
140    }
141    /// Consume queued events in order. Other consumers share this queue.
142    pub fn take_events(&self) -> Vec<TimedEvent> {
143        self.events
144            .lock()
145            .unwrap_or_else(|e| e.into_inner())
146            .drain(..)
147            .collect()
148    }
149    /// Read counters without acquiring an I/O or event lock.
150    pub fn snapshot(&self) -> Snapshot {
151        let progress = |p: &Progress| {
152            let last = p.last_progress.load(Ordering::Relaxed);
153            let pending = p.pending.load(Ordering::Relaxed);
154            ProgressSnapshot {
155                bytes: p.bytes.load(Ordering::Relaxed),
156                pending,
157                busy_for: (pending != 0).then(|| {
158                    self.started
159                        .elapsed()
160                        .saturating_sub(Duration::from_nanos(p.busy_since.load(Ordering::Relaxed)))
161                }),
162                since_progress: (last != 0).then(|| {
163                    self.started
164                        .elapsed()
165                        .saturating_sub(Duration::from_nanos(last - 1))
166                }),
167                errors: p.errors.load(Ordering::Relaxed),
168            }
169        };
170        Snapshot {
171            connections: self.connections.load(Ordering::Relaxed),
172            read: progress(&self.read),
173            write: progress(&self.write),
174            lost_events: self.lost_events.load(Ordering::Relaxed),
175        }
176    }
177    fn progress(&self, direction: Direction) -> &Progress {
178        match direction {
179            Direction::Read => &self.read,
180            Direction::Write => &self.write,
181        }
182    }
183    fn start(&self, direction: Direction) {
184        let progress = self.progress(direction);
185        if progress.pending.fetch_add(1, Ordering::Relaxed) == 0 {
186            progress.busy_since.store(
187                self.started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
188                Ordering::Relaxed,
189            );
190        }
191    }
192    fn begin(&self, direction: Direction) -> Pending<'_> {
193        self.start(direction);
194        Pending(&self.progress(direction).pending)
195    }
196    fn transferred(&self, direction: Direction, bytes: usize) {
197        if bytes == 0 {
198            return;
199        }
200        let p = self.progress(direction);
201        p.bytes.fetch_add(bytes as u64, Ordering::Relaxed);
202        let elapsed = self
203            .started
204            .elapsed()
205            .as_nanos()
206            .min(u128::from(u64::MAX - 1)) as u64;
207        p.last_progress.fetch_max(elapsed + 1, Ordering::Relaxed);
208    }
209    fn failed(&self, direction: Direction, error: &io::Error) {
210        if matches!(
211            error.kind(),
212            io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock
213        ) {
214            return;
215        }
216        self.progress(direction)
217            .errors
218            .fetch_add(1, Ordering::Relaxed);
219        self.record(Event::IoFailed {
220            direction,
221            kind: error.kind(),
222        });
223    }
224}
225impl Default for Observations {
226    fn default() -> Self {
227        Self::new(64)
228    }
229}
230
231struct Pending<'a>(&'a AtomicUsize);
232impl Drop for Pending<'_> {
233    fn drop(&mut self) {
234        self.0.fetch_sub(1, Ordering::Relaxed);
235    }
236}
237
238/// Blocking I/O adapter. It preserves partial transfers, errors and backpressure.
239/// Timeouts, authentication and cancellation remain the transport's responsibility.
240pub struct ObservedIo<S> {
241    inner: S,
242    observations: Arc<Observations>,
243    read_closed: bool,
244}
245impl<S> ObservedIo<S> {
246    /// Wrap one connection and retain a separate observation handle for readers.
247    pub fn new(inner: S, observations: Arc<Observations>) -> Self {
248        observations.connections.fetch_add(1, Ordering::Relaxed);
249        observations.record(Event::Connected);
250        Self {
251            inner,
252            observations,
253            read_closed: false,
254        }
255    }
256    /// Access transport configuration. I/O through this reference bypasses counters.
257    pub fn inner_mut(&mut self) -> &mut S {
258        &mut self.inner
259    }
260}
261impl<S> Drop for ObservedIo<S> {
262    fn drop(&mut self) {
263        self.observations
264            .connections
265            .fetch_sub(1, Ordering::Relaxed);
266        self.observations.record(Event::Disconnected);
267    }
268}
269impl<S: Read> Read for ObservedIo<S> {
270    fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
271        let _pending = self.observations.begin(Direction::Read);
272        let result = self.inner.read(bytes);
273        match &result {
274            Ok(0) if !bytes.is_empty() && !self.read_closed => {
275                self.read_closed = true;
276                self.observations.record(Event::ReadClosed);
277            }
278            Ok(n) => self.observations.transferred(Direction::Read, *n),
279            Err(error) => self.observations.failed(Direction::Read, error),
280        }
281        result
282    }
283}
284impl<S: Write> Write for ObservedIo<S> {
285    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
286        let _pending = self.observations.begin(Direction::Write);
287        let result = self.inner.write(bytes);
288        match &result {
289            Ok(n) => self.observations.transferred(Direction::Write, *n),
290            Err(error) => self.observations.failed(Direction::Write, error),
291        }
292        result
293    }
294    fn flush(&mut self) -> io::Result<()> {
295        let _pending = self.observations.begin(Direction::Write);
296        let result = self.inner.flush();
297        if let Err(error) = &result {
298            self.observations.failed(Direction::Write, error);
299        }
300        result
301    }
302}
303
304#[cfg(feature = "wire-async")]
305mod asynchronous {
306    use super::*;
307    use std::{
308        pin::Pin,
309        task::{Context, Poll},
310    };
311    use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
312
313    /// Async adapter with the same counters as [`ObservedIo`].
314    /// A Pending poll remains observable until a later completion or adapter drop.
315    /// Cancelling a caller's future alone does not prove that transport I/O stopped.
316    pub struct AsyncObservedIo<S> {
317        io: ObservedIo<S>,
318        read_pending: bool,
319        write_pending: bool,
320    }
321    impl<S> AsyncObservedIo<S> {
322        /// Wrap an async transport without spawning any task.
323        pub fn new(inner: S, observations: Arc<Observations>) -> Self {
324            Self {
325                io: ObservedIo::new(inner, observations),
326                read_pending: false,
327                write_pending: false,
328            }
329        }
330        /// Configure the inner transport; direct I/O bypasses observations.
331        pub fn inner_mut(&mut self) -> &mut S {
332            self.io.inner_mut()
333        }
334        fn begin(&mut self, direction: Direction) {
335            let pending = match direction {
336                Direction::Read => &mut self.read_pending,
337                Direction::Write => &mut self.write_pending,
338            };
339            if !*pending {
340                // The counter spans Pending polls; end() or Drop releases it.
341                self.io.observations.start(direction);
342                *pending = true;
343            }
344        }
345        fn end(&mut self, direction: Direction) {
346            let pending = match direction {
347                Direction::Read => &mut self.read_pending,
348                Direction::Write => &mut self.write_pending,
349            };
350            if std::mem::take(pending) {
351                self.io
352                    .observations
353                    .progress(direction)
354                    .pending
355                    .fetch_sub(1, Ordering::Relaxed);
356            }
357        }
358    }
359    impl<S> Drop for AsyncObservedIo<S> {
360        fn drop(&mut self) {
361            self.end(Direction::Read);
362            self.end(Direction::Write);
363        }
364    }
365    impl<S: AsyncRead + Unpin> AsyncRead for AsyncObservedIo<S> {
366        fn poll_read(
367            self: Pin<&mut Self>,
368            cx: &mut Context<'_>,
369            buf: &mut ReadBuf<'_>,
370        ) -> Poll<io::Result<()>> {
371            let this = self.get_mut();
372            this.begin(Direction::Read);
373            let before = buf.filled().len();
374            let capacity = buf.remaining();
375            let result = Pin::new(&mut this.io.inner).poll_read(cx, buf);
376            if let Poll::Ready(result) = &result {
377                this.end(Direction::Read);
378                match result {
379                    Ok(()) => {
380                        let bytes = buf.filled().len() - before;
381                        this.io.observations.transferred(Direction::Read, bytes);
382                        if bytes == 0 && capacity != 0 && !this.io.read_closed {
383                            this.io.read_closed = true;
384                            this.io.observations.record(Event::ReadClosed);
385                        }
386                    }
387                    Err(error) => this.io.observations.failed(Direction::Read, error),
388                }
389            }
390            result
391        }
392    }
393    impl<S: AsyncWrite + Unpin> AsyncWrite for AsyncObservedIo<S> {
394        fn poll_write(
395            self: Pin<&mut Self>,
396            cx: &mut Context<'_>,
397            bytes: &[u8],
398        ) -> Poll<io::Result<usize>> {
399            let this = self.get_mut();
400            this.begin(Direction::Write);
401            let result = Pin::new(&mut this.io.inner).poll_write(cx, bytes);
402            if let Poll::Ready(result) = &result {
403                this.end(Direction::Write);
404                match result {
405                    Ok(bytes) => this.io.observations.transferred(Direction::Write, *bytes),
406                    Err(error) => this.io.observations.failed(Direction::Write, error),
407                }
408            }
409            result
410        }
411        fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
412            let this = self.get_mut();
413            this.begin(Direction::Write);
414            let result = Pin::new(&mut this.io.inner).poll_flush(cx);
415            if let Poll::Ready(result) = &result {
416                this.end(Direction::Write);
417                if let Err(error) = result {
418                    this.io.observations.failed(Direction::Write, error);
419                }
420            }
421            result
422        }
423        fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
424            let this = self.get_mut();
425            this.begin(Direction::Write);
426            let result = Pin::new(&mut this.io.inner).poll_shutdown(cx);
427            if let Poll::Ready(result) = &result {
428                this.end(Direction::Write);
429                if let Err(error) = result {
430                    this.io.observations.failed(Direction::Write, error);
431                }
432            }
433            result
434        }
435    }
436}
437#[cfg(feature = "wire-async")]
438pub use asynchronous::AsyncObservedIo;
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443    #[test]
444    fn a_busy_event_reader_does_not_block_producers_or_snapshots() {
445        let observations = Observations::new(1);
446        let _reader = observations.events.lock().unwrap();
447        observations.record(Event::Rejected);
448        assert_eq!(observations.snapshot().lost_events, 1);
449    }
450}