Skip to main content

git_zero_rustix/network/server/
deadline.rs

1//! Deadline-aware intake without changing a borrowed descriptor's OS settings.
2use core::time::Duration;
3use git_zero::{Error, Result};
4use rustix::{
5    event::{PollFd, PollFlags, Timespec, poll},
6    fd::AsFd,
7    net::{RecvFlags, SendFlags, recv, send, sockopt},
8    time::{ClockId, clock_gettime},
9};
10
11/// Optional total intake and idle deadlines. Disabled defaults preserve the
12/// blocking API. A positive read resets idle only; total remains absolute.
13/// Zero durations and monotonic-clock arithmetic overflow fail before any read.
14#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
15pub struct RequestDeadlines {
16    pub total: Option<Duration>,
17    pub idle: Option<Duration>,
18}
19
20/// Optional total response and idle-write-progress deadlines. Each response call
21/// gets its own monotonic budget; intake and response budgets are independent.
22pub type ResponseDeadlines = RequestDeadlines;
23
24pub(super) struct DeadlineReader<'a, S: AsFd> {
25    socket: &'a S,
26    deadlines: RequestDeadlines,
27    total_end: Option<Duration>,
28    progress: Duration,
29    socket_timeout: Option<Duration>,
30}
31
32fn now() -> Result<Duration> {
33    let time = clock_gettime(ClockId::Monotonic);
34    Ok(Duration::new(
35        time.tv_sec.try_into().map_err(|_| Error::Backend)?,
36        time.tv_nsec.try_into().map_err(|_| Error::Backend)?,
37    ))
38}
39
40fn end(start: Duration, duration: Option<Duration>) -> Result<Option<Duration>> {
41    duration
42        .map(|value| start.checked_add(value).ok_or(Error::InvalidRequest))
43        .transpose()
44}
45
46impl<'a, S: AsFd> DeadlineReader<'a, S> {
47    pub(super) fn new(socket: &'a S, deadlines: RequestDeadlines) -> Result<Self> {
48        Self::configured(socket, deadlines, sockopt::Timeout::Recv)
49    }
50
51    fn configured(
52        socket: &'a S,
53        deadlines: RequestDeadlines,
54        option: sockopt::Timeout,
55    ) -> Result<Self> {
56        if [deadlines.total, deadlines.idle]
57            .into_iter()
58            .flatten()
59            .any(|value| value.is_zero())
60        {
61            return Err(Error::InvalidRequest);
62        }
63        let start = now()?;
64        let total_end = end(start, deadlines.total)?;
65        end(start, deadlines.idle)?;
66        let socket_timeout = sockopt::socket_timeout(socket, option).map_err(|_| Error::Backend)?;
67        Ok(Self {
68            socket,
69            deadlines,
70            total_end,
71            progress: start,
72            socket_timeout,
73        })
74    }
75
76    fn host_end(&self) -> Result<Option<Duration>> {
77        Ok(self
78            .total_end
79            .into_iter()
80            .chain(end(self.progress, self.deadlines.idle)?)
81            .min())
82    }
83
84    pub(super) fn check(&self) -> Result<()> {
85        let time = now()?;
86        if self.host_end()?.is_some_and(|deadline| time >= deadline) {
87            return Err(Error::LimitExceeded);
88        }
89        Ok(())
90    }
91
92    fn ready(&self, flags: PollFlags, call_end: Option<Duration>) -> Result<()> {
93        loop {
94            let time = now()?;
95            let host_end = self.host_end()?;
96            if host_end.is_some_and(|deadline| time >= deadline) {
97                return Err(Error::LimitExceeded);
98            }
99            if call_end.is_some_and(|deadline| time >= deadline) {
100                return Err(Error::Backend);
101            }
102            let deadline = host_end.into_iter().chain(call_end).min();
103            let remaining = deadline.map(|deadline| deadline.saturating_sub(time));
104            // poll on Darwin uses signed milliseconds. Cap long waits and loop
105            // against the absolute clock; round sub-ms waits up to avoid spins.
106            let wait = remaining.map(|value| {
107                let millis = value
108                    .as_millis()
109                    .saturating_add(u128::from(value.subsec_nanos() % 1_000_000 != 0));
110                let millis = millis.clamp(1, i32::MAX as u128) as u64;
111                Timespec {
112                    tv_sec: (millis / 1000) as _,
113                    tv_nsec: ((millis % 1000) * 1_000_000) as _,
114                }
115            });
116            let mut descriptors = [PollFd::new(self.socket, flags)];
117            match poll(&mut descriptors, wait.as_ref()) {
118                Ok(0) | Err(rustix::io::Errno::INTR) => continue,
119                Ok(_) => return Ok(()),
120                Err(_) => return Err(Error::Backend),
121            }
122        }
123    }
124
125    pub(super) fn receive(&mut self, storage: &mut [u8]) -> Result<usize> {
126        let call_end = end(now()?, self.socket_timeout)?;
127        loop {
128            self.ready(PollFlags::IN, call_end)?;
129            // Readiness can be stale. DONTWAIT bounds the syscall without
130            // mutating O_NONBLOCK or either of the host's socket timeouts.
131            match recv(self.socket, &mut *storage, RecvFlags::DONTWAIT) {
132                Ok((count, _)) => {
133                    self.check()?;
134                    if count > 0 {
135                        self.progress = now()?;
136                    }
137                    return Ok(count);
138                }
139                Err(rustix::io::Errno::INTR | rustix::io::Errno::AGAIN) => continue,
140                Err(_) => return Err(Error::Backend),
141            }
142        }
143    }
144}
145
146/// Uses the same clock and readiness logic with independent send-side settings.
147pub(super) struct DeadlineWriter<'a, S: AsFd>(DeadlineReader<'a, S>);
148
149impl<'a, S: AsFd> DeadlineWriter<'a, S> {
150    pub(super) fn new(socket: &'a S, deadlines: ResponseDeadlines) -> Result<Self> {
151        let state = DeadlineReader::configured(socket, deadlines, sockopt::Timeout::Send)?;
152        #[cfg(target_vendor = "apple")]
153        sockopt::set_socket_nosigpipe(socket, true).map_err(|_| Error::Backend)?;
154        Ok(Self(state))
155    }
156}
157
158impl<S: AsFd> git_zero::io::Write for DeadlineWriter<'_, S> {
159    fn write_all(&mut self, mut bytes: &[u8]) -> Result<()> {
160        self.0.check()?;
161        while !bytes.is_empty() {
162            let call_end = end(now()?, self.0.socket_timeout)?;
163            let count = loop {
164                self.0.ready(PollFlags::OUT, call_end)?;
165                #[cfg(target_vendor = "apple")]
166                let flags = SendFlags::DONTWAIT;
167                #[cfg(not(target_vendor = "apple"))]
168                let flags = SendFlags::DONTWAIT | SendFlags::NOSIGNAL;
169                match send(self.0.socket, bytes, flags) {
170                    Ok(0) => return Err(Error::Backend),
171                    Ok(count) => break count,
172                    Err(rustix::io::Errno::INTR | rustix::io::Errno::AGAIN) => continue,
173                    Err(_) => return Err(Error::Backend),
174                }
175            };
176            self.0.check()?;
177            self.0.progress = now()?;
178            bytes = &bytes[count..];
179        }
180        Ok(())
181    }
182}