use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use crate::transport::{Replica, ReplicaSnapshot, TransportCode};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ManifestVersion(pub u64);
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VersionedManifest {
pub version: ManifestVersion,
pub fields: HashMap<String, String>,
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum ManifestStoreError {
#[error("the manifest register changed since it was read")]
Conflict,
#[error("the manifest register already exists")]
AlreadyExists,
#[error("manifest store unavailable: {0}")]
Unavailable(String),
#[error("manifest store: {0}")]
Backend(String),
}
#[async_trait]
pub trait ManifestStore: Send + Sync {
fn max_directory_bytes(&self) -> usize;
async fn read(&self) -> Result<Option<VersionedManifest>, ManifestStoreError>;
async fn create(
&self,
fields: HashMap<String, String>,
) -> Result<VersionedManifest, ManifestStoreError>;
async fn update(
&self,
version: ManifestVersion,
fields: HashMap<String, String>,
) -> Result<VersionedManifest, ManifestStoreError>;
}
pub(crate) struct GcsManifestStore {
replica: Arc<dyn Replica>,
}
pub(crate) const GCS_MAX_DIRECTORY_BYTES: usize = 6144;
impl GcsManifestStore {
pub(crate) fn new(replica: Arc<dyn Replica>) -> Self {
Self { replica }
}
fn versioned(snapshot: ReplicaSnapshot) -> VersionedManifest {
VersionedManifest {
version: ManifestVersion(snapshot.metageneration as u64),
fields: snapshot.metadata,
}
}
}
fn store_error(error: crate::transport::TransportError) -> ManifestStoreError {
match error.code {
TransportCode::FailedPrecondition => ManifestStoreError::Conflict,
TransportCode::AlreadyExists => ManifestStoreError::AlreadyExists,
code if code.transient() => ManifestStoreError::Unavailable(error.to_string()),
_ => ManifestStoreError::Backend(error.to_string()),
}
}
#[async_trait]
impl ManifestStore for GcsManifestStore {
fn max_directory_bytes(&self) -> usize {
GCS_MAX_DIRECTORY_BYTES
}
async fn read(&self) -> Result<Option<VersionedManifest>, ManifestStoreError> {
match self.replica.stat().await {
Ok(snapshot) => Ok(Some(Self::versioned(snapshot))),
Err(error) if error.code == TransportCode::NotFound => Ok(None),
Err(error) => Err(store_error(error)),
}
}
async fn create(
&self,
fields: HashMap<String, String>,
) -> Result<VersionedManifest, ManifestStoreError> {
match self.replica.create_register(fields).await {
Ok(snapshot) => Ok(Self::versioned(snapshot)),
Err(error) => Err(store_error(error)),
}
}
async fn update(
&self,
version: ManifestVersion,
fields: HashMap<String, String>,
) -> Result<VersionedManifest, ManifestStoreError> {
match self.replica.update_register(version.0 as i64, fields).await {
Ok(snapshot) => Ok(Self::versioned(snapshot)),
Err(error) => Err(store_error(error)),
}
}
}
#[cfg(test)]
pub(crate) mod test_support {
use std::collections::HashMap;
use std::sync::Mutex;
use async_trait::async_trait;
use super::{ManifestStore, ManifestStoreError, ManifestVersion, VersionedManifest};
#[derive(Default)]
pub(crate) struct InMemoryManifestStore {
state: Mutex<Option<(u64, HashMap<String, String>)>>,
}
#[async_trait]
impl ManifestStore for InMemoryManifestStore {
fn max_directory_bytes(&self) -> usize {
64 * 1024
}
async fn read(&self) -> Result<Option<VersionedManifest>, ManifestStoreError> {
Ok(self
.state
.lock()
.unwrap()
.as_ref()
.map(|(version, fields)| VersionedManifest {
version: ManifestVersion(*version),
fields: fields.clone(),
}))
}
async fn create(
&self,
fields: HashMap<String, String>,
) -> Result<VersionedManifest, ManifestStoreError> {
let mut state = self.state.lock().unwrap();
if state.is_some() {
return Err(ManifestStoreError::AlreadyExists);
}
*state = Some((1, fields.clone()));
Ok(VersionedManifest {
version: ManifestVersion(1),
fields,
})
}
async fn update(
&self,
version: ManifestVersion,
fields: HashMap<String, String>,
) -> Result<VersionedManifest, ManifestStoreError> {
let mut state = self.state.lock().unwrap();
let Some((current, stored)) = state.as_mut() else {
return Err(ManifestStoreError::Backend(
"the register was never created".into(),
));
};
if ManifestVersion(*current) != version {
return Err(ManifestStoreError::Conflict);
}
*current += 1;
*stored = fields.clone();
Ok(VersionedManifest {
version: ManifestVersion(*current),
fields,
})
}
}
#[tokio::test]
async fn in_memory_store_enforces_cas_semantics() {
let store = InMemoryManifestStore::default();
assert!(store.read().await.unwrap().is_none());
let initial = store
.create(HashMap::from([("k".to_string(), "1".to_string())]))
.await
.unwrap();
assert!(matches!(
store.create(HashMap::new()).await,
Err(ManifestStoreError::AlreadyExists)
));
let updated = store
.update(
initial.version,
HashMap::from([("k".to_string(), "2".to_string())]),
)
.await
.unwrap();
assert!(matches!(
store.update(initial.version, HashMap::new()).await,
Err(ManifestStoreError::Conflict)
));
assert_eq!(
store.read().await.unwrap().unwrap().version,
updated.version
);
}
}