Skip to main content

delta_struct/
version.rs

1//! Detecting a delta that is being applied to the wrong state.
2//!
3//! [`Delta::apply_delta`] assumes the value it is handed is equal to the `old`
4//! the delta was computed from. Nothing checks that, so a dropped, reordered,
5//! or replayed message silently diverges the two sides. [`Versioned`] wraps a
6//! value with the bookkeeping to notice.
7//!
8//! This module is entirely opt-in. The [`Delta`] trait, the derive, and every
9//! generated struct are unchanged by it — if you are diffing locally or over a
10//! reliable transport, you never have to name anything in here.
11//!
12//! # What gets checked
13//!
14//! Each [`VersionedDelta`] carries four numbers, and they catch different
15//! failures:
16//!
17//! - `from` and `to` — a sequence. A delta arriving out of order leaves a gap,
18//!   and one arriving twice is recognised and ignored.
19//! - `base` — the [`Fingerprint`] of the state the sender diffed against. This
20//!   catches a receiver whose value drifted for any reason at all, including
21//!   one that never came through this stream.
22//! - `result` — the fingerprint the sender expects applying to produce. This
23//!   catches the delta itself being wrong.
24//!
25//! ```
26//! use delta_struct::{Applied, Delta, Fingerprint, Versioned};
27//!
28//! #[derive(Clone, Delta, Fingerprint)]
29//! struct Config {
30//!     host: String,
31//!     port: u16,
32//! }
33//!
34//! let config = |port| Config { host: "localhost".to_string(), port };
35//!
36//! let mut sender = Versioned::new(config(80));
37//! let mut receiver = Versioned::new(config(80));
38//!
39//! let message = sender.commit(config(8080)).expect("the port changed");
40//! assert!(matches!(receiver.apply(message), Ok(Applied::Updated)));
41//! assert_eq!(receiver.get().port, 8080);
42//! ```
43
44use crate::{fingerprint_of, Delta, Fingerprint, Mismatch};
45use std::fmt;
46
47/// A delta, plus everything needed to tell whether it belongs here.
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct VersionedDelta<D> {
51    /// The version this delta was computed against.
52    pub from: u64,
53    /// The version applying it produces.
54    pub to: u64,
55    /// The [`Fingerprint`] of the state it was computed against.
56    pub base: u64,
57    /// The fingerprint applying it should produce.
58    pub result: u64,
59    /// The delta itself.
60    pub delta: D,
61}
62
63/// A value and the version it is currently at.
64///
65/// Both ends of a connection hold one. The sender calls [`commit`], the
66/// receiver calls [`apply`], and the version and fingerprints travel between
67/// them inside a [`VersionedDelta`].
68///
69/// [`commit`]: Versioned::commit
70/// [`apply`]: Versioned::apply
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct Versioned<T> {
74    value: T,
75    version: u64,
76}
77
78impl<T> Versioned<T> {
79    /// Starts a value at version 0.
80    ///
81    /// Both ends have to start from the same value; sending a `Versioned<T>`
82    /// whole is how a receiver catches up after a [`Rejected`].
83    pub fn new(value: T) -> Self {
84        Versioned { value, version: 0 }
85    }
86
87    /// The version this value is at.
88    pub fn version(&self) -> u64 {
89        self.version
90    }
91
92    /// Borrows the value.
93    pub fn get(&self) -> &T {
94        &self.value
95    }
96
97    /// Takes the value back out, discarding the version.
98    pub fn into_inner(self) -> T {
99        self.value
100    }
101}
102
103impl<T: Fingerprint> Versioned<T> {
104    /// The fingerprint of the value as it stands.
105    pub fn fingerprint(&self) -> u64 {
106        fingerprint_of(&self.value)
107    }
108}
109
110impl<T: Delta + Fingerprint + Clone> Versioned<T> {
111    /// Moves to `new` and produces the delta that gets a peer here.
112    ///
113    /// Returns [`None`] when nothing changed, in which case the version does
114    /// not advance either — an update that would do nothing costs no message
115    /// and no number.
116    ///
117    /// `T: Clone` is needed because [`Delta::delta`] consumes both sides and
118    /// the new value has to be kept as well as diffed.
119    pub fn commit(&mut self, new: T) -> Option<VersionedDelta<T::Output>> {
120        let base = fingerprint_of(&self.value);
121        let result = fingerprint_of(&new);
122        let old = std::mem::replace(&mut self.value, new.clone());
123        Delta::delta(old, new).map(|delta| {
124            let from = self.version;
125            self.version += 1;
126            VersionedDelta {
127                from,
128                to: self.version,
129                base,
130                result,
131                delta,
132            }
133        })
134    }
135}
136
137impl<T: Delta + Fingerprint> Versioned<T> {
138    /// Applies a delta, or explains why it does not belong here.
139    ///
140    /// A delta that has already been applied is reported as
141    /// [`Applied::Stale`] and does nothing, so duplicate delivery is safe. A
142    /// [`Rejected`] leaves the version untouched, which means a later delta in
143    /// the same stream will fail too rather than papering over the hole — the
144    /// only way forward is to replace the whole value.
145    pub fn apply(&mut self, delta: VersionedDelta<T::Output>) -> Result<Applied, Rejected> {
146        if delta.to <= self.version {
147            return Ok(Applied::Stale);
148        }
149        if delta.from != self.version {
150            return Err(Rejected::Gap {
151                expected: self.version,
152                found: delta.from,
153            });
154        }
155        let found = fingerprint_of(&self.value);
156        if found != delta.base {
157            return Err(Rejected::Base {
158                expected: delta.base,
159                found,
160            });
161        }
162        // The base fingerprint matched, so the value is the one the delta was
163        // built against and an enum's variants line up — this can only fire on
164        // a fingerprint collision or a bug, which is worth being able to say.
165        self.value
166            .apply_delta(delta.delta)
167            .map_err(Rejected::Apply)?;
168        let found = fingerprint_of(&self.value);
169        if found != delta.result {
170            // The value is now wrong, and deliberately left that way: the
171            // version has not advanced, so the next delta cannot be mistaken
172            // for a clean apply.
173            return Err(Rejected::Result {
174                expected: delta.result,
175                found,
176            });
177        }
178        self.version = delta.to;
179        Ok(Applied::Updated)
180    }
181}
182
183/// What [`Versioned::apply`] did.
184#[derive(Clone, Copy, Debug, Eq, PartialEq)]
185pub enum Applied {
186    /// The delta moved the value forward.
187    Updated,
188    /// The value already reflected this delta, so nothing was done.
189    Stale,
190}
191
192/// Why a delta could not be applied.
193///
194/// Every variant means the same thing operationally — this receiver cannot
195/// catch up from deltas and needs the whole value resent — but they say
196/// different things about what went wrong.
197#[derive(Clone, Copy, Debug, Eq, PartialEq)]
198pub enum Rejected {
199    /// A delta was missed: this one was computed against a version that was
200    /// never reached. The stream lost or reordered a message.
201    Gap {
202        /// The version the receiver is at.
203        expected: u64,
204        /// The version the delta was computed against.
205        found: u64,
206    },
207    /// The version lined up but the content did not, so the value drifted for
208    /// a reason outside this stream — a direct mutation, a partly applied
209    /// earlier delta, or two senders writing to one receiver.
210    Base {
211        /// The fingerprint the sender diffed against.
212        expected: u64,
213        /// The fingerprint the receiver actually holds.
214        found: u64,
215    },
216    /// Applying the delta did not produce what the sender said it would. The
217    /// two sides disagree about what the delta means: mismatched schema
218    /// versions, or a bug.
219    Result {
220        /// The fingerprint the sender expected.
221        expected: u64,
222        /// The fingerprint applying actually produced.
223        found: u64,
224    },
225    /// The delta did not fit the value's shape at all — an enum delta built
226    /// for one variant meeting a value in another.
227    ///
228    /// The [`base`](VersionedDelta::base) fingerprint is checked first and
229    /// would normally have caught that, so reaching this means a fingerprint
230    /// collision or a bug rather than ordinary divergence.
231    Apply(Mismatch),
232}
233
234impl fmt::Display for Rejected {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        match self {
237            Rejected::Gap { expected, found } => write!(
238                f,
239                "missed a delta: at version {}, but this one starts from {}",
240                expected, found
241            ),
242            Rejected::Base { expected, found } => write!(
243                f,
244                "state has diverged: delta was computed against fingerprint {:#018x}, \
245                 but this value is {:#018x}",
246                expected, found
247            ),
248            Rejected::Apply(mismatch) => write!(f, "{}", mismatch),
249            Rejected::Result { expected, found } => write!(
250                f,
251                "delta applied to the wrong result: expected fingerprint {:#018x}, \
252                 got {:#018x}",
253                expected, found
254            ),
255        }
256    }
257}
258
259impl std::error::Error for Rejected {}