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};
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 [`Mismatch`].
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 /// [`Mismatch`] 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, Mismatch> {
146 if delta.to <= self.version {
147 return Ok(Applied::Stale);
148 }
149 if delta.from != self.version {
150 return Err(Mismatch::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(Mismatch::Base {
158 expected: delta.base,
159 found,
160 });
161 }
162 self.value.apply_delta(delta.delta);
163 let found = fingerprint_of(&self.value);
164 if found != delta.result {
165 // The value is now wrong, and deliberately left that way: the
166 // version has not advanced, so the next delta cannot be mistaken
167 // for a clean apply.
168 return Err(Mismatch::Result {
169 expected: delta.result,
170 found,
171 });
172 }
173 self.version = delta.to;
174 Ok(Applied::Updated)
175 }
176}
177
178/// What [`Versioned::apply`] did.
179#[derive(Clone, Copy, Debug, Eq, PartialEq)]
180pub enum Applied {
181 /// The delta moved the value forward.
182 Updated,
183 /// The value already reflected this delta, so nothing was done.
184 Stale,
185}
186
187/// Why a delta could not be applied.
188///
189/// Every variant means the same thing operationally — this receiver cannot
190/// catch up from deltas and needs the whole value resent — but they say
191/// different things about what went wrong.
192#[derive(Clone, Copy, Debug, Eq, PartialEq)]
193pub enum Mismatch {
194 /// A delta was missed: this one was computed against a version that was
195 /// never reached. The stream lost or reordered a message.
196 Gap {
197 /// The version the receiver is at.
198 expected: u64,
199 /// The version the delta was computed against.
200 found: u64,
201 },
202 /// The version lined up but the content did not, so the value drifted for
203 /// a reason outside this stream — a direct mutation, a partly applied
204 /// earlier delta, or two senders writing to one receiver.
205 Base {
206 /// The fingerprint the sender diffed against.
207 expected: u64,
208 /// The fingerprint the receiver actually holds.
209 found: u64,
210 },
211 /// Applying the delta did not produce what the sender said it would. The
212 /// two sides disagree about what the delta means: mismatched schema
213 /// versions, or a bug.
214 Result {
215 /// The fingerprint the sender expected.
216 expected: u64,
217 /// The fingerprint applying actually produced.
218 found: u64,
219 },
220}
221
222impl fmt::Display for Mismatch {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 match self {
225 Mismatch::Gap { expected, found } => write!(
226 f,
227 "missed a delta: at version {}, but this one starts from {}",
228 expected, found
229 ),
230 Mismatch::Base { expected, found } => write!(
231 f,
232 "state has diverged: delta was computed against fingerprint {:#018x}, \
233 but this value is {:#018x}",
234 expected, found
235 ),
236 Mismatch::Result { expected, found } => write!(
237 f,
238 "delta applied to the wrong result: expected fingerprint {:#018x}, \
239 got {:#018x}",
240 expected, found
241 ),
242 }
243 }
244}
245
246impl std::error::Error for Mismatch {}