Crate timesimp

Source
Expand description

Simple sans-io timesync client and server.

Timesimp is based on the averaging method described in Simpson (2002), A Stream-based Time Synchronization Technique For Networked Computer Games, but with a corrected delta calculation. Compared to NTP, it’s a simpler and less accurate time synchronisation algorithm that is usable over network streams, rather than datagrams. Simpson asserts they were able to achieve accuracies of 100ms or better, which is sufficient in many cases; my testing gets accuracies well below 5ms. The main limitation of the algorithm is that round-trip-time is assumed to be symmetric: if the forward trip time is different from the return trip time, then an error is induced equal to the value of the difference in trip times.

This library provides a sans-io implementation: you bring in your async runtime, your transport, and your storage; timesimp gives you time offsets.

If the local clock goes backward during a synchronisation, the invalid delta is discarded; this may cause the sync attempt to fail, especially if the samples count is lowered to its minimum of 3. This is a deliberate design decision: you should handle failure and retry, and the sync will proceed correctly when the clock is stable.

§Example

use std::{convert::Infallible, time::Duration};
use reqwest::{Client, Url};
use timesimp::{SignedDuration, Timesimp};

struct ServerSimp;
impl Timesimp for ServerSimp {
    type Err = Infallible;

    async fn load_offset(&self) -> Result<Option<SignedDuration>, Self::Err> {
        // server time is correct
        Ok(Some(SignedDuration::ZERO))
    }

    async fn store_offset(&mut self, _offset: SignedDuration) -> Result<(), Self::Err> {
        // as time is correct, no need to store offset
        unimplemented!()
    }

    async fn query_server(
        &self,
        _request: timesimp::Request,
    ) -> Result<timesimp::Response, Self::Err> {
        // server has no upstream timesimp
        unimplemented!()
    }

    async fn sleep(duration: std::time::Duration) {
        tokio::time::sleep(duration).await;
    }
}

// Not shown: serving ServerSimp from a URL

struct ClientSimp {
    offset: Option<SignedDuration>,
    url: Url,
}

impl Timesimp for ClientSimp {
    type Err = reqwest::Error;

    async fn load_offset(&self) -> Result<Option<SignedDuration>, Self::Err> {
        Ok(self.offset)
    }

    async fn store_offset(&mut self, offset: SignedDuration) -> Result<(), Self::Err> {
        self.offset = Some(offset);
        Ok(())
    }

    async fn query_server(
        &self,
        request: timesimp::Request,
    ) -> Result<timesimp::Response, Self::Err> {
        let resp = Client::new()
            .post(self.url.clone())
            .body(request.to_bytes().to_vec())
            .send()
            .await?
            .error_for_status()?
            .bytes()
            .await?;
        Ok(timesimp::Response::try_from(&resp[..]).unwrap())
    }

    async fn sleep(duration: std::time::Duration) {
        tokio::time::sleep(duration).await;
    }
}

#[tokio::main]
async fn main() {
    let mut client = ClientSimp {
        offset: None,
        url: "https://timesimp.server".try_into().unwrap(),
    };

    loop {
        if let Ok(Some(offset)) = client.attempt_sync(Default::default()).await {
            println!(
                "Received offset: {offset:?}; current time is {}",
                client.adjusted_timestamp().await.unwrap(),
            );
        }
        tokio::time::sleep(Duration::from_secs(300)).await;
    }
}

Structs§

Request
A timesimp request.
Response
A timesimp response.
Settings
Settings for a Timesimp.
SignedDuration
A signed duration of time represented as a 96-bit integer of nanoseconds.
Timestamp
An instant in time represented as the number of nanoseconds since the Unix epoch.

Enums§

ParseError
Error from parsing request or response data.

Traits§

Timesimp
A time sync client and/or server.