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 {}