harmos_storage/state/
mod.rs1mod admission;
13pub mod config;
14pub mod file;
15mod reader;
16
17pub use admission::Permit;
20pub use config::StorageStateSettings;
21pub use reader::RecoveryReader;
22
23use serde::{Deserialize, Serialize};
29use super::{ReadLimits, RecoveryPosition, StorageBatch, StorageFuture, StoreHistory};
31use crate::{Error, MemorySize, Result};
32
33#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(deny_unknown_fields)]
43pub struct RecoveryEntry {
44 pub required_data: super::StorageCursor,
45 pub position: RecoveryPosition,
46
47 #[serde(with = "payload")]
48 pub payload: Vec<u8>,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct RecoverySnapshot {
54 pub required_data: super::StorageCursor,
55 pub through: RecoveryPosition,
56
57 #[serde(with = "payload")]
58 pub payload: Vec<u8>,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct RecoveryPage {
63 pub entries: Vec<RecoveryEntry>,
64 pub next: Option<RecoveryPosition>,
65}
66
67pub trait StateBackend: Send + Sync + 'static {
72 fn history(&self) -> &StoreHistory;
73 fn open(&self) -> StorageFuture<'_, RecoveryPosition>;
74 fn read_snapshot(&self, max_bytes: std::num::NonZeroUsize) -> StorageFuture<'_, Option<RecoverySnapshot>>;
75 fn write_snapshot(&self, snapshot: StorageBatch<RecoverySnapshot>) -> StorageFuture<'_, ()>;
76 fn read_entries<'a>(
77 &'a self, after: &'a RecoveryPosition, through: &'a RecoveryPosition, limits: ReadLimits,
78 ) -> StorageFuture<'a, RecoveryPage>;
79 fn write_entries(&self, entries: StorageBatch<RecoveryEntry>) -> StorageFuture<'_, ()>;
80 fn flush<'a>(&'a self, through: &'a RecoveryPosition) -> StorageFuture<'a, RecoveryPosition>;
81 fn close(&self) -> StorageFuture<'_, ()>;
82}
83
84impl MemorySize for RecoveryEntry {
87 fn memory_size(&self) -> usize {
88 std::mem::size_of::<Self>()
89 .saturating_add(self.required_data.memory_size().saturating_sub(std::mem::size_of::<super::StorageCursor>()))
90 .saturating_add(self.position.history.memory_size().saturating_sub(std::mem::size_of::<StoreHistory>()))
91 .saturating_add(self.payload.capacity())
92 }
93}
94
95impl MemorySize for RecoverySnapshot {
96 fn memory_size(&self) -> usize {
97 std::mem::size_of::<Self>()
98 .saturating_add(self.required_data.memory_size().saturating_sub(std::mem::size_of::<super::StorageCursor>()))
99 .saturating_add(self.through.history.memory_size().saturating_sub(std::mem::size_of::<StoreHistory>()))
100 .saturating_add(self.payload.capacity())
101 }
102}
103
104impl MemorySize for RecoveryPage {
105 fn memory_size(&self) -> usize {
106 std::mem::size_of::<Self>()
107 .saturating_add(self.entries.memory_size().saturating_sub(std::mem::size_of::<Vec<RecoveryEntry>>()))
108 .saturating_add(
109 self.next.as_ref().map_or(0, |next| next.history.memory_size().saturating_sub(std::mem::size_of::<StoreHistory>())),
110 )
111 }
112}
113
114impl RecoveryPage {
115 pub fn check(
116 &self, after: &RecoveryPosition, through: &RecoveryPosition, limits: ReadLimits,
117 mut encoded_bytes: impl FnMut(&RecoveryEntry) -> usize,
118 ) -> Result<()> {
119 if !through.covers(after)? {
120 return Err(Error::InvalidInput {
121 field: "recovery range",
122 reason: "start exceeds fixed frontier",
123 });
124 }
125 let mut previous = after.sequence;
126 let mut bytes = 0usize;
127
128 for entry in &self.entries {
129 after.history.check(&entry.position.history)?;
130 let expected = previous.checked_add(1).ok_or(Error::InvalidInput {
131 field: "recovery page",
132 reason: "position overflow",
133 })?;
134 if entry.position.sequence < expected {
135 return Err(Error::InvalidInput {
136 field: "recovery page",
137 reason: "duplicate or reversed position",
138 });
139 }
140 if entry.position.sequence > expected {
141 return Err(Error::Gap {
142 requested: expected,
143 available: entry.position.sequence,
144 });
145 }
146 if entry.position.sequence > through.sequence {
147 return Err(Error::InvalidInput {
148 field: "recovery page",
149 reason: "entry exceeds fixed frontier",
150 });
151 }
152 bytes = bytes.checked_add(encoded_bytes(entry)).ok_or(Error::InvalidInput {
153 field: "recovery page",
154 reason: "encoded size overflow",
155 })?;
156 previous = entry.position.sequence;
157 }
158 limits.check(self.entries.len(), bytes)?;
159
160 let expected_next = if previous < through.sequence {
161 Some(RecoveryPosition {
162 history: after.history.clone(),
163 sequence: previous,
164 })
165 } else {
166 None
167 };
168 if self.next != expected_next || (self.entries.is_empty() && expected_next.is_some()) {
169 return Err(Error::InvalidInput {
170 field: "recovery page",
171 reason: "missing progress or invalid continuation",
172 });
173 }
174
175 Ok(())
176 }
177}
178
179mod payload {
185 use base64::Engine;
186 use base64::engine::general_purpose::STANDARD_NO_PAD;
187 use serde::{Deserialize, Deserializer, Serializer, de::Error};
188
189 pub(super) fn serialize<S: Serializer>(payload: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
190 serializer.serialize_str(&STANDARD_NO_PAD.encode(payload))
191 }
192
193 pub(super) fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
194 let encoded = String::deserialize(deserializer)?;
195 STANDARD_NO_PAD.decode(encoded.as_bytes()).map_err(D::Error::custom)
196 }
197}
198
199