use bytes::Bytes;
use crate::LixError;
use crate::storage_adapter::{
REVISION_KEY_JSON_STORE_PUBLICATION, REVISION_KEY_JSON_STORE_RECLAMATION, REVISION_SPACE,
StorageAdapterRead, StoragePrecondition, StorageValue, StorageWriteSet, load_revision,
load_revisions, revision_key,
};
fn fresh_revision_token() -> StorageValue {
StorageValue {
bytes: Bytes::copy_from_slice(uuid::Uuid::now_v7().as_bytes()),
}
}
fn unchanged_revision_precondition(
key: &'static [u8],
token: Option<Bytes>,
) -> StoragePrecondition {
let key = revision_key(key);
match token {
Some(expected) => StoragePrecondition::KeyValueEquals {
space: REVISION_SPACE,
key,
expected,
},
None => StoragePrecondition::KeyAbsent {
space: REVISION_SPACE,
key,
},
}
}
pub(crate) async fn stage_json_publication_fence(
store: &(impl StorageAdapterRead + ?Sized),
writes: &mut StorageWriteSet,
preconditions: &mut Vec<StoragePrecondition>,
) -> Result<(), LixError> {
let reclamation = load_revision(store, REVISION_KEY_JSON_STORE_RECLAMATION).await?;
writes.put(
REVISION_SPACE,
revision_key(REVISION_KEY_JSON_STORE_PUBLICATION),
fresh_revision_token(),
);
preconditions.push(unchanged_revision_precondition(
REVISION_KEY_JSON_STORE_RECLAMATION,
reclamation,
));
Ok(())
}
pub(crate) async fn stage_json_reclamation_fence(
store: &(impl StorageAdapterRead + ?Sized),
writes: &mut StorageWriteSet,
preconditions: &mut Vec<StoragePrecondition>,
) -> Result<(), LixError> {
let [publication, reclamation] = load_revisions(
store,
[
REVISION_KEY_JSON_STORE_PUBLICATION,
REVISION_KEY_JSON_STORE_RECLAMATION,
],
)
.await?;
writes.put(
REVISION_SPACE,
revision_key(REVISION_KEY_JSON_STORE_RECLAMATION),
fresh_revision_token(),
);
preconditions.push(unchanged_revision_precondition(
REVISION_KEY_JSON_STORE_PUBLICATION,
publication,
));
preconditions.push(unchanged_revision_precondition(
REVISION_KEY_JSON_STORE_RECLAMATION,
reclamation,
));
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage_adapter::{Memory, StorageAdapter, StorageReadOptions, StorageWriteOptions};
async fn stage_publication_only(
storage: &StorageAdapter<Memory>,
) -> (StorageWriteSet, Vec<StoragePrecondition>) {
let read = storage
.begin_read(StorageReadOptions::default())
.await
.expect("publication fence read should open");
let mut writes = storage.new_write_set();
let mut preconditions = Vec::new();
stage_json_publication_fence(&read, &mut writes, &mut preconditions)
.await
.expect("publication fence should stage");
(writes, preconditions)
}
async fn stage_reclamation_only(
storage: &StorageAdapter<Memory>,
) -> (StorageWriteSet, Vec<StoragePrecondition>) {
let read = storage
.begin_read(StorageReadOptions::default())
.await
.expect("reclamation fence read should open");
let mut writes = storage.new_write_set();
let mut preconditions = Vec::new();
stage_json_reclamation_fence(&read, &mut writes, &mut preconditions)
.await
.expect("reclamation fence should stage");
(writes, preconditions)
}
fn with_preconditions(preconditions: Vec<StoragePrecondition>) -> StorageWriteOptions {
let mut options = StorageWriteOptions::default();
options.preconditions.extend(preconditions);
options
}
#[tokio::test]
async fn concurrent_publication_fences_planned_from_one_snapshot_both_commit() {
let storage = StorageAdapter::new(Memory::new());
let (first_writes, first_preconditions) = stage_publication_only(&storage).await;
let (second_writes, second_preconditions) = stage_publication_only(&storage).await;
storage
.commit_write_set(first_writes, with_preconditions(first_preconditions))
.await
.expect("first publication fence should commit");
storage
.commit_write_set(second_writes, with_preconditions(second_preconditions))
.await
.expect("independent publishers must not conflict with each other");
}
#[tokio::test]
async fn a_publication_after_a_sweeps_plan_voids_that_sweep() {
let storage = StorageAdapter::new(Memory::new());
let (sweep_writes, sweep_preconditions) = stage_reclamation_only(&storage).await;
let (publish_writes, publish_preconditions) = stage_publication_only(&storage).await;
storage
.commit_write_set(publish_writes, with_preconditions(publish_preconditions))
.await
.expect("the publisher planned first and must commit");
storage
.commit_write_set(sweep_writes, with_preconditions(sweep_preconditions))
.await
.expect_err("a sweep whose plan predates a publication must not commit");
}
#[tokio::test]
async fn a_sweep_after_a_publishers_plan_voids_that_publisher() {
let storage = StorageAdapter::new(Memory::new());
let (publish_writes, publish_preconditions) = stage_publication_only(&storage).await;
let (sweep_writes, sweep_preconditions) = stage_reclamation_only(&storage).await;
storage
.commit_write_set(sweep_writes, with_preconditions(sweep_preconditions))
.await
.expect("the sweep planned against the same snapshot and must commit");
storage
.commit_write_set(publish_writes, with_preconditions(publish_preconditions))
.await
.expect_err("a publisher whose plan predates a reclamation must not commit");
}
#[tokio::test]
async fn concurrent_reclamation_fences_planned_from_one_snapshot_conflict() {
let storage = StorageAdapter::new(Memory::new());
let (first_writes, first_preconditions) = stage_reclamation_only(&storage).await;
let (second_writes, second_preconditions) = stage_reclamation_only(&storage).await;
storage
.commit_write_set(first_writes, with_preconditions(first_preconditions))
.await
.expect("first sweep should win the reclamation fence");
storage
.commit_write_set(second_writes, with_preconditions(second_preconditions))
.await
.expect_err("a stale sweep must lose the reclamation fence");
}
}