Skip to main content

ursula_stream/
snapshot.rs

1use serde::Deserialize;
2use serde::Serialize;
3use ursula_shard::BucketStreamId;
4
5use crate::integrity::StreamIntegritySnapshot;
6use crate::model::BucketQuotaSnapshot;
7use crate::model::BucketUsageSnapshot;
8use crate::model::ColdChunkRef;
9use crate::model::ColdGcEntry;
10use crate::model::HotPayloadSegment;
11use crate::model::ObjectPayloadRef;
12use crate::model::ProducerSnapshot;
13use crate::model::StreamAttrs;
14use crate::model::StreamMessageRecord;
15use crate::model::StreamMetadata;
16use crate::model::StreamVisibleSnapshot;
17use crate::record_index::StreamRecordIndex;
18
19#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
20pub struct StreamSnapshot {
21    pub buckets: Vec<String>,
22    /// Permanent bucket-erasure fences. Absent in legacy snapshots.
23    #[serde(default)]
24    pub erased_buckets: Vec<String>,
25    pub streams: Vec<StreamSnapshotEntry>,
26    #[serde(default)]
27    pub pending_cold_gc: Vec<ColdGcEntry>,
28    #[serde(default)]
29    pub next_cold_gc_seq: u64,
30    /// Bucket erasure owners retained until a shared physical object is
31    /// deleted. Absent in snapshots written before bucket-scoped proof.
32    #[serde(default)]
33    pub shared_cold_object_owners: Vec<SharedColdObjectOwnersSnapshot>,
34    /// Monotonic per-bucket usage counters. Absent in legacy snapshots, in
35    /// which case the monotonic counters restart from the restored gauges.
36    #[serde(default)]
37    pub bucket_usage: Vec<BucketUsageSnapshot>,
38    /// Per-bucket data-plane quota records. Absent in legacy snapshots.
39    #[serde(default)]
40    pub bucket_quotas: Vec<BucketQuotaSnapshot>,
41}
42
43#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
44pub struct SharedColdObjectOwnersSnapshot {
45    pub s3_path: String,
46    pub bucket_ids: Vec<String>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct StreamSnapshotEntry {
51    pub metadata: StreamMetadata,
52    #[serde(default)]
53    pub attrs: Option<StreamAttrs>,
54    pub hot_start_offset: u64,
55    pub payload: Vec<u8>,
56    pub hot_segments: Vec<HotPayloadSegment>,
57    #[serde(default)]
58    pub cold_frontier_offset: u64,
59    #[serde(default)]
60    pub cold_index_generation: u64,
61    pub cold_chunks: Vec<ColdChunkRef>,
62    pub external_segments: Vec<ObjectPayloadRef>,
63    pub message_records: Vec<StreamMessageRecord>,
64    #[serde(default)]
65    pub record_index: Option<StreamRecordIndex>,
66    pub integrity: StreamIntegritySnapshot,
67    /// Independent destructive-retention floor. `None` denotes a legacy
68    /// snapshot where the visible snapshot offset also implied retention.
69    #[serde(default)]
70    pub retained_offset: Option<u64>,
71    pub visible_snapshot: Option<StreamVisibleSnapshot>,
72    pub producer_states: Vec<ProducerSnapshot>,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
76pub enum StreamSnapshotError {
77    #[error("snapshot contains duplicate bucket '{0}'")]
78    DuplicateBucket(String),
79    #[error("snapshot contains duplicate erased bucket '{0}'")]
80    DuplicateErasedBucket(String),
81    #[error("snapshot bucket '{0}' is both active and erased")]
82    ActiveBucketErased(String),
83    #[error("snapshot contains duplicate stream '{0}'")]
84    DuplicateStream(BucketStreamId),
85    #[error("snapshot stream '{stream_id}' contains duplicate producer '{producer_id}'")]
86    DuplicateProducer {
87        stream_id: BucketStreamId,
88        producer_id: String,
89    },
90    #[error("snapshot stream '{0}' references a missing bucket")]
91    MissingBucket(BucketStreamId),
92    #[error(
93        "snapshot stream '{stream_id}' tail offset {tail_offset} does not match payload length {payload_len}"
94    )]
95    PayloadLengthMismatch {
96        stream_id: BucketStreamId,
97        tail_offset: u64,
98        payload_len: usize,
99    },
100    #[error("snapshot stream '{stream_id}' has inconsistent message boundaries")]
101    MessageBoundaryMismatch { stream_id: BucketStreamId },
102    #[error("snapshot stream '{stream_id}' has inconsistent record boundaries")]
103    RecordBoundaryMismatch { stream_id: BucketStreamId },
104    #[error("snapshot stream '{stream_id}' has inconsistent integrity setsums")]
105    IntegrityMismatch { stream_id: BucketStreamId },
106    #[error(
107        "snapshot stream '{stream_id}' visible snapshot offset {snapshot_offset} is beyond tail offset {tail_offset}"
108    )]
109    SnapshotOffsetOutOfRange {
110        stream_id: BucketStreamId,
111        snapshot_offset: u64,
112        tail_offset: u64,
113    },
114}