Add the transactional prepared-artifact catalog
This commit is contained in:
parent
2d4ec2ed8b
commit
96b494a8bb
1 changed files with 993 additions and 0 deletions
292
crates/lazaret-storage-catalog/tests/catalog.rs
Normal file
292
crates/lazaret-storage-catalog/tests/catalog.rs
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
use std::fs::OpenOptions;
|
||||
use std::sync::{Arc, Barrier};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use lazaret_core::codec::encode;
|
||||
use lazaret_core::identity::{
|
||||
DeviceId, DomainId, GenerationId, HostPath, IdMapDigest, InodeId, PolicyId, PrepareOperationId,
|
||||
PreparedArtifactId, SelectedManifestDigest, SourceIdentity,
|
||||
};
|
||||
use lazaret_core::records::{ArtifactLease, ArtifactState, PrepareOperation, PreparedArtifact};
|
||||
use lazaret_linux::filesystem::try_exclusive_flock;
|
||||
use lazaret_storage_catalog::{Catalog, CatalogError, CatalogRow};
|
||||
use redb::{Database, TableDefinition};
|
||||
use serde::Serialize;
|
||||
use tempfile::TempDir;
|
||||
|
||||
const METADATA: TableDefinition<u8, &[u8]> = TableDefinition::new("catalog_metadata");
|
||||
|
||||
struct Fixture {
|
||||
directory: TempDir,
|
||||
catalog: Catalog,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
fn new() -> Self {
|
||||
let directory = TempDir::new().unwrap();
|
||||
let catalog = Catalog::new(
|
||||
directory.path().join("catalog.redb"),
|
||||
directory.path().join("storage-catalog.lock"),
|
||||
);
|
||||
catalog.initialize().unwrap();
|
||||
Self { directory, catalog }
|
||||
}
|
||||
|
||||
fn artifact(seed: u8) -> PreparedArtifact {
|
||||
PreparedArtifact {
|
||||
prepared_artifact_id: PreparedArtifactId::new([seed; 32]),
|
||||
source_identity: SourceIdentity::new(vec![seed, 1]),
|
||||
selected_manifest_digest: SelectedManifestDigest::new([seed; 32]),
|
||||
policy_id: PolicyId::new([seed.wrapping_add(1); 32]),
|
||||
id_map_digest: IdMapDigest::new([seed.wrapping_add(2); 32]),
|
||||
path: HostPath::new(format!("artifacts/{seed}").into_bytes()),
|
||||
device: DeviceId::new(u64::from(seed)),
|
||||
inode: InodeId::new(u64::from(seed) + 100),
|
||||
state: ArtifactState::Available,
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare(artifact: &PreparedArtifact, seed: u8) -> PrepareOperation {
|
||||
PrepareOperation {
|
||||
operation_id: PrepareOperationId::new([seed; 32]),
|
||||
expected_artifact_id: artifact.prepared_artifact_id,
|
||||
source_identity: artifact.source_identity.clone(),
|
||||
stage_path: HostPath::new(format!("stages/{seed}").into_bytes()),
|
||||
device: DeviceId::new(u64::from(seed) + 200),
|
||||
inode: InodeId::new(u64::from(seed) + 300),
|
||||
}
|
||||
}
|
||||
|
||||
fn publish(&self, seed: u8) -> PreparedArtifact {
|
||||
let artifact = Self::artifact(seed);
|
||||
let operation = Self::prepare(&artifact, seed);
|
||||
self.catalog.record_prepare(&operation).unwrap();
|
||||
self.catalog.publish(&operation, &artifact).unwrap();
|
||||
artifact
|
||||
}
|
||||
}
|
||||
|
||||
fn lease(artifact: &PreparedArtifact, domain: u8, generation: u64) -> ArtifactLease {
|
||||
ArtifactLease {
|
||||
prepared_artifact_id: artifact.prepared_artifact_id,
|
||||
domain_id: DomainId::new([domain; 32]),
|
||||
generation_id: GenerationId::new(generation),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalog_reopens_with_committed_rows() {
|
||||
let fixture = Fixture::new();
|
||||
let artifact = fixture.publish(1);
|
||||
let first_lease = lease(&artifact, 10, 1);
|
||||
fixture.catalog.acquire_lease(&first_lease).unwrap();
|
||||
|
||||
let reopened = Catalog::new(
|
||||
fixture.directory.path().join("catalog.redb"),
|
||||
fixture.directory.path().join("storage-catalog.lock"),
|
||||
);
|
||||
let snapshot = reopened.snapshot().unwrap();
|
||||
assert_eq!(snapshot.prepared_artifacts, vec![artifact]);
|
||||
assert_eq!(snapshot.artifact_leases, vec![first_lease]);
|
||||
assert!(snapshot.prepare_operations.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn publication_is_one_exact_transaction() {
|
||||
let fixture = Fixture::new();
|
||||
let artifact = Fixture::artifact(2);
|
||||
let operation = Fixture::prepare(&artifact, 2);
|
||||
fixture.catalog.record_prepare(&operation).unwrap();
|
||||
|
||||
let mut forged = operation.clone();
|
||||
forged.inode = InodeId::new(999);
|
||||
assert!(matches!(
|
||||
fixture.catalog.publish(&forged, &artifact),
|
||||
Err(CatalogError::OwnershipMismatch {
|
||||
row: CatalogRow::PrepareOperation
|
||||
})
|
||||
));
|
||||
let before = fixture.catalog.snapshot().unwrap();
|
||||
assert_eq!(before.prepare_operations, vec![operation.clone()]);
|
||||
assert!(before.prepared_artifacts.is_empty());
|
||||
|
||||
fixture.catalog.publish(&operation, &artifact).unwrap();
|
||||
let after = fixture.catalog.snapshot().unwrap();
|
||||
assert!(after.prepare_operations.is_empty());
|
||||
assert_eq!(after.prepared_artifacts, vec![artifact]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_artifact_waits_for_every_lease() {
|
||||
let fixture = Fixture::new();
|
||||
let artifact = fixture.publish(3);
|
||||
let first = lease(&artifact, 1, 1);
|
||||
let second = lease(&artifact, 2, 9);
|
||||
fixture.catalog.acquire_lease(&first).unwrap();
|
||||
fixture.catalog.acquire_lease(&second).unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
fixture.catalog.begin_reclaim(&artifact),
|
||||
Err(CatalogError::LeasesPresent)
|
||||
));
|
||||
fixture.catalog.release_lease(&first).unwrap();
|
||||
assert!(matches!(
|
||||
fixture.catalog.begin_reclaim(&artifact),
|
||||
Err(CatalogError::LeasesPresent)
|
||||
));
|
||||
fixture.catalog.release_lease(&second).unwrap();
|
||||
let reclaiming = fixture.catalog.begin_reclaim(&artifact).unwrap();
|
||||
assert_eq!(reclaiming.state, ArtifactState::Reclaiming);
|
||||
fixture.catalog.finish_reclaim(&reclaiming).unwrap();
|
||||
assert!(
|
||||
fixture
|
||||
.catalog
|
||||
.snapshot()
|
||||
.unwrap()
|
||||
.prepared_artifacts
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_domain_generation_cannot_bind_two_artifacts() {
|
||||
let fixture = Fixture::new();
|
||||
let first_artifact = fixture.publish(5);
|
||||
let second_artifact = fixture.publish(6);
|
||||
let first = lease(&first_artifact, 4, 11);
|
||||
let conflicting = lease(&second_artifact, 4, 11);
|
||||
|
||||
fixture.catalog.acquire_lease(&first).unwrap();
|
||||
assert!(matches!(
|
||||
fixture.catalog.acquire_lease(&conflicting),
|
||||
Err(CatalogError::AlreadyExists {
|
||||
row: CatalogRow::ArtifactLease
|
||||
})
|
||||
));
|
||||
assert_eq!(
|
||||
fixture.catalog.snapshot().unwrap().artifact_leases,
|
||||
vec![first]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lease_and_reclaim_have_exactly_one_transaction_winner() {
|
||||
for seed in 10..30 {
|
||||
let fixture = Fixture::new();
|
||||
let artifact = fixture.publish(seed);
|
||||
let candidate = lease(&artifact, seed, u64::from(seed));
|
||||
let barrier = Arc::new(Barrier::new(3));
|
||||
|
||||
let lease_catalog = fixture.catalog.clone();
|
||||
let lease_barrier = Arc::clone(&barrier);
|
||||
let lease_handle = thread::spawn(move || {
|
||||
lease_barrier.wait();
|
||||
lease_catalog.acquire_lease(&candidate)
|
||||
});
|
||||
|
||||
let reclaim_catalog = fixture.catalog.clone();
|
||||
let reclaim_barrier = Arc::clone(&barrier);
|
||||
let reclaim_artifact = artifact.clone();
|
||||
let reclaim_handle = thread::spawn(move || {
|
||||
reclaim_barrier.wait();
|
||||
reclaim_catalog.begin_reclaim(&reclaim_artifact)
|
||||
});
|
||||
|
||||
barrier.wait();
|
||||
let lease_result = lease_handle.join().unwrap();
|
||||
let reclaim_result = reclaim_handle.join().unwrap();
|
||||
assert_ne!(lease_result.is_ok(), reclaim_result.is_ok());
|
||||
if lease_result.is_ok() {
|
||||
assert!(matches!(reclaim_result, Err(CatalogError::LeasesPresent)));
|
||||
} else {
|
||||
assert!(matches!(
|
||||
lease_result,
|
||||
Err(CatalogError::ArtifactUnavailable)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_row_deletion_refuses_forged_ownership() {
|
||||
let fixture = Fixture::new();
|
||||
let artifact = fixture.publish(4);
|
||||
let first = lease(&artifact, 1, 7);
|
||||
fixture.catalog.acquire_lease(&first).unwrap();
|
||||
|
||||
let forged_lease = lease(&artifact, 2, 7);
|
||||
assert!(matches!(
|
||||
fixture.catalog.release_lease(&forged_lease),
|
||||
Err(CatalogError::NotFound {
|
||||
row: CatalogRow::ArtifactLease
|
||||
})
|
||||
));
|
||||
assert_eq!(
|
||||
fixture.catalog.snapshot().unwrap().artifact_leases,
|
||||
vec![first]
|
||||
);
|
||||
|
||||
fixture.catalog.release_lease(&first).unwrap();
|
||||
let reclaiming = fixture.catalog.begin_reclaim(&artifact).unwrap();
|
||||
let mut forged_artifact = reclaiming.clone();
|
||||
forged_artifact.inode = InodeId::new(999);
|
||||
assert!(matches!(
|
||||
fixture.catalog.finish_reclaim(&forged_artifact),
|
||||
Err(CatalogError::OwnershipMismatch {
|
||||
row: CatalogRow::PreparedArtifact
|
||||
})
|
||||
));
|
||||
assert_eq!(
|
||||
fixture.catalog.snapshot().unwrap().prepared_artifacts,
|
||||
vec![reclaiming]
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UnknownMetadata<'a> {
|
||||
schema_id: &'a str,
|
||||
schema_version: u32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_version_refuses_before_rows() {
|
||||
let fixture = Fixture::new();
|
||||
let database = Database::open(fixture.directory.path().join("catalog.redb")).unwrap();
|
||||
let transaction = database.begin_write().unwrap();
|
||||
{
|
||||
let mut metadata = transaction.open_table(METADATA).unwrap();
|
||||
let encoded = encode(&UnknownMetadata {
|
||||
schema_id: "lazaret-storage-catalog-v1",
|
||||
schema_version: 2,
|
||||
})
|
||||
.unwrap();
|
||||
metadata.insert(0, encoded.as_slice()).unwrap();
|
||||
}
|
||||
transaction.commit().unwrap();
|
||||
drop(database);
|
||||
|
||||
assert!(matches!(
|
||||
fixture.catalog.snapshot(),
|
||||
Err(CatalogError::UnknownSchema)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_wait_is_bounded() {
|
||||
let fixture = Fixture::new();
|
||||
let lock_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(fixture.directory.path().join("storage-catalog.lock"))
|
||||
.unwrap();
|
||||
let _held = try_exclusive_flock(lock_file).unwrap();
|
||||
let impatient = fixture
|
||||
.catalog
|
||||
.clone()
|
||||
.with_lock_wait(Duration::from_millis(10));
|
||||
assert!(matches!(
|
||||
impatient.snapshot(),
|
||||
Err(CatalogError::LockTimeout)
|
||||
));
|
||||
}
|
||||
Loading…
Reference in a new issue