1use std::collections::HashMap;
13use std::sync::Arc;
14
15use async_trait::async_trait;
16
17use crate::transport::{Replica, ReplicaSnapshot, TransportCode};
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub struct ManifestVersion(pub u64);
29
30#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct VersionedManifest {
34 pub version: ManifestVersion,
36 pub fields: HashMap<String, String>,
38}
39
40#[derive(Clone, Debug, thiserror::Error)]
42pub enum ManifestStoreError {
43 #[error("the manifest register changed since it was read")]
46 Conflict,
47 #[error("the manifest register already exists")]
49 AlreadyExists,
50 #[error("manifest store unavailable: {0}")]
52 Unavailable(String),
53 #[error("manifest store: {0}")]
55 Backend(String),
56}
57
58#[async_trait]
80pub trait ManifestStore: Send + Sync {
81 fn max_directory_bytes(&self) -> usize;
87
88 async fn read(&self) -> Result<Option<VersionedManifest>, ManifestStoreError>;
90
91 async fn create(
93 &self,
94 fields: HashMap<String, String>,
95 ) -> Result<VersionedManifest, ManifestStoreError>;
96
97 async fn update(
99 &self,
100 version: ManifestVersion,
101 fields: HashMap<String, String>,
102 ) -> Result<VersionedManifest, ManifestStoreError>;
103}
104
105pub(crate) struct GcsManifestStore {
108 replica: Arc<dyn Replica>,
109}
110
111pub(crate) const GCS_MAX_DIRECTORY_BYTES: usize = 6144;
115
116impl GcsManifestStore {
117 pub(crate) fn new(replica: Arc<dyn Replica>) -> Self {
118 Self { replica }
119 }
120
121 fn versioned(snapshot: ReplicaSnapshot) -> VersionedManifest {
122 VersionedManifest {
123 version: ManifestVersion(snapshot.metageneration as u64),
124 fields: snapshot.metadata,
125 }
126 }
127}
128
129fn store_error(error: crate::transport::TransportError) -> ManifestStoreError {
130 match error.code {
131 TransportCode::FailedPrecondition => ManifestStoreError::Conflict,
132 TransportCode::AlreadyExists => ManifestStoreError::AlreadyExists,
133 code if code.transient() => ManifestStoreError::Unavailable(error.to_string()),
134 _ => ManifestStoreError::Backend(error.to_string()),
135 }
136}
137
138#[async_trait]
139impl ManifestStore for GcsManifestStore {
140 fn max_directory_bytes(&self) -> usize {
141 GCS_MAX_DIRECTORY_BYTES
142 }
143
144 async fn read(&self) -> Result<Option<VersionedManifest>, ManifestStoreError> {
145 match self.replica.stat().await {
148 Ok(snapshot) => Ok(Some(Self::versioned(snapshot))),
149 Err(error) if error.code == TransportCode::NotFound => Ok(None),
150 Err(error) => Err(store_error(error)),
151 }
152 }
153
154 async fn create(
155 &self,
156 fields: HashMap<String, String>,
157 ) -> Result<VersionedManifest, ManifestStoreError> {
158 match self.replica.create_register(fields).await {
159 Ok(snapshot) => Ok(Self::versioned(snapshot)),
160 Err(error) => Err(store_error(error)),
161 }
162 }
163
164 async fn update(
165 &self,
166 version: ManifestVersion,
167 fields: HashMap<String, String>,
168 ) -> Result<VersionedManifest, ManifestStoreError> {
169 match self.replica.update_register(version.0 as i64, fields).await {
170 Ok(snapshot) => Ok(Self::versioned(snapshot)),
171 Err(error) => Err(store_error(error)),
172 }
173 }
174}
175
176#[cfg(test)]
177pub(crate) mod test_support {
178 use std::collections::HashMap;
179 use std::sync::Mutex;
180
181 use async_trait::async_trait;
182
183 use super::{ManifestStore, ManifestStoreError, ManifestVersion, VersionedManifest};
184
185 #[derive(Default)]
188 pub(crate) struct InMemoryManifestStore {
189 state: Mutex<Option<(u64, HashMap<String, String>)>>,
190 }
191
192 #[async_trait]
193 impl ManifestStore for InMemoryManifestStore {
194 fn max_directory_bytes(&self) -> usize {
195 64 * 1024
196 }
197
198 async fn read(&self) -> Result<Option<VersionedManifest>, ManifestStoreError> {
199 Ok(self
200 .state
201 .lock()
202 .unwrap()
203 .as_ref()
204 .map(|(version, fields)| VersionedManifest {
205 version: ManifestVersion(*version),
206 fields: fields.clone(),
207 }))
208 }
209
210 async fn create(
211 &self,
212 fields: HashMap<String, String>,
213 ) -> Result<VersionedManifest, ManifestStoreError> {
214 let mut state = self.state.lock().unwrap();
215 if state.is_some() {
216 return Err(ManifestStoreError::AlreadyExists);
217 }
218 *state = Some((1, fields.clone()));
219 Ok(VersionedManifest {
220 version: ManifestVersion(1),
221 fields,
222 })
223 }
224
225 async fn update(
226 &self,
227 version: ManifestVersion,
228 fields: HashMap<String, String>,
229 ) -> Result<VersionedManifest, ManifestStoreError> {
230 let mut state = self.state.lock().unwrap();
231 let Some((current, stored)) = state.as_mut() else {
232 return Err(ManifestStoreError::Backend(
233 "the register was never created".into(),
234 ));
235 };
236 if ManifestVersion(*current) != version {
237 return Err(ManifestStoreError::Conflict);
238 }
239 *current += 1;
240 *stored = fields.clone();
241 Ok(VersionedManifest {
242 version: ManifestVersion(*current),
243 fields,
244 })
245 }
246 }
247
248 #[tokio::test]
249 async fn in_memory_store_enforces_cas_semantics() {
250 let store = InMemoryManifestStore::default();
251 assert!(store.read().await.unwrap().is_none());
252 let initial = store
253 .create(HashMap::from([("k".to_string(), "1".to_string())]))
254 .await
255 .unwrap();
256 assert!(matches!(
257 store.create(HashMap::new()).await,
258 Err(ManifestStoreError::AlreadyExists)
259 ));
260 let updated = store
261 .update(
262 initial.version,
263 HashMap::from([("k".to_string(), "2".to_string())]),
264 )
265 .await
266 .unwrap();
267 assert!(matches!(
268 store.update(initial.version, HashMap::new()).await,
269 Err(ManifestStoreError::Conflict)
270 ));
271 assert_eq!(
272 store.read().await.unwrap().unwrap().version,
273 updated.version
274 );
275 }
276}