Module timeout_track

Source
Expand description

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 unexpired timeout that was asked about.

I.e., the timeout tracker tells you when (in the future) any of the comparisons you have made, might produce different answers. So, that can be used to know how long to sleep for when waiting for timeout(s).

This approach means you must be sure to actually perform the timeout action whenever a comparison tells you the relevant period has elapsed. If you fail to do so, the timeout tracker will still disregard the event for the purposes of calculating how to wait, since it is in the past. So if you use the timeout tracker to decide how long to sleep, you won’t be woken up until something else occurs. (When the timeout has exactly elapsed, you should eagerly perform the action. Otherwise the timeout tracker will calculate a zero timeout and you’ll spin.)

Each tracker 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:

§Advantages, disadvantages, and alternatives

Using TrackingNow allows time-dependent code to be written in a natural, imperative, style, even if the timeout calculations are complex. Simply test whether it is time yet to do each thing, and if so do it.

This can conveniently be combined with an idempotent imperative style for handling non-time-based inputs: for each possible action, you can decide in one place whether it needs doing. (Use a select_biased!, with wait_for_earliest as one of the branches.)

This approach makes it harder to write bugs where some of the consequences of events are forgotten. Each timeout calculation is always done afresh from all its inputs. There is only ever one place where each action is considered, and the consideration is always redone from first principles.

However, this is not necessarily the most performant approach. Each iteration of the event loop doesn’t know why it has woken up, so must simply re-test all of the things that might need to be done.

When higher performance is needed, consider maintaining timeouts as state: either as wakeup times or durations, or as actual Futures, depending on how frequently they are going to occur, and how much they need to be modified. With this approach you must remember to update or recalculate the timeout, on every change to any of the inputs to each timeout calculation. You must write code to check (or perform) each action, in the handler for each event that might trigger it. Omitting a call is easy, can result in mysterious ordering-dependent “stuckess” bugs, and is often detectable only by very comprehensive testing.

§Example

use std::sync::{Arc, Mutex};
use std::time::Duration;
use futures::task::SpawnExt as _;
use tor_rtcompat::{ToplevelBlockOn as _, SleepProvider as _};

use crate::timeout_track;
use timeout_track::TrackingInstantNow;

// Test harness
let runtime = tor_rtmock::MockRuntime::new();
let actions = Arc::new(Mutex::new("".to_string())); // initial letters of performed actions
let perform_action = {
    let actions = actions.clone();
    move |s: &str| actions.lock().unwrap().extend(s.chars().take(1))
};

runtime.spawn({
    let runtime = runtime.clone();

    // Example program which models cooking a stir-fry
    async move {
        perform_action("add ingredients");
        let started = runtime.now();
        let mut last_stirred = started;
        loop {
            let now_track = TrackingInstantNow::now(&runtime);

            const STIR_EVERY: Duration = Duration::from_secs(25);
            // In production, we might avoid panics:  .. >= last_stirred.checked_add(..)
            if now_track >= last_stirred + STIR_EVERY {
                perform_action("stir");
                last_stirred = now_track.get_now_untracked();
                continue;
            }

            const COOK_FOR: Duration = Duration::from_secs(3 * 60);
            if now_track >= started + COOK_FOR {
                break;
            }

            now_track.wait_for_earliest(&runtime).await;
        }
        perform_action("dish up");
    }
}).unwrap();

// Do a test run
runtime.block_on(async {
    runtime.advance_by(Duration::from_secs(1 * 60)).await;
    assert_eq!(*actions.lock().unwrap(), "ass");
    runtime.advance_by(Duration::from_secs(2 * 60)).await;
    assert_eq!(*actions.lock().unwrap(), "asssssssd");
});

Modules§

sealed 🔒
Sealed

Macros§

define_PartialOrd_via_cmp 🔒
impl PartialOrd<$NOW> for $ttype in terms of ...$field.cmp()
derive_deftly_template_CombinedTimeoutTracker 🔒
Impls for TrackingNow, the combined tracker
derive_deftly_template_SingleTimeoutTracker 🔒
Defines methods and types which are common to trackers for Instant and SystemTime
derive_deftly_template_WaitForEarliest 🔒
Defines wait_for_earliest

Structs§

TrackingInstantNow
Utility to track timeouts based on Instant (monotonic time)
TrackingInstantOffsetNow
Current minus an offset, for Instant-based timeout checks
TrackingNow
Timeout tracker that can handle both Instants and SystemTimes
TrackingSystemTimeNow
Utility to track timeouts based on SystemTime (wall clock time)

Traits§

Update
Trait providing the update method on timeout trackers

Functions§

instant_cmp 🔒
Check t against a now-based threshold (and remember for wakeup)

Type Aliases§

InstantEarliest 🔒
Earliest timeout at which an Instant based timeout should occur, as duration from now