rqlite_client 0.1.0

rqlite database client with optional extra convenience
Documentation
//! Limiting read staleness with [`Freshness`]

use std::time::Duration;

use super::duration_string::DurationString;

/// You can tell the node which receives the read rquest not to return results if the node has been disconnected from
/// the cluster for longer than a specified duration. If a read request sets the query parameter freshness to a
/// [Go duration string](https://golang.org/pkg/time/#Duration), the node serving the read will check that less time
/// has passed since it was last in contact with the Leader, than that specified via freshness. If more time has
/// passed the node will return an error. This approach can be useful if you want to maximize successful query
/// operations, but are willing to tolerate occassional, short-lived networking issues between nodes.
///
/// It's important to note that the freshness does not guarantee that the node is caught up with the Leader, only that
/// it is contact with the Leader. But if a node is in contact with the Leader, it's usually caught up with all
/// changes that have taken place on the cluster. To check if node is actually caught up with Leader, use
/// `freshness_strict` in addition to the `freshness` query parameter.
///
/// The freshness parameter is always ignored if the node serving the query is the Leader. Any read, when served by
/// the Leader, is always going to be within any possible freshness bound. freshness is also ignored for all
/// consistency levels except none, and is also ignored if set to zero.
///
/// If you decide to deploy read-only nodes however, `none` combined with `freshness` can be a particularly effective
/// at adding read scalability to your system. You can use lots of read-only nodes, yet be sure that a given node
/// serving a request has not been disconnected from the cluster for too long.
///
/// See <https://rqlite.io/docs/api/read-consistency/#limiting-read-staleness>
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Freshness {
    duration: Duration,
    is_strict: bool,
}

impl Freshness {
    /// Duration set for `Freshness`
    #[must_use]
    pub fn duration(&self) -> &Duration {
        &self.duration
    }

    /// `true` for enabled `freshness_strict`
    ///
    /// See also [`Freshness::set_strict`]
    #[must_use]
    pub fn is_strict(&self) -> bool {
        self.is_strict
    }

    /// Enable `freshness_strict`
    ///
    /// As explained above freshness just checks that that the node has been in contact with the Leader within the
    /// specified time. If you also want the node to check that the data it last received is not out-of-date by
    /// (at most) the freshness interval, you should also add the `freshness_strict` flag as a query parameter. Note
    /// that this check works by comparing timestamps generated by the Leader to those generated by the node
    /// receiving the read request. Any clock skew between the nodes may therefore affect the correctness of the data
    /// returned by the node. You are responsible for controlling the amount of clock skew across your rqlite cluster.
    #[must_use]
    pub fn set_strict(mut self) -> Self {
        self.is_strict = true;
        self
    }
}

impl DurationString for Freshness {
    fn duration(&self) -> &Duration {
        &self.duration
    }
}

impl From<Duration> for Freshness {
    fn from(value: Duration) -> Self {
        Self {
            duration: value,
            is_strict: false,
        }
    }
}

impl Default for Freshness {
    fn default() -> Self {
        FRESHNESS_DEFAULT
    }
}

impl std::fmt::Display for Freshness {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&DurationString::to_string(self))
    }
}

const FRESHNESS_DEFAULT: Freshness = Freshness {
    duration: Duration::from_secs(1),
    is_strict: false,
};

#[cfg(test)]
mod tests {
    use std::time::Duration;

    use super::Freshness;

    #[test]
    fn display_test() {
        assert_eq!(
            &Freshness::from(Duration::from_millis(1_001)).to_string(),
            "1s1ms"
        );
    }

    #[test]
    fn strict_default_test() {
        assert!(!Freshness::default().is_strict());
    }
}