Skip to main content

liminal/durability/
settle.rs

1//! Writes that settle an unknown outcome by reading the position back.
2//!
3//! A store answers [`DurabilityError::OutcomeUnknown`] for a write it had
4//! started when its caller stopped waiting: the write may have been applied.
5//! It is never "not written". Each helper here performs one write and, on that
6//! answer, reads the position the write targeted, as
7//! [`super::channel`]'s pending append is resolved: the caller's own bytes
8//! standing there mean the write landed and its result is returned; nothing
9//! standing there means it had not landed yet, and the write is sent once
10//! more; anything else is the conflict it would have met. The first write may
11//! still land after that read, so a resend that meets a conflict reads the
12//! position once more, and the caller's own bytes there mean applied, never a
13//! conflict. When a read fails, the original
14//! [`DurabilityError::OutcomeUnknown`] is returned, so the caller still meets
15//! it by name.
16//!
17//! Every write the store offers is guarded by the position it expects (a
18//! sequence, or a swap's old value), so a resend can never duplicate a row:
19//! the guard refuses it once the first write stands.
20
21use super::{DurabilityError, DurableStore, RoutedBatchEntry, RoutedBatchItem};
22
23/// Appends `payload` at `expected_seq`, settling an unknown outcome.
24///
25/// # Errors
26///
27/// The append's error; [`DurabilityError::OutcomeUnknown`] when the position
28/// cannot be read back; [`DurabilityError::SequenceConflict`] when another row
29/// stands where the append was sent.
30pub async fn append_settled(
31    store: &dyn DurableStore,
32    stream_key: &str,
33    payload: Vec<u8>,
34    expected_seq: u64,
35) -> Result<u64, DurabilityError> {
36    match store
37        .append(stream_key, payload.clone(), expected_seq)
38        .await
39    {
40        Err(unknown @ DurabilityError::OutcomeUnknown { .. }) => {
41            match own_row(store, stream_key, expected_seq, &payload).await {
42                Err(read) => Err(unread(unknown, stream_key, &read)),
43                Ok(Standing::Own) => Ok(expected_seq),
44                Ok(Standing::Other) => Err(conflict(expected_seq)),
45                Ok(Standing::Empty) => {
46                    match store
47                        .append(stream_key, payload.clone(), expected_seq)
48                        .await
49                    {
50                        Err(resent @ DurabilityError::SequenceConflict { .. }) => {
51                            match own_row(store, stream_key, expected_seq, &payload).await {
52                                Ok(Standing::Own) => Ok(expected_seq),
53                                Ok(_) => Err(resent),
54                                Err(read) => Err(unread(unknown, stream_key, &read)),
55                            }
56                        }
57                        outcome => outcome,
58                    }
59                }
60            }
61        }
62        outcome => outcome,
63    }
64}
65
66/// Appends `payloads` from `expected_seq` in one write, settling an unknown
67/// outcome. Answers the sequence after the last row.
68///
69/// # Errors
70///
71/// As [`append_settled`].
72pub async fn append_all_settled(
73    store: &dyn DurableStore,
74    stream_key: &str,
75    payloads: Vec<Vec<u8>>,
76    expected_seq: u64,
77) -> Result<u64, DurabilityError> {
78    match store
79        .append_all(stream_key, payloads.clone(), expected_seq)
80        .await
81    {
82        Err(unknown @ DurabilityError::OutcomeUnknown { .. }) => {
83            let after = after_batch(stream_key, expected_seq, payloads.len())?;
84            match own_rows(store, stream_key, expected_seq, &payloads).await {
85                Err(read) => Err(unread(unknown, stream_key, &read)),
86                Ok(Standing::Own) => Ok(after),
87                Ok(Standing::Other) => Err(conflict(expected_seq)),
88                Ok(Standing::Empty) => {
89                    match store
90                        .append_all(stream_key, payloads.clone(), expected_seq)
91                        .await
92                    {
93                        Err(resent @ DurabilityError::SequenceConflict { .. }) => {
94                            match own_rows(store, stream_key, expected_seq, &payloads).await {
95                                Ok(Standing::Own) => Ok(after),
96                                Ok(_) => Err(resent),
97                                Err(read) => Err(unread(unknown, stream_key, &read)),
98                            }
99                        }
100                        outcome => outcome,
101                    }
102                }
103            }
104        }
105        outcome => outcome,
106    }
107}
108
109/// Swaps `key` from `old_value` to `new_value`, settling an unknown outcome.
110///
111/// `new_value` stored means the swap landed; `old_value` (or nothing, for an
112/// `old_value` of 0) means it did not, and it runs once more.
113///
114/// # Errors
115///
116/// The swap's error; [`DurabilityError::OutcomeUnknown`] when the value
117/// cannot be read back; [`DurabilityError::CursorRegression`] when another
118/// value stands there.
119pub async fn cas_settled(
120    store: &dyn DurableStore,
121    key: &str,
122    old_value: u64,
123    new_value: u64,
124) -> Result<(), DurabilityError> {
125    match store.cas(key, old_value, new_value).await {
126        Err(unknown @ DurabilityError::OutcomeUnknown { .. }) => {
127            let stored = match store.read_value(key).await {
128                Ok(stored) => stored.unwrap_or(0),
129                Err(read) => return Err(unread(unknown, key, &read)),
130            };
131            if stored == new_value {
132                return Ok(());
133            }
134            if stored != old_value {
135                return Err(DurabilityError::CursorRegression {
136                    stored,
137                    attempted: old_value,
138                });
139            }
140            match store.cas(key, old_value, new_value).await {
141                Err(resent @ DurabilityError::CursorRegression { .. }) => {
142                    match store.read_value(key).await {
143                        Ok(stored) if stored.unwrap_or(0) == new_value => Ok(()),
144                        Ok(_) => Err(resent),
145                        Err(read) => Err(unread(unknown, key, &read)),
146                    }
147                }
148                outcome => outcome,
149            }
150        }
151        outcome => outcome,
152    }
153}
154
155/// Flushes once, preserving any failure as an unconfirmed durability barrier.
156///
157/// # Errors
158///
159/// Returns the store's original flush failure without retrying the barrier.
160pub async fn flush_settled(store: &dyn DurableStore) -> Result<(), DurabilityError> {
161    store.flush().await
162}
163
164/// Applies one routed batch and settles an unknown outcome by reading every
165/// key the batch targeted.
166///
167/// The caller's own bytes at every key mean the batch landed; every key still
168/// at the state the batch expected means it had not landed yet, and the batch
169/// is sent once more; anything else is the conflict it would have met. A
170/// resend that meets the batch's own hash guard reads once more, and the
171/// caller's own bytes there mean applied.
172///
173/// # Errors
174/// Returns the store's refusal unchanged, the original unknown outcome when
175/// the read-back fails or the keys stand mixed, and the guard's refusal when a
176/// resend meets it over foreign bytes.
177pub async fn apply_routed_batch_settled(
178    store: &dyn DurableStore,
179    route_key: &str,
180    items: Vec<RoutedBatchItem>,
181) -> Result<(), DurabilityError> {
182    // An empty batch writes nothing and has no outcome to settle, so it never
183    // costs the store a call (the self-stream drainer's counts pin zero).
184    if items.is_empty() {
185        return Ok(());
186    }
187    match store.apply_routed_batch(route_key, items.clone()).await {
188        Err(unknown @ DurabilityError::OutcomeUnknown { .. }) => {
189            match own_keys(store, route_key, &items).await {
190                Err(read) => Err(unread(unknown, route_key, &read)),
191                Ok(Standing::Own) => Ok(()),
192                Ok(Standing::Other) => Err(unread(
193                    unknown,
194                    route_key,
195                    &DurabilityError::ConfigError(
196                        "routed batch keys stand neither as written nor as expected".to_owned(),
197                    ),
198                )),
199                Ok(Standing::Empty) => {
200                    match store.apply_routed_batch(route_key, items.clone()).await {
201                        Err(resent @ DurabilityError::ConfigError(_)) => {
202                            match own_keys(store, route_key, &items).await {
203                                Ok(Standing::Own) => Ok(()),
204                                Ok(_) => Err(resent),
205                                Err(read) => Err(unread(unknown, route_key, &read)),
206                            }
207                        }
208                        outcome => outcome,
209                    }
210                }
211            }
212        }
213        outcome => outcome,
214    }
215}
216
217/// Applies one routed batch of puts and deletes and settles an unknown
218/// outcome by reading every key the batch targeted (DUR-028 R3).
219///
220/// As [`apply_routed_batch_settled`]: the batch landed when every put reads
221/// as the caller's own bytes and every deleted key reads absent; it had not
222/// landed when every key still stands as the batch expected (a put's guard,
223/// a delete's hash), and it is sent once more; anything else is the
224/// conflict it would have met.
225///
226/// # Errors
227/// Returns the store's refusal unchanged, the original unknown outcome when
228/// the read-back fails or the keys stand mixed, and the guard's refusal when
229/// a resend meets it over foreign bytes.
230pub async fn apply_routed_entries_settled(
231    store: &dyn DurableStore,
232    route_key: &str,
233    entries: Vec<RoutedBatchEntry>,
234) -> Result<(), DurabilityError> {
235    if entries.is_empty() {
236        return Ok(());
237    }
238    match store.apply_routed_entries(route_key, entries.clone()).await {
239        Err(unknown @ DurabilityError::OutcomeUnknown { .. }) => {
240            match own_entries(store, route_key, &entries).await {
241                Err(read) => Err(unread(unknown, route_key, &read)),
242                Ok(Standing::Own) => Ok(()),
243                Ok(Standing::Other) => Err(unread(
244                    unknown,
245                    route_key,
246                    &DurabilityError::ConfigError(
247                        "routed batch keys stand neither as written nor as expected".to_owned(),
248                    ),
249                )),
250                Ok(Standing::Empty) => {
251                    match store.apply_routed_entries(route_key, entries.clone()).await {
252                        Err(
253                            resent @ (DurabilityError::ConfigError(_)
254                            | DurabilityError::StoreError(_)),
255                        ) => match own_entries(store, route_key, &entries).await {
256                            Ok(Standing::Own) => Ok(()),
257                            Ok(_) => Err(resent),
258                            Err(read) => Err(unread(unknown, route_key, &read)),
259                        },
260                        outcome => outcome,
261                    }
262                }
263            }
264        }
265        outcome => outcome,
266    }
267}
268
269/// The conflict an append sent to `expected_seq` meets when another row
270/// stands there.
271const fn conflict(expected_seq: u64) -> DurabilityError {
272    DurabilityError::SequenceConflict {
273        expected: expected_seq,
274        actual: expected_seq.saturating_add(1),
275    }
276}
277
278/// The unknown outcome a failed read-back leaves standing, with the read's
279/// failure warned beside it.
280fn unread(unknown: DurabilityError, key: &str, read: &DurabilityError) -> DurabilityError {
281    tracing::warn!(
282        key,
283        %read,
284        "a write's unknown outcome could not be read back; it stays unknown"
285    );
286    unknown
287}
288
289/// What stands where a write was sent.
290enum Standing {
291    /// The caller's own bytes: the write landed.
292    Own,
293    /// Nothing: the write has not landed.
294    Empty,
295    /// Another writer's row.
296    Other,
297}
298
299/// What stands at `sequence` against `payload`.
300async fn own_row(
301    store: &dyn DurableStore,
302    stream_key: &str,
303    sequence: u64,
304    payload: &[u8],
305) -> Result<Standing, DurabilityError> {
306    Ok(match store.read_at(stream_key, sequence).await? {
307        None => Standing::Empty,
308        Some(entry) if entry.payload == payload => Standing::Own,
309        Some(_) => Standing::Other,
310    })
311}
312
313/// What stands from `expected_seq` against `payloads`, as one batch: a batch
314/// lands whole or not at all.
315/// How every key of a routed batch stands against the bytes the batch wrote
316/// there: all the caller's own bytes, all still at the state the batch
317/// expected (absent, or hashing to the guard), or mixed.
318async fn own_keys(
319    store: &dyn DurableStore,
320    route_key: &str,
321    items: &[RoutedBatchItem],
322) -> Result<Standing, DurabilityError> {
323    let mut own = 0_usize;
324    let mut expected = 0_usize;
325    for item in items {
326        let stored = store.get_routed(route_key, &item.key).await?;
327        match stored {
328            Some(bytes) if bytes == item.value => own += 1,
329            None if item.expected.is_none() => expected += 1,
330            Some(bytes) if item.expected == Some(haematite::tree::Hash::of(&bytes)) => {
331                expected += 1;
332            }
333            _ => {}
334        }
335    }
336    Ok(if own == items.len() {
337        Standing::Own
338    } else if expected == items.len() {
339        Standing::Empty
340    } else {
341        Standing::Other
342    })
343}
344
345/// How every key of a routed batch of entries stands against it: a put as
346/// [`own_keys`] reads one, a delete as its own when the key is absent and
347/// as expected when the value still hashes to the delete's guard.
348async fn own_entries(
349    store: &dyn DurableStore,
350    route_key: &str,
351    entries: &[RoutedBatchEntry],
352) -> Result<Standing, DurabilityError> {
353    let mut own = 0_usize;
354    let mut expected = 0_usize;
355    for entry in entries {
356        let stored = store.get_routed(route_key, entry.key()).await?;
357        match (entry, stored) {
358            (RoutedBatchEntry::Put(item), Some(bytes)) if bytes == item.value => own += 1,
359            (RoutedBatchEntry::Put(item), None) if item.expected.is_none() => expected += 1,
360            (RoutedBatchEntry::Put(item), Some(bytes))
361                if item.expected == Some(haematite::tree::Hash::of(&bytes)) =>
362            {
363                expected += 1;
364            }
365            (RoutedBatchEntry::Delete { .. }, None) => own += 1,
366            (
367                RoutedBatchEntry::Delete {
368                    expected: guard, ..
369                },
370                Some(bytes),
371            ) if *guard == haematite::tree::Hash::of(&bytes) => {
372                expected += 1;
373            }
374            _ => {}
375        }
376    }
377    Ok(if own == entries.len() {
378        Standing::Own
379    } else if expected == entries.len() {
380        Standing::Empty
381    } else {
382        Standing::Other
383    })
384}
385
386async fn own_rows(
387    store: &dyn DurableStore,
388    stream_key: &str,
389    expected_seq: u64,
390    payloads: &[Vec<u8>],
391) -> Result<Standing, DurabilityError> {
392    let standing = store
393        .read_from(stream_key, expected_seq, payloads.len())
394        .await?;
395    if standing.is_empty() {
396        return Ok(Standing::Empty);
397    }
398    let own =
399        standing.len() == payloads.len()
400            && standing.iter().zip(payloads).zip(expected_seq..).all(
401                |((entry, payload), sequence)| {
402                    entry.sequence == sequence && entry.payload == *payload
403                },
404            );
405    Ok(if own { Standing::Own } else { Standing::Other })
406}
407
408/// The sequence after a batch of `rows` from `expected_seq`.
409fn after_batch(stream_key: &str, expected_seq: u64, rows: usize) -> Result<u64, DurabilityError> {
410    u64::try_from(rows)
411        .ok()
412        .and_then(|rows| expected_seq.checked_add(rows))
413        .ok_or_else(|| {
414            DurabilityError::ConfigError(format!(
415                "{stream_key}: sequence overflow settling a batch at {expected_seq}"
416            ))
417        })
418}