Skip to main content

time_persistence/
lib.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Testing-only code for persistent Timekeeper behavior changes around
6//! real-time clock (RTC) handling.
7
8use anyhow::{Result, anyhow};
9use fuchsia_async::{self as fasync, TimeoutExt};
10use fuchsia_runtime::UtcTimeline;
11use futures::TryFutureExt;
12use log::{debug, error};
13use serde::{Deserialize, Serialize};
14use std::path::Path;
15
16/// The path to the internal production persistent state file.
17const PERSISTENT_STATE_PATH: &'static str = "/data/persistent_state.json";
18
19/// Maximum time to wait for a persistent state file read or write operation.
20const PERSISTENT_STATE_IO_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(5);
21
22/// How many reboots should the setting persist for?
23const REBOOT_COUNT: u8 = 1;
24
25/// The persistent Timekeeper state.
26///
27/// See the method documentation for the specific components of the
28/// state that is being persisted.
29#[derive(Serialize, Deserialize, Debug, Default, Clone)]
30pub struct State {
31    // This many reboots remain until we're allowed to write to RTC again. This
32    // counter is decreased by Timekeeper on each restart, until zero is reached.
33    num_reboots_until_rtc_write_allowed: u8,
34
35    // If set to nonzero, this is the reference instant on the boot timeline
36    // used for estimating UTC.
37    //
38    // This reverence value MUST be either from this or the immediately preceding boot.
39    // This means that we must update this value at least once on each boot to
40    // ensure correct handling of the UTC timeline recovery from RTC.
41    #[serde(default)]
42    reference_boot_instant_ns: i64,
43
44    // If set to nonzero, this is the UTC instant corresponding to
45    // `reference_boot_instant_ns` above.
46    #[serde(default)]
47    reference_utc_instant_ns: i64,
48}
49
50impl State {
51    pub fn new(update_rtc: bool) -> Self {
52        let mut value = Self { ..Default::default() };
53        value.set_may_update_rtc(update_rtc);
54        value
55    }
56
57    /// Returns whether updating the RTC clock is allowed.
58    pub fn may_update_rtc(&self) -> bool {
59        self.num_reboots_until_rtc_write_allowed == 0
60    }
61
62    pub fn set_may_update_rtc(&mut self, update_rtc: bool) {
63        self.num_reboots_until_rtc_write_allowed = match update_rtc {
64            true => 0,
65            // Limit the validity of the setting, expressed in number of
66            // reboots.
67            false => REBOOT_COUNT,
68        };
69    }
70
71    /// Gets a known-good reference point on the boot-to-utc affine transform.
72    pub fn get_rtc_reference(&self) -> (zx::BootInstant, zx::Instant<UtcTimeline>) {
73        let reference_boot = zx::BootInstant::from_nanos(self.reference_boot_instant_ns);
74        let reference_utc = zx::Instant::from_nanos(self.reference_utc_instant_ns);
75        (reference_boot, reference_utc)
76    }
77
78    /// Sets a known-good reference point on the boot-to-utc affine transform.
79    ///
80    /// This setting can be used to recover a known-good UTC estimate.
81    ///
82    /// # Args
83    ///
84    /// - `boot_reference`: a reference instant on the boot timeline.
85    /// - `utc_reference`: a reference instant on the UTC timeline that corresponds
86    ///   to `boot_timeline`.
87    pub fn set_rtc_reference(
88        &mut self,
89        boot_reference: zx::BootInstant,
90        utc_reference: zx::Instant<UtcTimeline>,
91    ) {
92        self.reference_boot_instant_ns = boot_reference.into_nanos();
93        self.reference_utc_instant_ns = utc_reference.into_nanos();
94    }
95
96    // Change the reboot limit.
97    fn spend_one_reboot_count(&mut self) {
98        self.num_reboots_until_rtc_write_allowed =
99            self.num_reboots_until_rtc_write_allowed.saturating_sub(1);
100    }
101
102    /// Reads the persistent state, updating the testing-only components.
103    pub async fn read_and_update() -> Result<Self> {
104        Self::read_and_update_internal(PERSISTENT_STATE_PATH).await
105    }
106
107    pub async fn read_and_update_internal<P: AsRef<Path>>(path: P) -> Result<Self> {
108        let path = path.as_ref();
109        let path_str = path.to_str().ok_or_else(|| anyhow!("non-utf8 path: {path:?}"))?;
110        let maybe_state_str: String = fuchsia_fs::file::read_in_namespace_to_string(path_str)
111            .map_err(|e| {
112                debug!("while reading: {e:?}");
113                anyhow::Error::from(e)
114            })
115            .on_timeout(fasync::MonotonicInstant::after(PERSISTENT_STATE_IO_TIMEOUT), || {
116                debug!("timed out reading persistent state from {path:?}");
117                Err(anyhow!("timed out reading persistent state from {path:?}"))
118            })
119            .await?;
120        let state: State = serde_json::from_str(&maybe_state_str).map_err(|e| {
121            debug!("while deserializing: {e:?}");
122            e
123        })?;
124        // TODO(b/478927688): Convert to `debug!` once fix is confirmed.
125        log::info!("read persistent RTC state: {state:?}");
126        let mut next = state.clone();
127        next.spend_one_reboot_count();
128        Self::write_internal(path, &next).await?;
129        Ok(state)
130    }
131
132    /// Write the persistent state to mutable persistent storage.
133    pub async fn write(state: &Self) -> Result<()> {
134        Self::write_internal(PERSISTENT_STATE_PATH, state).await
135    }
136
137    pub async fn write_internal<P: AsRef<Path>>(path: P, state: &Self) -> Result<()> {
138        let path = path.as_ref();
139        let path_str = path.to_str().ok_or_else(|| anyhow!("non-utf8 path: {path:?}"))?;
140        // TODO(b/478927688): Convert to `debug!` once fix is confirmed.
141        log::info!("writing persistent RTC state: {state:?} to {path:?}");
142        let state_str = serde_json::to_string(state).map_err(|e| {
143            error!("while serializing state: {e:?}");
144            e
145        })?;
146        fuchsia_fs::file::write_in_namespace(path_str, state_str)
147            .map_err(|e| {
148                error!("while writing persistent state: {path:?}: {e:?}");
149                anyhow::Error::from(e)
150            })
151            .on_timeout(fasync::MonotonicInstant::after(PERSISTENT_STATE_IO_TIMEOUT), || {
152                error!("timed out writing persistent state: {path:?}");
153                Err(anyhow!("timed out writing persistent state: {path:?}"))
154            })
155            .await
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use fidl_fuchsia_io as fio;
163    use std::pin::pin;
164
165    #[fuchsia::test]
166    async fn test_roundtrip() {
167        let d = tempfile::TempDir::new().expect("tempdir created");
168        let p = d.path().join("file.json");
169        let s = State::new(false);
170        State::write_internal(&p, &s).await.unwrap();
171
172        // First time around, we may not update the RTC.
173        assert!(!State::read_and_update_internal(&p).await.unwrap().may_update_rtc());
174        // Second time around, we may.
175        assert!(State::read_and_update_internal(&p).await.unwrap().may_update_rtc());
176    }
177
178    #[fuchsia::test]
179    fn test_read_and_write_timeout() {
180        let mut executor = fasync::TestExecutor::new_with_fake_time();
181        executor.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
182
183        // Bind a directory in the local namespace whose server channel is held open without
184        // servicing requests so that file operations stall indefinitely until timed out.
185        let (dir_client, _dir_server) = fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
186        let ns = fdio::Namespace::installed().expect("installed namespace");
187        ns.bind("/stalled_persistence_dir", dir_client).expect("bind stalled dir");
188
189        let path = "/stalled_persistence_dir/state.json";
190        let state = State::new(true);
191
192        // 1. Verify write_internal times out after PERSISTENT_STATE_IO_TIMEOUT.
193        {
194            let mut write_fut = pin!(State::write_internal(path, &state));
195            assert!(executor.run_until_stalled(&mut write_fut).is_pending());
196
197            executor.set_fake_time(fasync::MonotonicInstant::after(
198                PERSISTENT_STATE_IO_TIMEOUT - zx::MonotonicDuration::from_millis(1),
199            ));
200            assert!(executor.run_until_stalled(&mut write_fut).is_pending());
201
202            executor.set_fake_time(fasync::MonotonicInstant::after(
203                zx::MonotonicDuration::from_millis(2),
204            ));
205            let res = executor.run_until_stalled(&mut write_fut);
206            match res {
207                std::task::Poll::Ready(Err(err)) => {
208                    assert!(err.to_string().contains("timed out"), "unexpected error: {err:?}");
209                }
210                other => panic!("expected Ready(Err), got {other:?}"),
211            }
212        }
213
214        // 2. Verify read_and_update_internal times out after PERSISTENT_STATE_IO_TIMEOUT.
215        {
216            let mut read_fut = pin!(State::read_and_update_internal(path));
217            assert!(executor.run_until_stalled(&mut read_fut).is_pending());
218
219            executor.set_fake_time(fasync::MonotonicInstant::after(
220                PERSISTENT_STATE_IO_TIMEOUT + zx::MonotonicDuration::from_millis(1),
221            ));
222            let res = executor.run_until_stalled(&mut read_fut);
223            match res {
224                std::task::Poll::Ready(Err(err)) => {
225                    assert!(err.to_string().contains("timed out"), "unexpected error: {err:?}");
226                }
227                other => panic!("expected Ready(Err), got {other:?}"),
228            }
229        }
230
231        let _ = ns.unbind("/stalled_persistence_dir");
232    }
233}