Guard the creation roll-back by revision
A store answers the revision it wrote and what it replaced, and restoring compares against that revision, so a write that landed after the creation's own is kept instead of overwritten. A write that changed nothing rolls back nothing, and a key that held no value is removed rather than set to an empty one. A test fails the second store after the first and checks the earlier caps come back.
This commit is contained in:
parent
37c91eb916
commit
4ad9579e97
4 changed files with 237 additions and 59 deletions
|
|
@ -539,21 +539,61 @@ pub(crate) async fn write<T: Serialize>(
|
|||
value: &T,
|
||||
revision: Option<u64>,
|
||||
) -> anyhow::Result<bool> {
|
||||
Ok(write_revision(store, key, value, revision).await?.is_some())
|
||||
}
|
||||
|
||||
/// As [`write`], answering the revision written, or `None` when another write
|
||||
/// landed first.
|
||||
pub(crate) async fn write_revision<T: Serialize>(
|
||||
store: &Store,
|
||||
key: &str,
|
||||
value: &T,
|
||||
revision: Option<u64>,
|
||||
) -> anyhow::Result<Option<u64>> {
|
||||
let encoded = serde_json::to_vec(value)?;
|
||||
match revision {
|
||||
Some(revision) => match store.update(key, encoded.into(), revision).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) if matches!(e.kind(), UpdateErrorKind::WrongLastRevision) => Ok(false),
|
||||
Ok(written) => Ok(Some(written)),
|
||||
Err(e) if matches!(e.kind(), UpdateErrorKind::WrongLastRevision) => Ok(None),
|
||||
Err(e) => Err(e).with_context(|| format!("writing {key}")),
|
||||
},
|
||||
None => match store.create(key, encoded.into()).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) if matches!(e.kind(), CreateErrorKind::AlreadyExists) => Ok(false),
|
||||
Ok(written) => Ok(Some(written)),
|
||||
Err(e) if matches!(e.kind(), CreateErrorKind::AlreadyExists) => Ok(None),
|
||||
Err(e) => Err(e).with_context(|| format!("writing {key}")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// A write to a key, with what it replaced, so [`Replaced::undo`] can reverse
|
||||
/// it.
|
||||
pub(crate) struct Replaced<T> {
|
||||
/// The value the key held, or `None` when it held none.
|
||||
previous: Option<T>,
|
||||
/// The revision the write made.
|
||||
written: u64,
|
||||
}
|
||||
|
||||
impl<T: Serialize> Replaced<T> {
|
||||
pub(crate) fn new(previous: Option<T>, written: u64) -> Self {
|
||||
Self { previous, written }
|
||||
}
|
||||
|
||||
/// Put `previous` back, or remove the key if it held none, against the
|
||||
/// revision the write made. `Ok(false)` when another write has landed
|
||||
/// since: that one is newer than what is being undone and stays.
|
||||
pub(crate) async fn undo(self, store: &Store, key: &str) -> anyhow::Result<bool> {
|
||||
match self.previous {
|
||||
Some(previous) => write(store, key, &previous, Some(self.written)).await,
|
||||
None => match store.delete_expect_revision(key, Some(self.written)).await {
|
||||
Ok(()) => Ok(true),
|
||||
Err(e) if matches!(e.kind(), UpdateErrorKind::WrongLastRevision) => Ok(false),
|
||||
Err(e) => Err(e).with_context(|| format!("removing {key}")),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply `change` to the definitions and write them back, re-reading and
|
||||
/// re-applying when another write landed in between.
|
||||
async fn commit<T>(
|
||||
|
|
|
|||
Loading…
Reference in a new issue