1use std::fmt;
4
5use async_trait::async_trait;
6
7use crate::{
8 ByteRange, ChangeBatch, ChangeCursor, CloudScope, ContentCacheKey, ContentCacheWriteIntent,
9 ContentCacheWriteOperationId, ContentCacheWriteRecord, ContentEvictionBegin,
10 ContentEvictionIntent, ContentEvictionOperationId, ContentEvictionPhysicalEffect,
11 ContentEvictionRecord, ContentLeaseId, ContentLeaseKind, ContentStorageEntry,
12 ContentUploadIntent, ContentUploadRecord, ContentUploadSession, LocalContentGeneration,
13 LocalContentSnapshot, MutationIntent, MutationRecord, MutationRemoteOutcome, OperationId,
14 PlatformMaterializationState, SessionGeneration, SessionState,
15};
16
17pub type StoreResult<T> = std::result::Result<T, CloudFilesStoreError>;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct RecoveryPage<T> {
23 items: Vec<T>,
24 has_more: bool,
25}
26
27impl<T> RecoveryPage<T> {
28 #[must_use]
30 pub fn new(items: Vec<T>, has_more: bool) -> Self {
31 Self { items, has_more }
32 }
33
34 #[must_use]
36 pub fn items(&self) -> &[T] {
37 &self.items
38 }
39
40 #[must_use]
42 pub const fn has_more(&self) -> bool {
43 self.has_more
44 }
45
46 #[must_use]
48 pub fn into_items(self) -> Vec<T> {
49 self.items
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub enum CloudFilesStoreErrorKind {
56 NotFound,
58 Conflict,
60 InvalidTransition,
62 PersistenceFailure,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
68#[error("cloud-files store {kind:?}: {context}")]
69pub struct CloudFilesStoreError {
70 kind: CloudFilesStoreErrorKind,
71 context: String,
72}
73
74impl CloudFilesStoreError {
75 pub fn new(kind: CloudFilesStoreErrorKind, context: impl Into<String>) -> Self {
77 Self {
78 kind,
79 context: context.into(),
80 }
81 }
82
83 #[must_use]
85 pub const fn kind(&self) -> CloudFilesStoreErrorKind {
86 self.kind
87 }
88
89 #[must_use]
91 pub fn context(&self) -> &str {
92 &self.context
93 }
94}
95
96#[derive(Clone, PartialEq, Eq, Hash)]
98pub struct ChangeBatchId(String);
99
100impl ChangeBatchId {
101 pub fn new(value: impl Into<String>) -> crate::Result<Self> {
107 let value = value.into();
108 if value.is_empty() {
109 return Err(crate::CloudFilesCoreError::empty("change batch id"));
110 }
111 Ok(Self(value))
112 }
113
114 #[must_use]
116 pub fn as_str(&self) -> &str {
117 &self.0
118 }
119}
120
121impl fmt::Debug for ChangeBatchId {
122 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123 formatter
124 .debug_struct("ChangeBatchId")
125 .field("byte_len", &self.0.len())
126 .finish()
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
132pub enum PersistedChangeBatchState {
133 Recorded,
135 EffectsReplayable,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct PersistedChangeBatch {
142 id: ChangeBatchId,
143 scope: CloudScope,
144 base_cursor: Option<ChangeCursor>,
145 batch: ChangeBatch,
146 state: PersistedChangeBatchState,
147}
148
149impl PersistedChangeBatch {
150 #[must_use]
152 pub const fn new(
153 id: ChangeBatchId,
154 scope: CloudScope,
155 base_cursor: Option<ChangeCursor>,
156 batch: ChangeBatch,
157 ) -> Self {
158 Self {
159 id,
160 scope,
161 base_cursor,
162 batch,
163 state: PersistedChangeBatchState::Recorded,
164 }
165 }
166
167 #[must_use]
169 pub const fn id(&self) -> &ChangeBatchId {
170 &self.id
171 }
172
173 #[must_use]
175 pub const fn scope(&self) -> &CloudScope {
176 &self.scope
177 }
178
179 #[must_use]
181 pub const fn base_cursor(&self) -> Option<&ChangeCursor> {
182 self.base_cursor.as_ref()
183 }
184
185 #[must_use]
187 pub const fn batch(&self) -> &ChangeBatch {
188 &self.batch
189 }
190
191 #[must_use]
193 pub const fn state(&self) -> PersistedChangeBatchState {
194 self.state
195 }
196
197 pub fn mark_effects_replayable(&mut self) -> bool {
199 if self.state == PersistedChangeBatchState::EffectsReplayable {
200 return false;
201 }
202 self.state = PersistedChangeBatchState::EffectsReplayable;
203 true
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
209pub struct ChangeCursorCheckpoint {
210 scope: CloudScope,
211 active_cursor: Option<ChangeCursor>,
212 pending_batch: Option<PersistedChangeBatch>,
213}
214
215impl ChangeCursorCheckpoint {
216 #[must_use]
218 pub const fn initial(scope: CloudScope) -> Self {
219 Self {
220 scope,
221 active_cursor: None,
222 pending_batch: None,
223 }
224 }
225
226 #[must_use]
228 pub const fn new(
229 scope: CloudScope,
230 active_cursor: Option<ChangeCursor>,
231 pending_batch: Option<PersistedChangeBatch>,
232 ) -> Self {
233 Self {
234 scope,
235 active_cursor,
236 pending_batch,
237 }
238 }
239
240 #[must_use]
242 pub const fn scope(&self) -> &CloudScope {
243 &self.scope
244 }
245
246 #[must_use]
248 pub const fn active_cursor(&self) -> Option<&ChangeCursor> {
249 self.active_cursor.as_ref()
250 }
251
252 #[must_use]
254 pub const fn pending_batch(&self) -> Option<&PersistedChangeBatch> {
255 self.pending_batch.as_ref()
256 }
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
261pub enum StoreWriteStatus {
262 Applied,
264 AlreadyApplied,
266 Fenced,
268}
269
270#[async_trait]
272pub trait ChangeCheckpointStore: Send + Sync {
273 async fn load_change_checkpoint(
275 &self,
276 scope: &CloudScope,
277 ) -> StoreResult<ChangeCursorCheckpoint>;
278
279 async fn record_change_batch(
281 &self,
282 batch: PersistedChangeBatch,
283 ) -> StoreResult<StoreWriteStatus>;
284
285 async fn mark_change_effects_replayable(
287 &self,
288 scope: &CloudScope,
289 batch_id: &ChangeBatchId,
290 ) -> StoreResult<StoreWriteStatus>;
291
292 async fn commit_change_cursor(
296 &self,
297 scope: &CloudScope,
298 batch_id: &ChangeBatchId,
299 ) -> StoreResult<StoreWriteStatus>;
300}
301
302#[async_trait]
308pub trait ContentStorageStore: Send + Sync {
309 async fn create_content_entry(
311 &self,
312 entry: ContentStorageEntry,
313 ) -> StoreResult<StoreWriteStatus>;
314
315 async fn load_content_entry(
317 &self,
318 key: &ContentCacheKey,
319 ) -> StoreResult<Option<ContentStorageEntry>>;
320
321 async fn record_provider_cached_range(
323 &self,
324 key: &ContentCacheKey,
325 range: ByteRange,
326 ) -> StoreResult<StoreWriteStatus>;
327
328 async fn observe_platform_materialization(
330 &self,
331 key: &ContentCacheKey,
332 state: PlatformMaterializationState,
333 ) -> StoreResult<StoreWriteStatus>;
334
335 async fn set_content_pinned(
337 &self,
338 key: &ContentCacheKey,
339 pinned: bool,
340 ) -> StoreResult<StoreWriteStatus>;
341
342 async fn mark_content_dirty(
344 &self,
345 key: &ContentCacheKey,
346 snapshot: LocalContentSnapshot,
347 ) -> StoreResult<StoreWriteStatus>;
348
349 async fn clear_content_dirty(
351 &self,
352 key: &ContentCacheKey,
353 generation: LocalContentGeneration,
354 ) -> StoreResult<StoreWriteStatus>;
355
356 async fn acquire_content_lease(
358 &self,
359 key: &ContentCacheKey,
360 lease_id: ContentLeaseId,
361 kind: ContentLeaseKind,
362 ) -> StoreResult<StoreWriteStatus>;
363
364 async fn release_content_lease(
366 &self,
367 key: &ContentCacheKey,
368 lease_id: &ContentLeaseId,
369 ) -> StoreResult<StoreWriteStatus>;
370
371 async fn begin_content_eviction(
373 &self,
374 intent: ContentEvictionIntent,
375 ) -> StoreResult<ContentEvictionBegin>;
376
377 async fn load_content_eviction(
379 &self,
380 operation_id: &ContentEvictionOperationId,
381 ) -> StoreResult<Option<ContentEvictionRecord>>;
382
383 async fn recoverable_content_evictions_page(
387 &self,
388 scope: &CloudScope,
389 after_operation_id: Option<&str>,
390 limit: usize,
391 ) -> StoreResult<RecoveryPage<ContentEvictionRecord>>;
392
393 async fn record_content_eviction_effect(
395 &self,
396 operation_id: &ContentEvictionOperationId,
397 effect: ContentEvictionPhysicalEffect,
398 ) -> StoreResult<StoreWriteStatus>;
399
400 async fn reconcile_content_eviction_metadata(
402 &self,
403 operation_id: &ContentEvictionOperationId,
404 ) -> StoreResult<StoreWriteStatus>;
405
406 async fn complete_content_eviction(
408 &self,
409 operation_id: &ContentEvictionOperationId,
410 ) -> StoreResult<StoreWriteStatus>;
411}
412
413#[async_trait]
418pub trait ContentCacheWriteStore: Send + Sync {
419 async fn begin_content_cache_write(
421 &self,
422 intent: ContentCacheWriteIntent,
423 ) -> StoreResult<StoreWriteStatus>;
424
425 async fn load_content_cache_write(
427 &self,
428 operation_id: &ContentCacheWriteOperationId,
429 ) -> StoreResult<Option<ContentCacheWriteRecord>>;
430
431 async fn recoverable_content_cache_writes_page(
433 &self,
434 scope: &CloudScope,
435 after_operation_id: Option<&str>,
436 limit: usize,
437 ) -> StoreResult<RecoveryPage<ContentCacheWriteRecord>>;
438
439 async fn record_content_cache_write_physical_commit(
441 &self,
442 operation_id: &ContentCacheWriteOperationId,
443 ) -> StoreResult<StoreWriteStatus>;
444
445 async fn reconcile_content_cache_write_coverage(
447 &self,
448 operation_id: &ContentCacheWriteOperationId,
449 ) -> StoreResult<StoreWriteStatus>;
450
451 async fn complete_content_cache_write(
453 &self,
454 operation_id: &ContentCacheWriteOperationId,
455 ) -> StoreResult<StoreWriteStatus>;
456}
457
458#[async_trait]
466pub trait ContentUploadStore: Send + Sync {
467 async fn persist_content_upload_intent(
469 &self,
470 intent: ContentUploadIntent,
471 execution_generation: SessionGeneration,
472 ) -> StoreResult<StoreWriteStatus>;
473
474 async fn load_content_upload(
476 &self,
477 operation_id: &OperationId,
478 ) -> StoreResult<Option<ContentUploadRecord>>;
479
480 async fn recoverable_content_uploads_page(
482 &self,
483 scope: &CloudScope,
484 after_operation_id: Option<&str>,
485 limit: usize,
486 ) -> StoreResult<RecoveryPage<ContentUploadRecord>>;
487
488 async fn record_content_upload_session(
490 &self,
491 operation_id: &OperationId,
492 session: ContentUploadSession,
493 execution_generation: SessionGeneration,
494 ) -> StoreResult<StoreWriteStatus>;
495
496 async fn begin_content_upload_remote_commit(
498 &self,
499 operation_id: &OperationId,
500 execution_generation: SessionGeneration,
501 ) -> StoreResult<StoreWriteStatus>;
502
503 async fn record_content_upload_remote_outcome(
505 &self,
506 operation_id: &OperationId,
507 outcome: MutationRemoteOutcome,
508 execution_generation: SessionGeneration,
509 ) -> StoreResult<StoreWriteStatus>;
510
511 async fn reconcile_content_upload_metadata(
513 &self,
514 operation_id: &OperationId,
515 execution_generation: SessionGeneration,
516 ) -> StoreResult<StoreWriteStatus>;
517
518 async fn complete_content_upload(
520 &self,
521 operation_id: &OperationId,
522 execution_generation: SessionGeneration,
523 ) -> StoreResult<StoreWriteStatus>;
524}
525
526#[async_trait]
528pub trait MutationJournalStore: Send + Sync {
529 async fn activate_session(
531 &self,
532 scope: &CloudScope,
533 generation: SessionGeneration,
534 ) -> StoreResult<StoreWriteStatus>;
535
536 async fn load_session(
538 &self,
539 scope: &CloudScope,
540 ) -> StoreResult<Option<(SessionGeneration, SessionState)>>;
541
542 async fn transition_session(
544 &self,
545 scope: &CloudScope,
546 generation: SessionGeneration,
547 next: SessionState,
548 ) -> StoreResult<StoreWriteStatus>;
549
550 async fn persist_mutation_intent(
552 &self,
553 intent: MutationIntent,
554 ) -> StoreResult<StoreWriteStatus>;
555
556 async fn load_mutation(
558 &self,
559 operation_id: &OperationId,
560 ) -> StoreResult<Option<MutationRecord>>;
561
562 async fn recoverable_mutations_page(
564 &self,
565 scope: &CloudScope,
566 after_operation_id: Option<&str>,
567 limit: usize,
568 ) -> StoreResult<RecoveryPage<MutationRecord>>;
569
570 async fn begin_remote_apply(
575 &self,
576 operation_id: &OperationId,
577 generation: SessionGeneration,
578 ) -> StoreResult<StoreWriteStatus>;
579
580 async fn record_remote_outcome(
582 &self,
583 operation_id: &OperationId,
584 generation: SessionGeneration,
585 outcome: MutationRemoteOutcome,
586 ) -> StoreResult<StoreWriteStatus>;
587
588 async fn mark_platform_reconciled(
595 &self,
596 operation_id: &OperationId,
597 generation: SessionGeneration,
598 ) -> StoreResult<StoreWriteStatus>;
599
600 async fn complete_mutation(
602 &self,
603 operation_id: &OperationId,
604 generation: SessionGeneration,
605 ) -> StoreResult<StoreWriteStatus>;
606}