Skip to main content

kunobi_daemon/
serve.rs

1//! Accept local connections, authenticate them, admit them, and serve them
2//! until the service drains.
3//!
4//! [`serve`] is the accept loop a daemon would otherwise write itself:
5//!
6//! 1. Accept a connection. Running out of file descriptors pauses accepting
7//!    briefly instead of ending the loop.
8//! 2. Read its [`Credentials`](crate::peer::Credentials) and run the consumer's
9//!    [`Policy`]. A rejected peer is dropped before it can hold any capacity.
10//! 3. Take an [`Admission`] permit from the pool the consumer picks for that
11//!    grant. No capacity drops the connection.
12//! 4. Run the consumer's handler with the [`Authenticated`] connection and its
13//!    permit.
14//!
15//! When [`Lifecycle`] starts draining, the loop stops accepting, gives the
16//! running handlers until the drain budget expires, then aborts the rest and
17//! reports what happened. Handlers still take a [`crate::RequestGuard`] per
18//! operation; [`Lifecycle::unless_draining`] stops a persistent connection
19//! from reading new requests once draining starts.
20//!
21//! Shut down by starting the drain and awaiting [`serve`]. Dropping the
22//! [`serve`] future instead aborts the handlers without waiting for them.
23//!
24//! On Unix the listener is Tokio's `UnixListener`, which
25//! `local::unix_socket::acquire` produces through `from_std`; on Windows it is
26//! `interprocess`'s Tokio listener, which `local::windows_socket::acquire_tokio`
27//! produces.
28
29use crate::{
30    Lifecycle,
31    admission::{Admission, Permit, Pool},
32    local::peer::PeerCredentials,
33    peer::{Authenticated, Policy, authenticate},
34};
35use std::{future::Future, io, sync::Arc, time::Duration};
36use tokio::task::JoinSet;
37
38/// How long to stop accepting after the process runs out of file descriptors.
39/// Accepting again immediately would spin on the same error.
40const DESCRIPTOR_BACKOFF: Duration = Duration::from_millis(50);
41
42/// A listener whose connections can report their peer.
43pub trait Listener {
44    /// The accepted connection.
45    type Connection: PeerCredentials + Send + 'static;
46    /// Wait for the next connection. Must be cancellation-safe: [`serve`]
47    /// drops a pending accept whenever a handler finishes, and a dropped
48    /// accept must not lose a connection.
49    fn accept(&self) -> impl Future<Output = io::Result<Self::Connection>> + Send;
50}
51
52#[cfg(unix)]
53impl Listener for tokio::net::UnixListener {
54    type Connection = tokio::net::UnixStream;
55    async fn accept(&self) -> io::Result<tokio::net::UnixStream> {
56        tokio::net::UnixListener::accept(self)
57            .await
58            .map(|(stream, _)| stream)
59    }
60}
61
62#[cfg(windows)]
63mod windows;
64
65/// What [`serve`] did with the connections it accepted.
66#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
67#[non_exhaustive]
68pub struct Served {
69    /// Connections handed to the handler.
70    pub admitted: u64,
71    /// Connections whose evidence could not be read or whose peer the policy
72    /// rejected.
73    pub unauthenticated: u64,
74    /// Authenticated connections dropped because their pool was full.
75    pub over_capacity: u64,
76    /// Handlers cancelled because they were still running when the drain
77    /// budget expired.
78    pub aborted: u64,
79}
80
81/// Whether an accept error means this process has no file descriptors left,
82/// which is temporary: wait and accept again.
83fn is_descriptor_exhaustion(error: &io::Error) -> bool {
84    // EMFILE and ENFILE, the same numbers on Linux, macOS and the BSDs.
85    cfg!(unix) && matches!(error.raw_os_error(), Some(23 | 24))
86}
87
88/// Configure an accept loop with its listener, lifecycle, admission and peer policy.
89/// Call [`Serve::run`] with the grant-to-pool mapping and connection handler.
90pub fn serve<L, P>(
91    listener: L,
92    lifecycle: Arc<Lifecycle>,
93    admission: Arc<Admission>,
94    policy: P,
95) -> Serve<L, P> {
96    Serve {
97        listener,
98        lifecycle,
99        admission,
100        policy,
101        drain_budget: Duration::from_secs(5),
102    }
103}
104
105/// An accept loop configured with one peer policy and a bounded handler drain.
106pub struct Serve<L, P> {
107    listener: L,
108    lifecycle: Arc<Lifecycle>,
109    admission: Arc<Admission>,
110    policy: P,
111    drain_budget: Duration,
112}
113
114/// An accept failure, retaining the counts after running handlers have drained.
115#[derive(Debug)]
116#[non_exhaustive]
117pub struct ServeError {
118    /// The listener's error.
119    pub error: io::Error,
120    /// Accepted, rejected and aborted connections before the loop ended.
121    pub served: Served,
122}
123
124impl std::fmt::Display for ServeError {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        write!(f, "accept loop failed: {}", self.error)
127    }
128}
129
130impl std::error::Error for ServeError {
131    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
132        Some(&self.error)
133    }
134}
135
136impl<L, P> Serve<L, P> {
137    /// Give running handlers this long to finish after draining starts.
138    /// The default is five seconds. Remaining handlers are then aborted.
139    pub fn drain_budget(mut self, budget: Duration) -> Self {
140        self.drain_budget = budget;
141        self
142    }
143
144    /// Accept, authenticate, admit and serve until the lifecycle drains.
145    ///
146    /// `pool` chooses capacity for a grant; `handler` owns the authenticated
147    /// connection and its permit. An accept failure drains handlers before
148    /// returning [`ServeError`]. Dropping the future aborts handlers immediately.
149    pub async fn run<H, F>(
150        self,
151        pool: impl Fn(&P::Grant) -> Pool,
152        handler: H,
153    ) -> Result<Served, ServeError>
154    where
155        L: Listener,
156        P: Policy,
157        P::Grant: Send + 'static,
158        H: Fn(Authenticated<L::Connection, P::Grant>, Permit) -> F,
159        F: Future<Output = ()> + Send + 'static,
160    {
161        let Self {
162            listener,
163            lifecycle,
164            admission,
165            policy,
166            drain_budget,
167        } = self;
168        let mut served = Served::default();
169        let mut handlers = JoinSet::new();
170        let stopped = 'accept: loop {
171            tokio::select! {
172                biased;
173                () = lifecycle.draining() => break 'accept Ok(()),
174                Some(_) = handlers.join_next(), if !handlers.is_empty() => {}
175                accepted = listener.accept() => match accepted {
176                    Ok(connection) => {
177                        let Ok(evidence) = connection.credentials() else {
178                            served.unauthenticated += 1;
179                            lifecycle.record(crate::observation::Event::Rejected);
180                            continue;
181                        };
182                        let Ok(authenticated) = authenticate(connection, evidence, &policy) else {
183                            served.unauthenticated += 1;
184                            lifecycle.record(crate::observation::Event::Rejected);
185                            continue;
186                        };
187                        let Some(permit) = admission.try_acquire(pool(authenticated.grant())) else {
188                            served.over_capacity += 1;
189                            continue;
190                        };
191                        // Draining can start while this peer is checked.
192                        if !lifecycle.accepting_calls() {
193                            break 'accept Ok(());
194                        }
195                        served.admitted += 1;
196                        handlers.spawn(handler(authenticated, permit));
197                    }
198                    Err(error) if is_descriptor_exhaustion(&error) => {
199                        tokio::select! {
200                            () = lifecycle.draining() => break 'accept Ok(()),
201                            () = descriptor_backoff() => {}
202                        }
203                    }
204                    Err(error) => break 'accept Err(error),
205                },
206            }
207        };
208        served.aborted = finish(&mut handlers, drain_budget).await;
209        stopped
210            .map(|()| served)
211            .map_err(|error| ServeError { error, served })
212    }
213}
214
215#[expect(
216    clippy::disallowed_methods,
217    reason = "No event says a descriptor was freed; pause briefly, then accept again."
218)]
219async fn descriptor_backoff() {
220    tokio::time::sleep(DESCRIPTOR_BACKOFF).await;
221}
222
223/// Give running handlers until `budget` expires, abort the rest, and count the
224/// handlers that were cancelled.
225async fn finish(handlers: &mut JoinSet<()>, budget: Duration) -> u64 {
226    let deadline = tokio::time::Instant::now() + budget;
227    while !handlers.is_empty() {
228        if tokio::time::timeout_at(deadline, handlers.join_next())
229            .await
230            .is_err()
231        {
232            break;
233        }
234    }
235    handlers.abort_all();
236    let mut aborted = 0;
237    while let Some(joined) = handlers.join_next().await {
238        if joined.is_err_and(|error| error.is_cancelled()) {
239            aborted += 1;
240        }
241    }
242    aborted
243}
244
245#[cfg(all(test, unix))]
246mod tests {
247    use super::*;
248    use crate::{
249        ProcessId,
250        admission::Limits,
251        peer::{Credentials, ExpectedProcess, First, SameUser},
252    };
253    use tokio::io::{AsyncReadExt, AsyncWriteExt};
254
255    #[test]
256    fn only_emfile_and_enfile_count_as_running_out_of_descriptors() {
257        assert!(is_descriptor_exhaustion(&io::Error::from_raw_os_error(23)));
258        assert!(is_descriptor_exhaustion(&io::Error::from_raw_os_error(24)));
259        assert!(!is_descriptor_exhaustion(&io::Error::from_raw_os_error(22)));
260        assert!(!is_descriptor_exhaustion(&io::ErrorKind::Other.into()));
261    }
262
263    struct Fixture {
264        _dir: tempfile::TempDir,
265        path: std::path::PathBuf,
266        listener: tokio::net::UnixListener,
267    }
268
269    fn fixture() -> Fixture {
270        let dir = tempfile::tempdir().unwrap();
271        let path = dir.path().join("serve.sock");
272        let listener = tokio::net::UnixListener::bind(&path).unwrap();
273        Fixture {
274            _dir: dir,
275            path,
276            listener,
277        }
278    }
279
280    /// Connect and read until the server closes; returns what it sent.
281    async fn exchange(path: &std::path::Path) -> Vec<u8> {
282        let mut stream = tokio::net::UnixStream::connect(path).await.unwrap();
283        let mut reply = Vec::new();
284        stream.read_to_end(&mut reply).await.unwrap();
285        reply
286    }
287
288    #[tokio::test]
289    async fn an_accepted_peer_is_served_with_its_grant_and_a_permit_from_its_pool() {
290        let Fixture {
291            _dir,
292            path,
293            listener,
294        } = fixture();
295        let lifecycle = Arc::new(Lifecycle::default());
296        // A full grant routed to the application pool would be refused.
297        let admission = Arc::new(Admission::new(Limits {
298            application: 0,
299            ..Limits::default()
300        }));
301        let own = ProcessId::new(std::process::id());
302        let policy = First::new()
303            .then(ExpectedProcess::new(move || own), "full")
304            .then(SameUser, "redacted");
305        let server = tokio::spawn(
306            serve(
307                listener,
308                Arc::clone(&lifecycle),
309                Arc::clone(&admission),
310                policy,
311            )
312            .drain_budget(Duration::from_secs(5))
313            .run(
314                |grant: &&str| {
315                    if *grant == "full" {
316                        Pool::Control
317                    } else {
318                        Pool::Application
319                    }
320                },
321                |mut connection, permit| async move {
322                    let _permit = permit;
323                    let grant = *connection.grant();
324                    connection.write_all(grant.as_bytes()).await.unwrap();
325                },
326            ),
327        );
328        assert_eq!(exchange(&path).await, b"full");
329        assert_eq!(admission.snapshot(Pool::Application).rejected, 0);
330        lifecycle.start_drain();
331        let served = server.await.unwrap().unwrap();
332        assert_eq!(
333            served,
334            Served {
335                admitted: 1,
336                ..Served::default()
337            }
338        );
339    }
340
341    #[tokio::test]
342    async fn a_rejected_peer_is_dropped_before_it_takes_capacity() {
343        let Fixture {
344            _dir,
345            path,
346            listener,
347        } = fixture();
348        let observations = Arc::new(crate::observation::Observations::new(4));
349        let lifecycle = Arc::new(Lifecycle::observed(Arc::clone(&observations)));
350        let admission = Arc::new(Admission::new(Limits::default()));
351        let server = tokio::spawn(
352            serve(
353                listener,
354                Arc::clone(&lifecycle),
355                Arc::clone(&admission),
356                ExpectedProcess::new(|| ProcessId::new(1)),
357            )
358            .drain_budget(Duration::from_secs(5))
359            .run(
360                |_| Pool::Control,
361                |_connection, _permit| async move {
362                    panic!("a rejected peer reached the handler");
363                },
364            ),
365        );
366        assert_eq!(exchange(&path).await, b"");
367        let control = admission.snapshot(Pool::Control);
368        assert_eq!((control.active, control.rejected), (0, 0));
369        assert!(
370            observations
371                .take_events()
372                .iter()
373                .any(|event| event.event == crate::observation::Event::Rejected)
374        );
375        lifecycle.start_drain();
376        let served = server.await.unwrap().unwrap();
377        assert_eq!(served.unauthenticated, 1);
378        assert_eq!(served.admitted, 0);
379    }
380
381    #[tokio::test]
382    async fn a_full_pool_refuses_an_authenticated_peer() {
383        let Fixture {
384            _dir,
385            path,
386            listener,
387        } = fixture();
388        let lifecycle = Arc::new(Lifecycle::default());
389        let admission = Arc::new(Admission::new(Limits {
390            control: 0,
391            ..Limits::default()
392        }));
393        let server = tokio::spawn(
394            serve(
395                listener,
396                Arc::clone(&lifecycle),
397                Arc::clone(&admission),
398                SameUser,
399            )
400            .drain_budget(Duration::from_secs(5))
401            .run(
402                |()| Pool::Control,
403                |_connection, _permit| async move {
404                    panic!("a refused peer reached the handler");
405                },
406            ),
407        );
408        assert_eq!(exchange(&path).await, b"");
409        assert_eq!(admission.snapshot(Pool::Control).rejected, 1);
410        lifecycle.start_drain();
411        let served = server.await.unwrap().unwrap();
412        assert_eq!(served.over_capacity, 1);
413        assert_eq!(served.unauthenticated, 0);
414    }
415
416    #[tokio::test(start_paused = true)]
417    async fn draining_stops_accepting_and_aborts_handlers_past_the_budget() {
418        let Fixture {
419            _dir,
420            path,
421            listener,
422        } = fixture();
423        let lifecycle = Arc::new(Lifecycle::default());
424        let admission = Arc::new(Admission::new(Limits::default()));
425        let (started, mut running) = tokio::sync::mpsc::unbounded_channel();
426        let server = tokio::spawn(
427            serve(listener, Arc::clone(&lifecycle), admission, SameUser)
428                .drain_budget(Duration::from_secs(5))
429                .run(
430                    |()| Pool::Application,
431                    move |_connection, permit| {
432                        let started = started.clone();
433                        async move {
434                            let _permit = permit;
435                            started.send(()).unwrap();
436                            std::future::pending::<()>().await;
437                        }
438                    },
439                ),
440        );
441        let _client = tokio::net::UnixStream::connect(&path).await.unwrap();
442        running.recv().await.unwrap();
443        lifecycle.start_drain();
444        let served = server.await.unwrap().unwrap();
445        assert_eq!(served.admitted, 1);
446        assert_eq!(served.aborted, 1);
447    }
448
449    #[tokio::test(start_paused = true)]
450    async fn a_handler_that_finishes_within_the_budget_is_not_aborted() {
451        let Fixture {
452            _dir,
453            path,
454            listener,
455        } = fixture();
456        let lifecycle = Arc::new(Lifecycle::default());
457        let admission = Arc::new(Admission::new(Limits::default()));
458        let (started, mut running) = tokio::sync::mpsc::unbounded_channel();
459        let finish = Arc::new(tokio::sync::Notify::new());
460        let release = Arc::clone(&finish);
461        let server = tokio::spawn(
462            serve(listener, Arc::clone(&lifecycle), admission, SameUser)
463                .drain_budget(Duration::from_secs(5))
464                .run(
465                    |()| Pool::Application,
466                    move |_connection, permit| {
467                        let started = started.clone();
468                        let finish = Arc::clone(&finish);
469                        async move {
470                            let _permit = permit;
471                            started.send(()).unwrap();
472                            finish.notified().await;
473                        }
474                    },
475                ),
476        );
477        let _client = tokio::net::UnixStream::connect(&path).await.unwrap();
478        running.recv().await.unwrap();
479        lifecycle.start_drain();
480        // Let the loop see the drain, then finish well inside the budget.
481        tokio::task::yield_now().await;
482        release.notify_one();
483        let served = server.await.unwrap().unwrap();
484        assert_eq!(served.admitted, 1);
485        assert_eq!(served.aborted, 0);
486    }
487
488    #[tokio::test]
489    async fn an_accept_failure_keeps_counts_and_its_error_source() {
490        let error = serve(
491            Scripted::new([
492                Ok(Fake(None)),
493                Ok(same_user()),
494                Err(io::ErrorKind::PermissionDenied.into()),
495            ]),
496            Arc::new(Lifecycle::default()),
497            Arc::new(Admission::new(Limits::default())),
498            SameUser,
499        )
500        .run(|()| Pool::Application, |_connection, _permit| async {})
501        .await
502        .unwrap_err();
503        assert_eq!(error.served.unauthenticated, 1);
504        assert_eq!(error.served.admitted, 1);
505        assert_eq!(error.served.over_capacity, 0);
506        assert_eq!(error.served.aborted, 0);
507        assert_eq!(error.error.kind(), io::ErrorKind::PermissionDenied);
508        assert_eq!(
509            error.to_string(),
510            format!("accept loop failed: {}", error.error)
511        );
512        assert!(
513            std::error::Error::source(&error)
514                .unwrap()
515                .downcast_ref::<io::Error>()
516                .is_some()
517        );
518    }
519
520    #[tokio::test(start_paused = true)]
521    async fn the_handler_drain_budget_is_configurable_and_defaults_to_five_seconds() {
522        for budget in [None, Some(Duration::from_secs(13))] {
523            let lifecycle = Arc::new(Lifecycle::default());
524            let (started, mut ready) = tokio::sync::mpsc::unbounded_channel();
525            let configured = serve(
526                Scripted::new([Ok(same_user())]),
527                Arc::clone(&lifecycle),
528                Arc::new(Admission::new(Limits::default())),
529                SameUser,
530            );
531            let configured = match budget {
532                Some(budget) => configured.drain_budget(budget),
533                None => configured,
534            };
535            let server = tokio::spawn(configured.run(
536                |()| Pool::Application,
537                move |_connection, permit| {
538                    let started = started.clone();
539                    async move {
540                        let _permit = permit;
541                        started.send(()).unwrap();
542                        std::future::pending::<()>().await;
543                    }
544                },
545            ));
546            ready.recv().await.unwrap();
547            let began = tokio::time::Instant::now();
548            lifecycle.start_drain();
549            let served = server.await.unwrap().unwrap();
550            assert_eq!(served.aborted, 1);
551            assert_eq!(began.elapsed(), budget.unwrap_or(Duration::from_secs(5)));
552        }
553    }
554
555    /// A connection whose evidence the test decides.
556    struct Fake(Option<Credentials>);
557
558    impl PeerCredentials for Fake {
559        fn credentials(&self) -> io::Result<Credentials> {
560            self.0
561                .clone()
562                .ok_or_else(|| io::Error::other("no peer credentials"))
563        }
564    }
565
566    /// Returns the scripted results in order, then waits forever.
567    struct Scripted(std::sync::Mutex<std::collections::VecDeque<io::Result<Fake>>>);
568
569    impl Scripted {
570        fn new(results: impl IntoIterator<Item = io::Result<Fake>>) -> Self {
571            Self(std::sync::Mutex::new(results.into_iter().collect()))
572        }
573    }
574
575    impl Listener for Scripted {
576        type Connection = Fake;
577        async fn accept(&self) -> io::Result<Fake> {
578            let next = self.0.lock().unwrap().pop_front();
579            match next {
580                Some(result) => result,
581                None => std::future::pending().await,
582            }
583        }
584    }
585
586    fn same_user() -> Fake {
587        Fake(Some(Credentials::new(
588            ProcessId::new(std::process::id()),
589            true,
590        )))
591    }
592
593    /// Serve `listener` with a handler that reports each admission, then drain
594    /// once `admissions` connections were admitted (or right away for zero).
595    async fn serve_scripted(
596        listener: Scripted,
597        policy: impl Policy<Grant = ()> + Send + 'static,
598        lifecycle: Arc<Lifecycle>,
599        admissions: usize,
600    ) -> Result<Served, ServeError> {
601        let (admitted, mut seen) = tokio::sync::mpsc::unbounded_channel();
602        let server = tokio::spawn(
603            serve(
604                listener,
605                Arc::clone(&lifecycle),
606                Arc::new(Admission::new(Limits::default())),
607                policy,
608            )
609            .drain_budget(Duration::from_secs(5))
610            .run(
611                |()| Pool::Application,
612                move |_connection, _permit| {
613                    let admitted = admitted.clone();
614                    async move {
615                        let _ = admitted.send(());
616                    }
617                },
618            ),
619        );
620        for _ in 0..admissions {
621            seen.recv().await.unwrap();
622        }
623        tokio::task::yield_now().await;
624        lifecycle.start_drain();
625        server.await.unwrap()
626    }
627
628    #[tokio::test]
629    async fn unreadable_evidence_rejects_the_connection() {
630        let served = serve_scripted(
631            Scripted::new([Ok(Fake(None)), Ok(same_user())]),
632            SameUser,
633            Arc::new(Lifecycle::default()),
634            1,
635        )
636        .await
637        .unwrap();
638        assert_eq!(
639            (
640                served.unauthenticated,
641                served.admitted,
642                served.over_capacity
643            ),
644            (1, 1, 0)
645        );
646    }
647
648    #[tokio::test(start_paused = true)]
649    async fn running_out_of_descriptors_pauses_accepting_then_continues() {
650        let started = tokio::time::Instant::now();
651        let served = serve_scripted(
652            Scripted::new([Err(io::Error::from_raw_os_error(24)), Ok(same_user())]),
653            SameUser,
654            Arc::new(Lifecycle::default()),
655            1,
656        )
657        .await
658        .unwrap();
659        assert_eq!(served.admitted, 1);
660        assert!(started.elapsed() >= DESCRIPTOR_BACKOFF);
661    }
662
663    #[tokio::test]
664    async fn any_other_accept_error_stops_the_loop_and_is_returned() {
665        let error = serve_scripted(
666            Scripted::new([Err(io::Error::from(io::ErrorKind::PermissionDenied))]),
667            SameUser,
668            Arc::new(Lifecycle::default()),
669            0,
670        )
671        .await
672        .unwrap_err();
673        assert_eq!(error.error.kind(), io::ErrorKind::PermissionDenied);
674    }
675
676    #[tokio::test]
677    async fn a_connection_checked_while_draining_starts_is_not_handed_out() {
678        struct DrainsWhileChecking(Arc<Lifecycle>);
679        impl Policy for DrainsWhileChecking {
680            type Grant = ();
681            fn grant(&self, _: &Credentials) -> Result<(), crate::peer::Rejected> {
682                self.0.start_drain();
683                Ok(())
684            }
685        }
686        let lifecycle = Arc::new(Lifecycle::default());
687        let served = serve_scripted(
688            Scripted::new([Ok(same_user())]),
689            DrainsWhileChecking(Arc::clone(&lifecycle)),
690            lifecycle,
691            0,
692        )
693        .await
694        .unwrap();
695        assert_eq!(served.admitted, 0);
696    }
697
698    #[tokio::test]
699    async fn unless_draining_stops_a_read_once_draining_starts() {
700        let lifecycle = Lifecycle::default();
701        assert_eq!(lifecycle.unless_draining(async { 7 }).await, Some(7));
702        lifecycle.start_drain();
703        assert_eq!(
704            lifecycle
705                .unless_draining(std::future::pending::<()>())
706                .await,
707            None
708        );
709    }
710}