From cbb2c30b561d5e3046a72e03f46f2fb54079017f Mon Sep 17 00:00:00 2001 From: Scott Lamb Date: Mon, 19 Aug 2024 21:04:06 -0700 Subject: [PATCH] use `jiff` crate --- CHANGELOG.md | 1 + guide/install.md | 5 +- guide/troubleshooting.md | 13 ++- server/Cargo.lock | 148 ++++++++++++-------------------- server/Cargo.toml | 5 +- server/base/Cargo.toml | 5 +- server/base/clock.rs | 148 +++++++++++++++++++++----------- server/base/time.rs | 158 +++++++++++++++++++++-------------- server/base/tracing_setup.rs | 16 ++-- server/db/Cargo.toml | 2 +- server/db/days.rs | 136 +++++++++++------------------- server/db/db.rs | 12 +-- server/db/testutil.rs | 8 +- server/db/writer.rs | 44 ++++++---- server/src/cmds/login.rs | 2 +- server/src/cmds/run/mod.rs | 93 ++------------------- server/src/main.rs | 4 +- server/src/mp4.rs | 44 +++++----- server/src/streamer.rs | 38 +++++---- server/src/web/mod.rs | 18 ++-- server/src/web/signals.rs | 2 +- server/src/web/view.rs | 10 +-- 22 files changed, 435 insertions(+), 477 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09db410..7f0c598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ even on minor releases, e.g. `v0.7.5` -> `v0.7.6`. * bump minimum Rust version to 1.81. * improve error message on timeout opening stream. +* use `jiff` for time manipulations. ## v0.7.17 (2024-09-03) diff --git a/guide/install.md b/guide/install.md index 8dc6909..b10659f 100644 --- a/guide/install.md +++ b/guide/install.md @@ -105,8 +105,8 @@ services: # - seccomp:unconfined environment: - # Edit zone below to taste. The `:` is functional. - TZ: ":America/Los_Angeles" + # Edit zone below to taste. + TZ: "America/Los_Angeles" RUST_BACKTRACE: 1 # docker's default log driver won't rotate logs properly, and will throw @@ -323,7 +323,6 @@ After=network-online.target [Service] ExecStart=/usr/local/bin/moonfire-nvr run -Environment=TZ=:/etc/localtime Environment=MOONFIRE_FORMAT=systemd Environment=MOONFIRE_LOG=info Environment=RUST_BACKTRACE=1 diff --git a/guide/troubleshooting.md b/guide/troubleshooting.md index 6eedf21..ccdd25f 100644 --- a/guide/troubleshooting.md +++ b/guide/troubleshooting.md @@ -13,9 +13,10 @@ need more help. * [Docker setup](#docker-setup) * [`"/etc/moonfire-nvr.toml" is a directory`](#etcmoonfire-nvrtoml-is-a-directory) * [`Error response from daemon: unable to find user UID: no matching entries in passwd file`](#error-response-from-daemon-unable-to-find-user-uid-no-matching-entries-in-passwd-file) - * [`clock_gettime failed: EPERM: Operation not permitted`](#clock_gettime-failed-eperm-operation-not-permitted) + * [`clock_gettime(CLOCK_MONOTONIC) failed: EPERM: Operation not permitted`](#clock_gettimeclock_monotonic-failed-eperm-operation-not-permitted) * [`VFS is unable to determine a suitable directory for temporary files`](#vfs-is-unable-to-determine-a-suitable-directory-for-temporary-files) * [Server errors](#server-errors) + * [`unable to get IANA time zone name; check your $TZ and /etc/localtime`](#unable-to-get-iana-time-zone-name-check-your-tz-and-etclocaltime) * [`Error: pts not monotonically increasing; got 26615520 then 26539470`](#error-pts-not-monotonically-increasing-got-26615520-then-26539470) * [Out of disk space](#out-of-disk-space) * [Database or filesystem corruption errors](#database-or-filesystem-corruption-errors) @@ -217,7 +218,7 @@ If Docker produces this error, look at this section of the docker compose setup: user: UID:GID ``` -#### `clock_gettime failed: EPERM: Operation not permitted` +#### `clock_gettime(CLOCK_MONOTONIC) failed: EPERM: Operation not permitted` If commands fail with an error like the following, you're likely running Docker with an overly restrictive `seccomp` setup. [This stackoverflow @@ -227,7 +228,7 @@ the `- seccomp: unconfined` line in your Docker compose file. ```console $ sudo docker compose run --rm moonfire-nvr --version -clock_gettime failed: EPERM: Operation not permitted +clock_gettime(CLOCK_MONOTONIC) failed: EPERM: Operation not permitted This indicates a broken environment. See the troubleshooting guide. ``` @@ -250,6 +251,12 @@ container in your Docker compose file. ### Server errors +#### `unable to get IANA time zone name; check your $TZ and /etc/localtime` + +Moonfire NVR loads the system time zone via the logic described at +[`jiff::tz::TimeZone::system`](https://docs.rs/jiff/0.1.8/jiff/tz/struct.TimeZone.html#method.system) +and expects to be able to get the IANA zone name. + #### `Error: pts not monotonically increasing; got 26615520 then 26539470` If your streams cut out and you see error messages like this one in Moonfire diff --git a/server/Cargo.lock b/server/Cargo.lock index 0b5b422..53aaa76 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -45,21 +45,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anyhow" version = "1.0.86" @@ -224,20 +209,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chrono" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-targets", -] - [[package]] name = "cipher" version = "0.4.4" @@ -282,12 +253,6 @@ dependencies = [ "futures", ] -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "cpufeatures" version = "0.2.13" @@ -386,7 +351,7 @@ dependencies = [ "num", "parking_lot", "serde_json", - "time 0.3.36", + "time", "unicode-segmentation", "unicode-width", "xi-unicode", @@ -678,7 +643,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -899,29 +864,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "iana-time-zone" -version = "0.1.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -984,6 +926,35 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jiff" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb73dbeee753ae9411475ddd8861765fa7f25fe1eebf180c24e1bbabef3fbdcd" +dependencies = [ + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", + "windows-sys 0.59.0", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2cec2f5d266af45a071ece48b1fb89f3b00b2421ac3a5fe10285a6caaa60d3" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a63c62e404e7b92979d2792352d885a7f8f83fd1d0d31eea582d77b2ceca697e" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "js-sys" version = "0.3.70" @@ -1139,7 +1110,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.52.0", ] @@ -1148,9 +1119,9 @@ name = "moonfire-base" version = "0.0.0" dependencies = [ "ahash", - "chrono", "coded", "futures", + "jiff", "libc", "nix", "nom", @@ -1158,7 +1129,6 @@ dependencies = [ "serde", "serde_json", "slab", - "time 0.1.45", "tracing", "tracing-core", "tracing-log", @@ -1178,6 +1148,7 @@ dependencies = [ "h264-reader", "hashlink", "itertools", + "jiff", "libc", "moonfire-base", "nix", @@ -1193,7 +1164,6 @@ dependencies = [ "serde_json", "smallvec", "tempfile", - "time 0.1.45", "tokio", "tracing", "ulid", @@ -1223,6 +1193,7 @@ dependencies = [ "hyper", "hyper-util", "itertools", + "jiff", "libc", "libsystemd", "log", @@ -1245,7 +1216,6 @@ dependencies = [ "serde_json", "smallvec", "tempfile", - "time 0.1.45", "tokio", "tokio-tungstenite", "toml", @@ -1541,6 +1511,21 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1775,9 +1760,9 @@ dependencies = [ [[package]] name = "retina" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd5652758580215edaf590a03298ff72ff5f965c2dea8d6e3f058ef728fbf773" +checksum = "30d83a8e0892d8e0836cd9be6cc413ab3f1c574276e047ac6e27e3027832d00e" dependencies = [ "base64", "bitstream-io", @@ -1786,7 +1771,9 @@ dependencies = [ "h264-reader", "hex", "http-auth", + "jiff", "log", + "memchr", "once_cell", "pin-project", "pretty-hex", @@ -1795,7 +1782,6 @@ dependencies = [ "sdp-types", "smallvec", "thiserror", - "time 0.1.45", "tokio", "tokio-util", "url", @@ -2172,17 +2158,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - [[package]] name = "time" version = "0.3.36" @@ -2598,12 +2573,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2740,15 +2709,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-registry" version = "0.2.0" diff --git a/server/Cargo.toml b/server/Cargo.toml index 4d69dd9..7c67807 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -26,6 +26,7 @@ members = ["base", "db"] base64 = "0.22.0" h264-reader = "0.7.0" itertools = "0.12.0" +jiff = "0.1.8" nix = "0.27.0" pretty-hex = "0.4.0" ring = "0.17.0" @@ -51,6 +52,7 @@ http = "1.1.0" http-serve = { version = "0.4.0-rc.1", features = ["dir"] } hyper = { version = "1.4.1", features = ["http1", "server"] } itertools = { workspace = true } +jiff = { workspace = true, features = ["tz-system"] } libc = "0.2" log = { version = "0.4" } memchr = "2.0.2" @@ -60,13 +62,12 @@ password-hash = "0.5.0" pretty-hex = { workspace = true } protobuf = "3.0" reffers = "0.7.0" -retina = "0.4.9" +retina = "0.4.11" ring = { workspace = true } rusqlite = { workspace = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" smallvec = { version = "1.7", features = ["union"] } -time = "0.1" tokio = { version = "1.24", features = ["macros", "rt-multi-thread", "signal", "sync", "time"] } tokio-tungstenite = "0.23.1" toml = "0.8" diff --git a/server/base/Cargo.toml b/server/base/Cargo.toml index 45d7d14..96e697a 100644 --- a/server/base/Cargo.toml +++ b/server/base/Cargo.toml @@ -15,17 +15,16 @@ path = "lib.rs" [dependencies] ahash = "0.8" -chrono = "0.4.23" coded = { git = "https://github.com/scottlamb/coded", rev = "2c97994974a73243d5dd12134831814f42cdb0e8"} futures = "0.3" +jiff = { workspace = true } libc = "0.2" -nix = { workspace = true } +nix = { workspace = true, features = ["time"] } nom = "7.0.0" rusqlite = { workspace = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" slab = "0.4" -time = "0.1" tracing = { workspace = true } tracing-core = { workspace = true } tracing-log = { workspace = true } diff --git a/server/base/clock.rs b/server/base/clock.rs index 43c9543..9b48d76 100644 --- a/server/base/clock.rs +++ b/server/base/clock.rs @@ -3,28 +3,91 @@ // SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception. //! Clock interface and implementations for testability. +//! +//! Note these types are in a more standard nanosecond-based format, where +//! [`crate::time`] uses Moonfire's 90 kHz time base. -use std::mem; +use nix::sys::time::{TimeSpec, TimeValLike as _}; use std::sync::Mutex; use std::sync::{mpsc, Arc}; use std::thread; -use std::time::Duration as StdDuration; -use time::{Duration, Timespec}; +pub use std::time::Duration; use tracing::warn; use crate::error::Error; use crate::shutdown::ShutdownError; +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct SystemTime(pub TimeSpec); + +impl SystemTime { + pub fn new(sec: nix::sys::time::time_t, nsec: i64) -> Self { + SystemTime(TimeSpec::new(sec, nsec)) + } + + pub fn as_secs(&self) -> i64 { + self.0.num_seconds() + } +} + +impl std::ops::Add for SystemTime { + type Output = SystemTime; + + fn add(self, rhs: Duration) -> SystemTime { + SystemTime(self.0 + TimeSpec::from(rhs)) + } +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Instant(pub TimeSpec); + +impl Instant { + pub fn from_secs(secs: i64) -> Self { + Instant(TimeSpec::seconds(secs)) + } + + pub fn saturating_sub(&self, o: &Instant) -> Duration { + if o > self { + Duration::default() + } else { + Duration::from(self.0 - o.0) + } + } +} + +impl std::fmt::Debug for Instant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +// TODO: should use saturating always? +impl std::ops::Sub for Instant { + type Output = Duration; + + fn sub(self, rhs: Instant) -> Duration { + Duration::from(self.0 - rhs.0) + } +} + +impl std::ops::Add for Instant { + type Output = Instant; + + fn add(self, rhs: Duration) -> Instant { + Instant(self.0 + TimeSpec::from(rhs)) + } +} + /// Abstract interface to the system clocks. This is for testability. pub trait Clocks: Send + Sync + 'static { /// Gets the current time from `CLOCK_REALTIME`. - fn realtime(&self) -> Timespec; + fn realtime(&self) -> SystemTime; /// Gets the current time from a monotonic clock. /// /// On Linux, this uses `CLOCK_BOOTTIME`, which includes suspended time. /// On other systems, it uses `CLOCK_MONOTONIC`. - fn monotonic(&self) -> Timespec; + fn monotonic(&self) -> Instant; /// Causes the current thread to sleep for the specified time. fn sleep(&self, how_long: Duration); @@ -33,7 +96,7 @@ pub trait Clocks: Send + Sync + 'static { fn recv_timeout( &self, rcv: &mpsc::Receiver, - timeout: StdDuration, + timeout: Duration, ) -> Result; } @@ -52,7 +115,7 @@ where Err(e) => e.into(), }; shutdown_rx.check()?; - let sleep_time = Duration::seconds(1); + let sleep_time = Duration::from_secs(1); warn!( exception = %e.chain(), "sleeping for 1 s after error" @@ -64,49 +127,38 @@ where #[derive(Copy, Clone)] pub struct RealClocks {} -impl RealClocks { - fn get(&self, clock: libc::clockid_t) -> Timespec { - unsafe { - let mut ts = mem::MaybeUninit::uninit(); - assert_eq!(0, libc::clock_gettime(clock, ts.as_mut_ptr())); - let ts = ts.assume_init(); - Timespec::new( - // On 32-bit arm builds, `tv_sec` is an `i32` and requires conversion. - // On other platforms, the `.into()` is a no-op. - #[allow(clippy::useless_conversion)] - ts.tv_sec.into(), - ts.tv_nsec as i32, - ) - } - } -} - impl Clocks for RealClocks { - fn realtime(&self) -> Timespec { - self.get(libc::CLOCK_REALTIME) + fn realtime(&self) -> SystemTime { + SystemTime( + nix::time::clock_gettime(nix::time::ClockId::CLOCK_REALTIME) + .expect("clock_gettime(REALTIME) should succeed"), + ) } #[cfg(target_os = "linux")] - fn monotonic(&self) -> Timespec { - self.get(libc::CLOCK_BOOTTIME) + fn monotonic(&self) -> Instant { + Instant( + nix::time::clock_gettime(nix::time::ClockId::CLOCK_BOOTTIME) + .expect("clock_gettime(BOOTTIME) should succeed"), + ) } #[cfg(not(target_os = "linux"))] - fn monotonic(&self) -> Timespec { - self.get(libc::CLOCK_MONOTONIC) + fn monotonic(&self) -> Instant { + Instant( + nix::time::clock_gettime(nix::time::ClockId::CLOCK_MONOTONIC) + .expect("clock_gettime(MONOTONIC) should succeed"), + ) } fn sleep(&self, how_long: Duration) { - match how_long.to_std() { - Ok(d) => thread::sleep(d), - Err(err) => warn!(%err, "invalid duration {:?}", how_long), - }; + thread::sleep(how_long) } fn recv_timeout( &self, rcv: &mpsc::Receiver, - timeout: StdDuration, + timeout: Duration, ) -> Result { rcv.recv_timeout(timeout) } @@ -117,7 +169,7 @@ impl Clocks for RealClocks { pub struct TimerGuard<'a, C: Clocks + ?Sized, S: AsRef, F: FnOnce() -> S + 'a> { clocks: &'a C, label_f: Option, - start: Timespec, + start: Instant, } impl<'a, C: Clocks + ?Sized, S: AsRef, F: FnOnce() -> S + 'a> TimerGuard<'a, C, S, F> { @@ -138,9 +190,9 @@ where { fn drop(&mut self) { let elapsed = self.clocks.monotonic() - self.start; - if elapsed.num_seconds() >= 1 { + if elapsed.as_secs() >= 1 { let label_f = self.label_f.take().unwrap(); - warn!("{} took {}!", label_f().as_ref(), elapsed); + warn!("{} took {:?}!", label_f().as_ref(), elapsed); } } } @@ -150,42 +202,42 @@ where pub struct SimulatedClocks(Arc); struct SimulatedClocksInner { - boot: Timespec, + boot: SystemTime, uptime: Mutex, } impl SimulatedClocks { - pub fn new(boot: Timespec) -> Self { + pub fn new(boot: SystemTime) -> Self { SimulatedClocks(Arc::new(SimulatedClocksInner { boot, - uptime: Mutex::new(Duration::seconds(0)), + uptime: Mutex::new(Duration::from_secs(0)), })) } } impl Clocks for SimulatedClocks { - fn realtime(&self) -> Timespec { + fn realtime(&self) -> SystemTime { self.0.boot + *self.0.uptime.lock().unwrap() } - fn monotonic(&self) -> Timespec { - Timespec::new(0, 0) + *self.0.uptime.lock().unwrap() + fn monotonic(&self) -> Instant { + Instant(TimeSpec::from(*self.0.uptime.lock().unwrap())) } /// Advances the clock by the specified amount without actually sleeping. fn sleep(&self, how_long: Duration) { let mut l = self.0.uptime.lock().unwrap(); - *l = *l + how_long; + *l += how_long; } /// Advances the clock by the specified amount if data is not immediately available. fn recv_timeout( &self, rcv: &mpsc::Receiver, - timeout: StdDuration, + timeout: Duration, ) -> Result { - let r = rcv.recv_timeout(StdDuration::new(0, 0)); + let r = rcv.recv_timeout(Duration::new(0, 0)); if r.is_err() { - self.sleep(Duration::from_std(timeout).unwrap()); + self.sleep(timeout); } r } diff --git a/server/base/time.rs b/server/base/time.rs index 46cb322..4e64567 100644 --- a/server/base/time.rs +++ b/server/base/time.rs @@ -14,24 +14,48 @@ use std::fmt; use std::ops; use std::str::FromStr; +use super::clock::SystemTime; + type IResult<'a, I, O> = nom::IResult>; pub const TIME_UNITS_PER_SEC: i64 = 90_000; +/// The zone to use for all time handling. +/// +/// In normal operation this is assigned from `jiff::tz::TimeZone::system()` at +/// startup, but tests set it to a known political time zone instead. +/// +/// Note that while fresh calls to `jiff::tz::TimeZone::system()` might return +/// new values, this time zone is fixed for the entire run. This is important +/// for `moonfire_db::days::Map`, where it's expected that adding values and +/// then later subtracting them will cancel out. +static GLOBAL_ZONE: std::sync::OnceLock = std::sync::OnceLock::new(); + +pub fn init_zone jiff::tz::TimeZone>(f: F) { + GLOBAL_ZONE.get_or_init(f); +} + +pub fn global_zone() -> jiff::tz::TimeZone { + GLOBAL_ZONE + .get() + .expect("global zone should be initialized") + .clone() +} + /// A time specified as 90,000ths of a second since 1970-01-01 00:00:00 UTC. #[derive(Clone, Copy, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] pub struct Time(pub i64); -/// Returns a parser for a `len`-digit non-negative number which fits into an i32. -fn fixed_len_num<'a>(len: usize) -> impl FnMut(&'a str) -> IResult<'a, &'a str, i32> { +/// Returns a parser for a `len`-digit non-negative number which fits into `T`. +fn fixed_len_num<'a, T: FromStr>(len: usize) -> impl FnMut(&'a str) -> IResult<'a, &'a str, T> { map_res( take_while_m_n(len, len, |c: char| c.is_ascii_digit()), - |input: &str| input.parse::(), + |input: &str| input.parse(), ) } /// Parses `YYYY-mm-dd` into pieces. -fn parse_datepart(input: &str) -> IResult<&str, (i32, i32, i32)> { +fn parse_datepart(input: &str) -> IResult<&str, (i16, i8, i8)> { tuple(( fixed_len_num(4), preceded(tag("-"), fixed_len_num(2)), @@ -40,7 +64,7 @@ fn parse_datepart(input: &str) -> IResult<&str, (i32, i32, i32)> { } /// Parses `HH:MM[:SS[:FFFFF]]` into pieces. -fn parse_timepart(input: &str) -> IResult<&str, (i32, i32, i32, i32)> { +fn parse_timepart(input: &str) -> IResult<&str, (i8, i8, i8, i32)> { let (input, (hr, _, min)) = tuple((fixed_len_num(2), tag(":"), fixed_len_num(2)))(input)?; let (input, stuff) = opt(tuple(( preceded(tag(":"), fixed_len_num(2)), @@ -57,16 +81,16 @@ fn parse_zone(input: &str) -> IResult<&str, i32> { map( tuple(( opt(nom::character::complete::one_of(&b"+-"[..])), - fixed_len_num(2), + fixed_len_num::(2), tag(":"), - fixed_len_num(2), + fixed_len_num::(2), )), |(sign, hr, _, min)| { let off = hr * 3600 + min * 60; if sign == Some('-') { - off - } else { -off + } else { + off } }, ), @@ -77,10 +101,6 @@ impl Time { pub const MIN: Self = Time(i64::MIN); pub const MAX: Self = Time(i64::MAX); - pub fn new(tm: time::Timespec) -> Self { - Time(tm.sec * TIME_UNITS_PER_SEC + tm.nsec as i64 * TIME_UNITS_PER_SEC / 1_000_000_000) - } - /// Parses a time as either 90,000ths of a second since epoch or a RFC 3339-like string. /// /// The former is 90,000ths of a second since 1970-01-01T00:00:00 UTC, excluding leap seconds. @@ -114,38 +134,22 @@ impl Time { ); } let (tm_hour, tm_min, tm_sec, subsec) = opt_time.unwrap_or((0, 0, 0, 0)); - let mut tm = time::Tm { - tm_sec, - tm_min, - tm_hour, - tm_mday, - tm_mon, - tm_year, - tm_wday: 0, - tm_yday: 0, - tm_isdst: -1, - tm_utcoff: 0, - tm_nsec: 0, - }; - if tm.tm_mon == 0 { - bail!(InvalidArgument, msg("time {input:?} has month 0")); - } - tm.tm_mon -= 1; - if tm.tm_year < 1900 { - bail!(InvalidArgument, msg("time {input:?} has year before 1900")); - } - tm.tm_year -= 1900; - - // The time crate doesn't use tm_utcoff properly; it just calls timegm() if tm_utcoff == 0, - // mktime() otherwise. If a zone is specified, use the timegm path and a manual offset. - // If no zone is specified, use the tm_utcoff path. This is pretty lame, but follow the - // chrono crate's lead and just use 0 or 1 to choose between these functions. - let sec = if let Some(off) = opt_zone { - tm.to_timespec().sec + i64::from(off) - } else { - tm.tm_utcoff = 1; - tm.to_timespec().sec - }; + let dt = jiff::civil::DateTime::new(tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, 0) + .map_err(|e| err!(InvalidArgument, source(e)))?; + let tz = + if let Some(off) = opt_zone { + jiff::tz::TimeZone::fixed(jiff::tz::Offset::from_seconds(off).map_err(|e| { + err!(InvalidArgument, msg("invalid time zone offset"), source(e)) + })?) + } else { + global_zone() + }; + let sec = tz + .into_ambiguous_zoned(dt) + .compatible() + .map_err(|e| err!(InvalidArgument, source(e)))? + .timestamp() + .as_second(); Ok(Time(sec * TIME_UNITS_PER_SEC + i64::from(subsec))) } @@ -155,6 +159,18 @@ impl Time { } } +impl From for Time { + fn from(tm: SystemTime) -> Self { + Time(tm.0.tv_sec() * TIME_UNITS_PER_SEC + tm.0.tv_nsec() * 9 / 100_000) + } +} + +impl From for Time { + fn from(tm: jiff::Timestamp) -> Self { + Time((tm.as_nanosecond() * 9 / 100_000) as i64) + } +} + impl std::str::FromStr for Time { type Err = Error; @@ -199,32 +215,39 @@ impl fmt::Debug for Time { impl fmt::Display for Time { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let tm = time::at(time::Timespec { - sec: self.0 / TIME_UNITS_PER_SEC, - nsec: 0, - }); - let zone_minutes = tm.tm_utcoff.abs() / 60; + let tm = jiff::Zoned::new( + jiff::Timestamp::from_second(self.0 / TIME_UNITS_PER_SEC).map_err(|_| fmt::Error)?, + global_zone(), + ); write!( f, - "{}:{:05}{}{:02}:{:02}", - tm.strftime("%FT%T").map_err(|_| fmt::Error)?, + "{}:{:05}{}", + tm.strftime("%FT%T"), self.0 % TIME_UNITS_PER_SEC, - if tm.tm_utcoff > 0 { '+' } else { '-' }, - zone_minutes / 60, - zone_minutes % 60 + tm.strftime("%:z"), ) } } /// A duration specified in 1/90,000ths of a second. -/// Durations are typically non-negative, but a `moonfire_db::db::CameraDayValue::duration` may be -/// negative. +/// Durations are typically non-negative, but a `moonfire_db::db::StreamDayValue::duration` may be +/// negative when used as a `::Change`. #[derive(Clone, Copy, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] pub struct Duration(pub i64); -impl Duration { - pub fn to_tm_duration(&self) -> time::Duration { - time::Duration::nanoseconds(self.0 * 100000 / 9) +impl From for jiff::SignedDuration { + fn from(d: Duration) -> Self { + jiff::SignedDuration::from_nanos(d.0 * 100_000 / 9) + } +} + +impl TryFrom for std::time::Duration { + type Error = std::num::TryFromIntError; + + fn try_from(value: Duration) -> Result { + Ok(std::time::Duration::from_nanos( + u64::try_from(value.0)? * 100_000 / 9, + )) } } @@ -327,6 +350,15 @@ impl ops::SubAssign for Duration { } } +pub mod testutil { + pub fn init_zone() { + super::init_zone(|| { + jiff::tz::TimeZone::get("America/Los_Angeles") + .expect("America/Los_Angeles should exist") + }) + } +} + #[cfg(test)] mod tests { use super::{Duration, Time, TIME_UNITS_PER_SEC}; @@ -334,8 +366,7 @@ mod tests { #[test] fn test_parse_time() { - std::env::set_var("TZ", "America/Los_Angeles"); - time::tzset(); + super::testutil::init_zone(); #[rustfmt::skip] let tests = &[ ("2006-01-02T15:04:05-07:00", 102261550050000), @@ -358,8 +389,7 @@ mod tests { #[test] fn test_format_time() { - std::env::set_var("TZ", "America/Los_Angeles"); - time::tzset(); + super::testutil::init_zone(); assert_eq!( "2006-01-02T15:04:05:00000-08:00", format!("{}", Time(102261874050000)) diff --git a/server/base/tracing_setup.rs b/server/base/tracing_setup.rs index dfae444..7cb1f1a 100644 --- a/server/base/tracing_setup.rs +++ b/server/base/tracing_setup.rs @@ -17,12 +17,18 @@ use tracing_subscriber::{ struct FormatSystemd; -struct ChronoTimer; +struct JiffTimer; -impl FormatTime for ChronoTimer { +impl FormatTime for JiffTimer { fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result { const TIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.6f"; - write!(w, "{}", chrono::Local::now().format(TIME_FORMAT)) + + // Always use the system time zone here, not `base::time::GLOBAL_ZONE`, + // to resolve a chicken-and-egg problem. `jiff::tz::TimeZone::system()` + // may log an error that is worth seeing. Therefore, we install the + // tracing subscriber before initializing `GLOBAL_ZONE`. The latter + // only exists to override the zone for tests anyway. + write!(w, "{}", jiff::Zoned::now().strftime(TIME_FORMAT)) } } @@ -139,7 +145,7 @@ pub fn install() { let sub = tracing_subscriber::registry().with( tracing_subscriber::fmt::Layer::new() .with_writer(std::io::stderr) - .with_timer(ChronoTimer) + .with_timer(JiffTimer) .with_thread_names(true) .with_filter(filter), ); @@ -164,7 +170,7 @@ pub fn install_for_tests() { let sub = tracing_subscriber::registry().with( tracing_subscriber::fmt::Layer::new() .with_test_writer() - .with_timer(ChronoTimer) + .with_timer(JiffTimer) .with_thread_names(true) .with_filter(filter), ); diff --git a/server/db/Cargo.toml b/server/db/Cargo.toml index 4befd6a..85e7505 100644 --- a/server/db/Cargo.toml +++ b/server/db/Cargo.toml @@ -25,6 +25,7 @@ futures = "0.3" h264-reader = { workspace = true } hashlink = "0.9.1" itertools = { workspace = true } +jiff = { workspace = true } libc = "0.2" nix = { workspace = true, features = ["dir", "feature", "fs", "mman"] } num-rational = { version = "0.4.0", default-features = false, features = ["std"] } @@ -38,7 +39,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" smallvec = "1.0" tempfile = "3.2.0" -time = "0.1" tokio = { version = "1.24", features = ["macros", "rt-multi-thread", "sync"] } tracing = { workspace = true } ulid = "1.0.0" diff --git a/server/db/days.rs b/server/db/days.rs index d78244b..06c3fbd 100644 --- a/server/db/days.rs +++ b/server/db/days.rs @@ -5,7 +5,7 @@ //! In-memory indexes by calendar day. use base::time::{Duration, Time, TIME_UNITS_PER_SEC}; -use base::{err, Error}; +use base::Error; use smallvec::SmallVec; use std::cmp; use std::collections::BTreeMap; @@ -20,28 +20,22 @@ use tracing::{error, trace}; pub struct Key(pub(crate) [u8; 10]); impl Key { - fn new(tm: time::Tm) -> Result { + fn new(tm: &jiff::Zoned) -> Result { let mut s = Key([0u8; 10]); - write!( - &mut s.0[..], - "{}", - tm.strftime("%Y-%m-%d") - .map_err(|e| err!(Internal, source(e)))? - )?; + write!(&mut s.0[..], "{}", tm.strftime("%Y-%m-%d"))?; Ok(s) } pub fn bounds(&self) -> Range