aboutsummaryrefslogtreecommitdiff
path: root/doh/boot_time.rs
blob: 666a44de6b90648e6b748a364fe2b9248262c7d8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! This module provides a time hack to work around the broken `Instant` type in the standard
//! library.
//!
//! `BootTime` looks like `Instant`, but represents `CLOCK_BOOTTIME` instead of `CLOCK_MONOTONIC`.
//! This means the clock increments correctly during suspend.

pub use std::time::Duration;

use std::io;

use futures::future::pending;
use std::convert::TryInto;
use std::fmt;
use std::future::Future;
use std::os::unix::io::{AsRawFd, RawFd};
use tokio::io::unix::AsyncFd;
use tokio::select;

/// Represents a moment in time, with differences including time spent in suspend. Only valid for
/// a single boot - numbers from different boots are incomparable.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BootTime {
    d: Duration,
}

// Return an error with the same structure as tokio::time::timeout to facilitate migration off it,
// and hopefully some day back to it.
/// Error returned by timeout
#[derive(Debug, PartialEq, Eq)]
pub struct Elapsed(());

impl fmt::Display for Elapsed {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        "deadline has elapsed".fmt(fmt)
    }
}

impl std::error::Error for Elapsed {}

impl BootTime {
    /// Gets a `BootTime` representing the current moment in time.
    pub fn now() -> BootTime {
        let mut t = libc::timespec { tv_sec: 0, tv_nsec: 0 };
        // SAFETY: clock_gettime's only action will be to possibly write to the pointer provided,
        // and no borrows exist from that object other than the &mut used to construct the pointer
        // itself.
        if unsafe { libc::clock_gettime(libc::CLOCK_BOOTTIME, &mut t as *mut libc::timespec) } != 0
        {
            panic!(
                "libc::clock_gettime(libc::CLOCK_BOOTTIME) failed: {:?}",
                io::Error::last_os_error()
            );
        }
        BootTime { d: Duration::new(t.tv_sec as u64, t.tv_nsec as u32) }
    }

    /// Determines how long has elapsed since the provided `BootTime`.
    pub fn elapsed(&self) -> Duration {
        BootTime::now().checked_duration_since(*self).unwrap()
    }

    /// Add a specified time delta to a moment in time. If this would overflow the representation,
    /// returns `None`.
    pub fn checked_add(&self, duration: Duration) -> Option<BootTime> {
        Some(BootTime { d: self.d.checked_add(duration)? })
    }

    /// Finds the difference from an earlier point in time. If the provided time is later, returns
    /// `None`.
    pub fn checked_duration_since(&self, earlier: BootTime) -> Option<Duration> {
        self.d.checked_sub(earlier.d)
    }
}

struct TimerFd(RawFd);

impl Drop for TimerFd {
    fn drop(&mut self) {
        // SAFETY: The fd is owned by the TimerFd struct, and no memory access occurs as a result of
        // this call.
        unsafe {
            libc::close(self.0);
        }
    }
}

impl AsRawFd for TimerFd {
    fn as_raw_fd(&self) -> RawFd {
        self.0
    }
}

impl TimerFd {
    fn create() -> io::Result<Self> {
        // SAFETY: This libc call will either give us back a file descriptor or fail, it does not
        // act on memory or resources.
        let raw = unsafe {
            libc::timerfd_create(libc::CLOCK_BOOTTIME, libc::TFD_NONBLOCK | libc::TFD_CLOEXEC)
        };
        if raw < 0 {
            return Err(io::Error::last_os_error());
        }
        Ok(Self(raw))
    }

    fn set(&self, duration: Duration) {
        assert_ne!(duration, Duration::from_millis(0));
        let timer = libc::itimerspec {
            it_interval: libc::timespec { tv_sec: 0, tv_nsec: 0 },
            it_value: libc::timespec {
                tv_sec: duration.as_secs().try_into().unwrap(),
                tv_nsec: duration.subsec_nanos().try_into().unwrap(),
            },
        };
        // SAFETY: We own `timer` and there are no borrows to it other than the pointer we pass to
        // timerfd_settime. timerfd_settime is explicitly documented to handle a null output
        // parameter for its fourth argument by not filling out the output. The fd passed in at
        // self.0 is owned by the `TimerFd` struct, so we aren't breaking anyone else's invariants.
        if unsafe { libc::timerfd_settime(self.0, 0, &timer, std::ptr::null_mut()) } != 0 {
            panic!("timerfd_settime failed: {:?}", io::Error::last_os_error());
        }
    }
}

/// Runs the provided future until completion or `duration` has passed on the `CLOCK_BOOTTIME`
/// clock. In the event of a timeout, returns the elapsed time as an error.
pub async fn timeout<T>(duration: Duration, future: impl Future<Output = T>) -> Result<T, Elapsed> {
    // Ideally, all timeouts in a runtime would share a timerfd. That will be much more
    // straightforwards to implement when moving this functionality into `tokio`.

    // According to timerfd_settime(), setting zero duration will disarm the timer, so
    // we return immediate timeout here.
    // Can't use is_zero() for now because sc-mainline-prod's Rust version is below 1.53.
    if duration == Duration::from_millis(0) {
        return Err(Elapsed(()));
    }

    // The failure conditions for this are rare (see `man 2 timerfd_create`) and the caller would
    // not be able to do much in response to them. When integrated into tokio, this would be called
    // during runtime setup.
    let timer_fd = TimerFd::create().unwrap();
    timer_fd.set(duration);
    let async_fd = AsyncFd::new(timer_fd).unwrap();
    select! {
        v = future => Ok(v),
        _ = async_fd.readable() => Err(Elapsed(())),
    }
}

/// Provides a future which will complete once the provided duration has passed, as measured by the
/// `CLOCK_BOOTTIME` clock.
pub async fn sleep(duration: Duration) {
    assert!(timeout(duration, pending::<()>()).await.is_err());
}

#[test]
fn monotonic_smoke() {
    for _ in 0..1000 {
        // If BootTime is not monotonic, .elapsed() will panic on the unwrap.
        BootTime::now().elapsed();
    }
}

#[test]
fn round_trip() {
    use std::thread::sleep;
    for _ in 0..10 {
        let start = BootTime::now();
        sleep(Duration::from_millis(1));
        let end = BootTime::now();
        let delta = end.checked_duration_since(start).unwrap();
        assert_eq!(start.checked_add(delta).unwrap(), end);
    }
}

#[tokio::test]
async fn timeout_drift() {
    let delta = Duration::from_millis(40);
    for _ in 0..5 {
        let start = BootTime::now();
        assert!(timeout(delta, pending::<()>()).await.is_err());
        let taken = start.elapsed();
        let drift = if taken > delta { taken - delta } else { delta - taken };
        assert!(drift < Duration::from_millis(10));
    }

    for _ in 0..5 {
        let start = BootTime::now();
        sleep(delta).await;
        let taken = start.elapsed();
        let drift = if taken > delta { taken - delta } else { delta - taken };
        assert!(drift < Duration::from_millis(10));
    }
}

#[tokio::test]
async fn timeout_duration_zero() {
    let start = BootTime::now();
    assert!(timeout(Duration::from_millis(0), pending::<()>()).await.is_err());
    let taken = start.elapsed();
    assert!(taken < Duration::from_millis(5));
}