diff --git a/Cargo.lock b/Cargo.lock
index 9f08a62c9..5189bb234 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4629,8 +4629,10 @@ name = "tor-hsservice"
version = "0.2.3"
dependencies = [
"async-trait",
+ "derive-adhoc",
"educe",
"futures",
+ "itertools",
"once_cell",
"postage",
"rand 0.8.5",
diff --git a/crates/tor-hsservice/Cargo.toml b/crates/tor-hsservice/Cargo.toml
index 7051834ed..2f5e958d1 100644
--- a/crates/tor-hsservice/Cargo.toml
+++ b/crates/tor-hsservice/Cargo.toml
@@ -26,8 +26,10 @@ full = [
[dependencies]
async-trait = "0.1.54"
+derive-adhoc = "0.7.3"
educe = "0.4.6"
futures = "0.3.14"
+itertools = "0.11.0"
once_cell = "1"
postage = { version = "0.5.0", default-features = false, features = ["futures-traits"] }
rand = "0.8.5"
diff --git a/crates/tor-hsservice/src/lib.rs b/crates/tor-hsservice/src/lib.rs
index 6917af456..271da9cb5 100644
--- a/crates/tor-hsservice/src/lib.rs
+++ b/crates/tor-hsservice/src/lib.rs
@@ -48,6 +48,7 @@ mod keys;
mod req;
mod status;
mod svc;
+mod timeout_track;
pub use config::OnionServiceConfig;
pub use err::{ClientError, FatalError, StartupError};
diff --git a/crates/tor-hsservice/src/timeout_track.rs b/crates/tor-hsservice/src/timeout_track.rs
new file mode 100644
index 000000000..817134ab8
--- /dev/null
+++ b/crates/tor-hsservice/src/timeout_track.rs
@@ -0,0 +1,431 @@
+//! Utilities to track and compare times and timeouts
+//!
+//! Contains [`TrackingNow`], and variants.
+//!
+//! Each one records the current time,
+//! and can be used to see if prospective timeouts have expired yet,
+//! via the [`PartialOrd`] implementations.
+//!
+//! Each can be compared with a prospective wakeup time via a `.cmp()` method,
+//! and via implementations of [`PartialOrd`] (including via `<` operators etc.)
+//!
+//! Each tracks every such comparison,
+//! and can yield the earliest timeout that was asked about.
+//!
+//! Each has interior mutability,
+//! which is necessary because `PartialOrd` (`<=` etc.) only passes immutable references.
+//! Most are `Send`, none are `Sync`,
+//! so use in thread-safe async code is somewhat restricted.
+//! (Recommended use is to do all work influencing timeout calculations synchronously;
+//! otherwise, in any case, you risk the time advancing mid-calculations.)
+//!
+//! `Clone` gives you a *copy*, not a handle onto the same tracker.
+//! Comparisons done with the clone do not update the original.
+//! (Exception: `TrackingInstantOffsetNow::clone`.)
+//!
+//! The types are:
+//!
+//! * [`TrackingNow`]: tracks timeouts based on both [`SystemTime` and `Instant`],
+//! * [`TrackingSystemTimeNow`]: tracks timeouts based on [`SystemTime`]
+//! * [`TrackingInstantNow`]: tracks timeouts based on [`Instant`]
+//! * [`TrackingInstantOffsetNow`]: `InstantTrackingNow` but with an offset applied
+
+#![allow(unreachable_pub)] // TODO - eventually we hope this will become pub, in another crate
+
+use std::cell::Cell;
+use std::cmp::Ordering;
+use std::time::{Duration, Instant, SystemTime};
+
+use derive_adhoc::{define_derive_adhoc, Adhoc};
+use futures::{future, select_biased, FutureExt as _};
+use itertools::chain;
+
+use tor_rtcompat::{SleepProvider, SleepProviderExt as _};
+
+//========== derive-adhoc macros, which must come first ==========
+
+define_derive_adhoc! {
+ /// Defines methods and types which are common to trackers for `Instant` and `SystemTime`
+ SingleTimeoutTracker for struct, expect items =
+
+ // type of the `now` field, ie the absolute time type
+ ${define NOW $(
+ ${when approx_equal($fname, now)}
+ $ftype
+ ) }
+
+ // type that we track, ie the inner contents of the `Cell>`
+ ${define TRACK ${tmeta(track)}}
+
+ // TODO maybe some of this should be a trait? But that would probably include
+ // wait_for_earliest, which would be an async trait method and quite annoying.
+ impl $ttype {
+ /// Creates a new timeout tracker, given a value for the current time
+ pub fn new(now: $NOW) -> Self {
+ Self {
+ now,
+ earliest: None.into(),
+ }
+ }
+
+ /// Creates a new timeout tracker from the current time as seen by a runtime
+ pub fn now(r: &impl SleepProvider) -> Self {
+ let now = r.${tmeta(from_runtime)}();
+ Self::new(now)
+ }
+
+ /// Return the "current time" value in use
+ ///
+ /// If you do comparisons with this, they won't be tracked, obviously.
+ pub fn get_now_untracked(&self) -> $NOW {
+ self.now
+ }
+
+ /// Core of a tracked update: updates `earliest` with `maybe_earlier`
+ fn update_inner(earliest: &Cell >, maybe_earlier: $TRACK) {
+ earliest.set(chain!(
+ earliest.take(),
+ [maybe_earlier],
+ ).min())
+ }
+ }
+}
+
+define_derive_adhoc! {
+ /// Impls for `TrackingNow`, the combined tracker
+ ///
+ /// Defines just the methods which want to abstract over fields
+ CombinedTimeoutTracker for struct, expect items =
+
+ ${define NOW ${fmeta(now)}}
+
+ impl $ttype {
+ /// Creates a new combined timeout tracker, given values for the current time
+ pub fn new( $(
+ $fname: $NOW,
+ ) ) -> $ttype {
+ $ttype { $( $fname: $ftype::new($fname), ) }
+ }
+
+ /// Creates a new timeout tracker from the current times as seen by a runtime
+ pub fn now(r: &impl SleepProvider) -> Self {
+ $ttype { $(
+ $fname: $ftype::now(r),
+ ) }
+ }
+
+ $(
+ #[doc = concat!("Access the specific timeout tracker for [`", stringify!($NOW), "`]")]
+ pub fn $fname(&self) -> &$ftype {
+ &self.$fname
+ }
+ )
+ }
+
+ $(
+ define_PartialOrd_via_cmp! { $ttype, $NOW, .$fname }
+ )
+}
+
+define_derive_adhoc! {
+ /// Defines `wait_for_earliest`
+ ///
+ /// Combined into this macro mostly so we only have to write the docs once
+ WaitForEarliest for struct, expect items =
+
+ impl $ttype {
+ /// Wait for the earliest timeout implied by any of the comparisons
+ ///
+ /// Waits until the earliest time at which any of the comparisons performed
+ /// might change their answer.
+ ///
+ /// If there were no comparisons there are no timeouts, so we wait forever.
+ pub async fn wait_for_earliest(self, runtime: &impl SleepProvider) {
+ ${if tmeta(runtime_sleep) {
+ // tracker for a single kind of time
+ match self.earliest.into_inner() {
+ None => future::pending().await,
+ Some(earliest) => runtime.${tmeta(runtime_sleep)}(earliest).await,
+ }
+ } else {
+ // combined tracker, wait for earliest of any kind of timeout
+ select_biased! { $(
+ () = self.$fname.wait_for_earliest(runtime).fuse() => {},
+ ) }
+ }}
+ }
+ }
+}
+
+/// `impl PartialOrd<$NOW> for $ttype` in terms of `...$field.cmp()`
+macro_rules! define_PartialOrd_via_cmp { {
+ $ttype:ty, $NOW:ty, $( $field:tt )*
+} => {
+ /// Check if time `t` has been reached yet (and remember that we want to wake up then)
+ ///
+ /// Always returns `Some`.
+ impl PartialEq<$NOW> for $ttype {
+ fn eq(&self, t: &$NOW) -> bool {
+ self $($field)* .cmp(*t) == Ordering::Equal
+ }
+ }
+
+ /// Check if time `t` has been reached yet (and remember that we want to wake up then)
+ ///
+ /// Always returns `Some`.
+ impl PartialOrd<$NOW> for $ttype {
+ fn partial_cmp(&self, t: &$NOW) -> Option {
+ Some(self $($field)* .cmp(*t))
+ }
+ }
+
+ /// Check if we have reached time `t` yet (and remember that we want to wake up then)
+ ///
+ /// Always returns `Some`.
+ impl PartialEq<$ttype> for $NOW {
+ fn eq(&self, t: &$ttype) -> bool {
+ t.eq(self)
+ }
+ }
+
+ /// Check if we have reached time `t` yet (and remember that we want to wake up then)
+ ///
+ /// Always returns `Some`.
+ impl PartialOrd<$ttype> for $NOW {
+ fn partial_cmp(&self, t: &$ttype) -> Option {
+ t.partial_cmp(self).map(|o| o.reverse())
+ }
+ }
+} }
+
+//========== data structures ==========
+
+/// Utility to track timeouts based on [`SystemTime`] (wall clock time)
+///
+/// Represents the current `SystemTime` (from when it was created).
+/// See the [module-level documentation](self) for the general overview.
+///
+/// To operate a timeout,
+/// you should calculate the `SystemTime` at which you should time out,
+/// and compare that future planned wakeup time with this `TrackingSystemTimeNow`
+/// (via [`.cmp()`](Self::cmp) or inequality operators and [`PartialOrd`]).
+#[derive(Clone, Debug, Adhoc)]
+#[derive_adhoc(SingleTimeoutTracker, WaitForEarliest)]
+#[adhoc(track = "SystemTime")]
+#[adhoc(from_runtime = "wallclock", runtime_sleep = "sleep_until_wallclock")]
+pub struct TrackingSystemTimeNow {
+ /// Current time
+ now: SystemTime,
+ /// Earliest time at which we should wake up
+ earliest: Cell>,
+}
+
+/// Earliest timeout at which an [`Instant`] based timeout should occur, as duration from now
+///
+/// The actual tracker, found via `TrackingInstantNow` or `TrackingInstantOffsetNow`
+type InstantEarliest = Cell >;
+
+/// Utility to track timeouts based on [`Instant`] (monotonic time)
+///
+/// Represents the current `Instant` (from when it was created).
+/// See the [module-level documentation](self) for the general overview.
+///
+/// To calculate and check a timeout,
+/// you can
+/// calculate the future `Instant` at which you wish to wake up,
+/// and compare it with a `TrackingInstantNow`,
+/// via [`.cmp()`](Self::cmp) or inequality operators and [`PartialOrd`].
+///
+/// Or you can
+/// use
+/// [`.checked_sub()`](TrackingInstantNow::checked_sub)
+/// to obtain a [`TrackingInstantOffsetNow`].
+#[derive(Clone, Debug, Adhoc)]
+#[derive_adhoc(SingleTimeoutTracker, WaitForEarliest)]
+#[adhoc(track = "Duration")]
+#[adhoc(from_runtime = "now", runtime_sleep = "sleep")]
+pub struct TrackingInstantNow {
+ /// Current time
+ now: Instant,
+ /// Duration until earliest time we should wake up
+ earliest: InstantEarliest,
+}
+
+/// Current minus an offset, for [`Instant`]-based timeout checks
+///
+/// Returned by
+/// [`TrackingNow::checked_sub()`]
+/// and
+/// [`TrackingInstantNow::checked_sub()`].
+///
+/// You can compare this with an interesting fixed `Instant`,
+/// via [`.cmp()`](Self::cmp) or inequality operators and [`PartialOrd`].
+///
+/// Borrows from its parent `TrackingInstantNow`;
+/// multiple different `TrackingInstantOffsetNow`'s can exist
+/// for the same parent tracker,
+/// and they'll all update it.
+///
+/// (There is no corresponding call for `SystemTime`;
+/// see the [docs for `TrackingNow::checked_sub()`](TrackingNow::checked_sub)
+/// for why.)
+#[derive(Debug)]
+pub struct TrackingInstantOffsetNow<'i> {
+ /// Value to compare with
+ threshold: Instant,
+ /// Comparison tracker
+ earliest: &'i InstantEarliest,
+}
+
+/// Timeout tracker that can handle both `Instant`s and `SystemTime`s
+///
+/// Internally, the two kinds of timeouts are tracked separately:
+/// this contains a [`TrackingInstantNow`] and a [`TrackingSystemTimeNow`].
+#[derive(Clone, Debug, Adhoc)]
+#[derive_adhoc(CombinedTimeoutTracker, WaitForEarliest)]
+pub struct TrackingNow {
+ /// For `Instant`s
+ #[adhoc(now = "Instant")]
+ instant: TrackingInstantNow,
+ /// For `SystemTime`s
+ #[adhoc(now = "SystemTime")]
+ system_time: TrackingSystemTimeNow,
+}
+
+//========== implementations, organised by theme ==========
+
+//----- earliest accessor ----
+
+impl TrackingSystemTimeNow {
+ /// Return the earliest `SystemTime` with which this has been compared
+ pub fn earliest(self) -> Option {
+ self.earliest.into_inner()
+ }
+}
+
+impl TrackingInstantNow {
+ /// Return the shortest `Duration` until any `Instant` with which this has been compared
+ pub fn shortest(self) -> Option {
+ self.earliest.into_inner()
+ }
+}
+
+//----- manual update functions ----
+
+impl TrackingSystemTimeNow {
+ /// Update the "earliest timeout" notion, to ensure it's at least as early as `t`
+ ///
+ /// (Equivalent to comparing with `t` but discarding the answer.)
+ pub fn update(&self, t: SystemTime) {
+ Self::update_inner(&self.earliest, t);
+ }
+}
+
+impl TrackingInstantNow {
+ /// Update the "earliest timeout" notion, to ensure it's at least as early as `t`
+ ///
+ /// Equivalent to comparing with `t` but discarding the answer.
+ fn update_abs(&self, t: Instant) {
+ self.update_rel(t.checked_duration_since(self.now).unwrap_or_default());
+ }
+
+ /// Update the "earliest timeout" notion, to ensure it's at no later than `d` from now
+ fn update_rel(&self, d: Duration) {
+ Self::update_inner(&self.earliest, d);
+ }
+}
+
+//----- cmp and PartialOrd implementation ----
+
+impl TrackingSystemTimeNow {
+ /// Check if time `t` has been reached yet (and remember that we want to wake up then)
+ ///
+ /// Also available via [`PartialOrd`]
+ fn cmp(&self, t: SystemTime) -> std::cmp::Ordering {
+ Self::update_inner(&self.earliest, t);
+ self.now.cmp(&t)
+ }
+}
+define_PartialOrd_via_cmp! { TrackingSystemTimeNow, SystemTime, }
+
+/// Check `t` against a now-based `threshold` (and remember for wakeup)
+///
+/// Common code for `TrackingInstantNow` and `TrackingInstantOffsetNow`'s
+/// `cmp`.
+fn instant_cmp(earliest: &InstantEarliest, threshold: Instant, t: Instant) -> Ordering {
+ let Some(d) = t.checked_duration_since(threshold) else {
+ earliest.set(Some(Duration::ZERO));
+ return Ordering::Less;
+ };
+
+ TrackingInstantNow::update_inner(earliest, d);
+ d.cmp(&Duration::ZERO)
+}
+
+impl TrackingInstantNow {
+ /// Check if time `t` has been reached yet (and remember that we want to wake up then)
+ ///
+ /// Also available via [`PartialOrd`]
+ fn cmp(&self, t: Instant) -> std::cmp::Ordering {
+ instant_cmp(&self.earliest, self.now, t)
+ }
+}
+define_PartialOrd_via_cmp! { TrackingInstantNow, Instant, }
+
+impl<'i> TrackingInstantOffsetNow<'i> {
+ /// Check if the offset current time has advanced to `t` yet (and remember for wakeup)
+ ///
+ /// Also available via [`PartialOrd`]
+ ///
+ /// ### Alternative description
+ ///
+ /// Checks if the current time has advanced to `offset` *after* `t`,
+ /// where `offset` was passed to `TrackingInstantNow::checked_sub`.
+ fn cmp(&self, t: Instant) -> std::cmp::Ordering {
+ instant_cmp(self.earliest, self.threshold, t)
+ }
+}
+define_PartialOrd_via_cmp! { TrackingInstantOffsetNow<'_>, Instant, }
+
+// Combined TrackingNow cmp and PartialOrd impls done via derive-adhoc
+
+//----- checked_sub (constructor for Instant offset tracker) -----
+
+impl TrackingInstantNow {
+ /// Return a tracker representing a specific offset before the current time
+ ///
+ /// You can use this to pre-calculate an offset from the current time,
+ /// to compare other `Instant`s with.
+ ///
+ /// This can be convenient to avoid repetition;
+ /// also,
+ /// when working with checked time arithmetic,
+ /// this can helpfully centralise the out-of-bounds error handling site.
+ pub fn checked_sub(&self, offset: Duration) -> Option {
+ let threshold = self.now.checked_sub(offset)?;
+ Some(TrackingInstantOffsetNow {
+ threshold,
+ earliest: &self.earliest,
+ })
+ }
+}
+
+impl TrackingNow {
+ /// Return a tracker representing an `Instant` a specific offset before the current time
+ ///
+ /// See [`TrackingInstantNow::checked_sub()`] for more details.
+ ///
+ /// ### `Instant`-only
+ ///
+ /// The returned tracker handles only `Instant`s,
+ /// for reasons relating to clock warps:
+ /// broadly, waiting for a particular `SystemTime` must always be done
+ /// by working with the future `SystemTime` at which to wake up;
+ /// whereas, waiting for a particular `Instant` can be done by calculating `Durations`s.
+ ///
+ /// For the same reason there is no
+ /// `.checked_sub()` method on [`TrackingSystemTimeNow`].
+ pub fn checked_sub(&self, offset: Duration) -> Option {
+ self.instant.checked_sub(offset)
+ }
+}