Skip to main content

harmos_storage/
objects.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10/* === Modules === */
11
12/* === Re-Exports === */
13
14/* === Imports === */
15
16// Standard Library Imports
17use std::num::NonZeroUsize;
18// Third Party Imports
19// Local Imports
20use super::capacity::{Pool, Refusal, Reservation};
21use super::manager::{Domain, ENTRY_OVERHEAD, Operation};
22use super::{
23    ObjectCatalog, ObjectEntry, ObjectPoint, ObjectReceipt, ObjectRevision, ObjectValue, ObjectVersion, ObjectVersionToken, StorageManager,
24    StorageSchema, StoredObject,
25};
26use crate::{Error, MemorySize, Result};
27use harmos_domain::catalog::Contains;
28use harmos_domain::model::storage::catalog::{decode_admitted, object_schema};
29use harmos_domain::model::storage::object_key;
30
31/* === Constants === */
32
33/* === Macros === */
34
35/* === Type Aliases === */
36
37/* === Structs === */
38
39pub struct ObjectGroup<'a, C> {
40    manager: &'a StorageManager<C>,
41    first: u64,
42    count: usize,
43    allowance: usize,
44    used: usize,
45    schemas: Vec<StorageSchema>,
46    objects: Vec<ObjectEntry>,
47    receipts: Vec<ObjectReceipt>,
48}
49
50/* === Enums === */
51
52/* === Traits === */
53
54/* === Implementations === */
55
56impl<C: ObjectCatalog> ObjectGroup<'_, C> {
57    pub fn remaining_bytes(&self) -> usize {
58        self.allowance - self.used
59    }
60
61    pub fn write<T: StoredObject>(&mut self, value: &T) -> Result<()>
62    where
63        C: Contains<T>,
64    {
65        if self.objects.len() == self.count {
66            return Err(invalid("object group contains too many objects"));
67        }
68        let settings = self.schema_settings()?;
69        let graph = crate::schema::graph::Schema::from_declaration(T::DEFINITION, &settings)
70            .map_err(|_| invalid("invalid object schema or preparation allowance"))?;
71        let (id, _) = T::definition();
72        if graph
73            .memory_size()
74            .checked_add(id.as_str().len())
75            .and_then(|bytes| bytes.checked_add(std::mem::size_of::<super::SchemaKey>()))
76            .is_none_or(|bytes| bytes > self.remaining_bytes())
77        {
78            return Err(invalid("object identity exceeds allowance"));
79        }
80        let schema = object_schema::<T>()?;
81        let metadata = self.metadata(&schema, &graph)?;
82        if metadata >= self.remaining_bytes() {
83            return Err(invalid("object metadata exceeds allowance"));
84        }
85        let limit = NonZeroUsize::new((self.remaining_bytes() - metadata) / 2).ok_or_else(|| invalid("object key exceeds allowance"))?;
86        let item = object_key(value.key(), limit)?;
87        let key_bytes = item.memory_size();
88        let length = value.encoded_len();
89        let bytes =
90            metadata.checked_add(key_bytes).and_then(|bytes| bytes.checked_add(length)).ok_or_else(|| invalid("object size overflow"))?;
91        if bytes > self.remaining_bytes() {
92            return Err(invalid("object payload exceeds allowance"));
93        }
94        let payload = value.encode_to_vec();
95        if payload.len() != length || payload.capacity() > self.remaining_bytes() - metadata - key_bytes {
96            return Err(invalid("object codec exceeded its reported allowance"));
97        }
98        self.append(ObjectValue { schema, item, payload }, graph, metadata + key_bytes)?;
99        Ok(())
100    }
101}
102
103impl<C> ObjectGroup<'_, C> {
104    pub(super) fn write_value(&mut self, value: ObjectValue) -> Result<()> {
105        if self.objects.len() == self.count {
106            return Err(invalid("object group contains too many objects"));
107        }
108        // Catalog Identity Construction And Verification Share The Original Claim.
109        if self.allowance - self.used < harmos_domain::model::storage::catalog::LOOKUP_MEMORY_BYTES {
110            return Err(invalid("object catalog lookup exceeds allowance"));
111        }
112        let definition = (self.manager.inner.object_definition)(&value.schema)?;
113        let graph = definition.graph(&self.schema_settings()?)?;
114        let metadata = self.metadata(&value.schema, &graph)?;
115        let verification =
116            (self.allowance - self.used).checked_sub(metadata).ok_or_else(|| invalid("object metadata exceeds allowance"))?;
117        definition.validate_admitted(&value, self.manager.inner.settings.max_batch_bytes(), verification)?;
118        let retained = metadata
119            .checked_add(value.item.memory_size())
120            .and_then(|bytes| bytes.checked_add(value.payload.capacity()))
121            .ok_or_else(|| invalid("object size overflow"))?;
122        if retained > self.allowance - self.used {
123            return Err(invalid("object payload exceeds allowance"));
124        }
125        let bytes = metadata + value.item.memory_size();
126        self.append(value, graph, bytes)
127    }
128
129    fn schema_settings(&self) -> Result<harmos_domain::config::SchemaSettings> {
130        let mut settings = harmos_domain::config::SchemaSettings::default();
131        settings.preparation_bytes = NonZeroUsize::new(settings.preparation_bytes.get().min(self.allowance - self.used))
132            .ok_or_else(|| invalid("object schema preparation exceeds allowance"))?;
133        Ok(settings)
134    }
135
136    fn metadata(&self, schema: &super::SchemaKey, graph: &crate::schema::graph::Schema) -> Result<usize> {
137        let descriptor = StorageSchema::resident_bytes(schema, graph, self.manager.inner.primary.history())?;
138        let clone = graph.clone_memory_bytes().map_err(|_| invalid("object schema size overflow"))?;
139        let retained = graph.memory_size().checked_sub(clone).ok_or_else(|| invalid("object schema clone exceeds retained allocation"))?;
140        let identity = self.manager.charge(&(schema, self.manager.inner.primary.history()), 4)?;
141        descriptor.checked_add(retained).and_then(|bytes| bytes.checked_add(identity)).ok_or_else(|| invalid("object size overflow"))
142    }
143
144    fn append(&mut self, value: ObjectValue, graph: crate::schema::graph::Schema, metadata: usize) -> Result<()> {
145        let bytes = metadata.checked_add(value.payload.capacity()).ok_or_else(|| invalid("object size overflow"))?;
146        if bytes > self.allowance - self.used {
147            return Err(invalid("object payload exceeds allowance"));
148        }
149        let sequence = self.first + (self.objects.len() * 2) as u64;
150        let descriptor = StorageSchema::new(self.manager.position(sequence), value.schema.clone(), graph);
151        let position = self.manager.position(sequence + 1);
152        let version = ObjectVersionToken::new(position.clone());
153        self.used += bytes;
154        self.schemas.push(descriptor);
155        self.objects.push(ObjectEntry { position: position.clone(), version: version.clone(), value });
156        self.receipts.push(ObjectReceipt { position, version });
157        Ok(())
158    }
159}
160
161impl<C: ObjectCatalog> StorageManager<C> {
162    #[tracing::instrument(name = "storage.objects.write_object", level = "debug", skip_all)]
163    pub async fn write_object<T: StoredObject>(&self, value: &T) -> Result<ObjectReceipt>
164    where
165        C: Contains<T>,
166    {
167        let receipts = self.write_object_group(self.object_admission_bytes(), NonZeroUsize::MIN, |group| group.write(value)).await?;
168        Ok(receipts.into_iter().next().unwrap())
169    }
170
171    #[tracing::instrument(name = "storage.objects.write_object_group", level = "debug", skip_all)]
172    pub async fn write_object_group(
173        &self, max_bytes: NonZeroUsize, count: NonZeroUsize, build: impl FnOnce(&mut ObjectGroup<'_, C>) -> Result<()> + Send,
174    ) -> Result<Vec<ObjectReceipt>> {
175        self.admit_object_group(max_bytes, count, None, build).await
176    }
177
178    #[tracing::instrument(name = "storage.objects.read_object", level = "debug", skip_all)]
179    pub async fn read_object<T: StoredObject>(
180        &self, key: &T::Key, version: ObjectVersion,
181    ) -> Result<Option<crate::ReadBatch<ObjectRevision<T>>>>
182    where
183        C: Contains<T>,
184    {
185        crate::ReadSession::standalone(self).read_object(key, version).await
186    }
187}
188
189impl<C: ObjectCatalog> crate::ReadSession<C> {
190    #[tracing::instrument(name = "storage.read_session.read_object", level = "debug", skip_all)]
191    pub async fn read_object<T: StoredObject>(
192        &self, key: &T::Key, version: ObjectVersion,
193    ) -> Result<Option<crate::ReadBatch<ObjectRevision<T>>>>
194    where
195        C: Contains<T>,
196    {
197        let schema = object_schema::<T>()?;
198        let budget = self.manager.inner.settings.scan_bytes();
199        let point = ObjectPoint { schema: schema.id.clone(), item: object_key(key, budget)?, version };
200        let saved = self.manager.cursor_saved();
201        if let ObjectVersion::Exact(version) = &point.version {
202            version.history().check(&saved.history)?;
203            if version.revision().get() > saved.sequence {
204                return Ok(None);
205            }
206        }
207
208        // Charge The Bounded Raw Read, Then Its Decoded Value. Growth Refused As
209        // A Certain Cycle Releases This Attempt Before Retrying Its Combined Claim.
210        // Other Session Dependencies Remain Held, Including Queued Snapshot Release.
211        let raw = budget.get().saturating_add(ENTRY_OVERHEAD);
212        let mut credit = raw;
213        let held = &self.held;
214        let (object, charge) = loop {
215            let charge =
216                Reservation::wait(&self.manager.inner, Pool::Read, held, credit, None).await.map_err(Refusal::into_error)?.retain();
217            let snapshot = self.manager.snapshot(&saved, held).await?;
218            let Some(object) = snapshot.read_object(&point, charge.clone()).await? else {
219                return Ok(None);
220            };
221            if matches!(&point.version, ObjectVersion::Exact(version) if version != &object.version) {
222                return Err(invalid("backend returned a different object revision"));
223            }
224            let decoded = T::decoding_memory(&object.value.payload)
225                .map_err(|error| Error::backend("preflight object", crate::BackendFailure::Corrupt, error))?;
226            let total = raw.saturating_add(decoded);
227            match charge.grow_to(total).await {
228                Ok(()) => break (object, charge),
229                Err(Refusal::Cycle) => credit = total,
230                Err(refusal) => return Err(refusal.into_error()),
231            }
232        };
233        let value = decode_admitted::<T>(&object.value, &schema)?;
234        let revision = ObjectRevision { value, position: object.position, version: object.version };
235        let bytes = revision.memory_size().saturating_add(ENTRY_OVERHEAD);
236        charge.grow_to(bytes).await.map_err(Refusal::into_error)?;
237        charge.shrink_to(bytes)?;
238        Ok(Some(crate::ReadBatch::retained(revision, charge)))
239    }
240}
241
242impl<C> StorageManager<C> {
243    /// Bounds the owned receipt Vec returned for an object group.
244    ///
245    /// Uses the producer's declared history, including both cloned histories in
246    /// each receipt. Preclaim this plus caller-owned pending/response scaffolding
247    /// before writing. Move the resulting cursor and claim into pending ownership
248    /// before the next await; retries reuse that receipt instead of reissuing an
249    /// ordinary group write. This method neither allocates nor admits memory.
250    #[tracing::instrument(name = "storage.objects.object_receipts_memory", level = "trace", skip_all, fields(count = count.get(), outcome = "cancelled"))]
251    pub fn object_receipts_memory(&self, count: NonZeroUsize) -> Result<usize> {
252        let result: Result<usize> = (|| {
253            let history = self.inner.primary.history();
254            let exhausted = || Error::ResourceExhausted { resource: "object receipt memory" };
255            let history = history.store.as_str().len().checked_add(history.epoch.as_str().len()).ok_or_else(exhausted)?;
256            let receipt =
257                history.checked_mul(2).and_then(|bytes| bytes.checked_add(std::mem::size_of::<ObjectReceipt>())).ok_or_else(exhausted)?;
258            count
259                .get()
260                .checked_mul(receipt)
261                .and_then(|bytes| bytes.checked_add(std::mem::size_of::<Vec<ObjectReceipt>>()))
262                .ok_or_else(exhausted)
263        })();
264        tracing::Span::current().record("outcome", if result.is_ok() { "completed" } else { "failed" });
265        result
266    }
267
268    pub(super) fn object_admission_bytes(&self) -> NonZeroUsize {
269        NonZeroUsize::new(
270            self.inner.settings.max_batch_bytes().get().saturating_mul(4).min(self.inner.settings.request.max_pending_write_bytes.get()),
271        )
272        .unwrap()
273    }
274
275    #[tracing::instrument(name = "storage.objects.admit_object_group", level = "debug", skip_all)]
276    pub(super) async fn admit_object_group(
277        &self, max_bytes: NonZeroUsize, count: NonZeroUsize, mutation: Option<super::admissions::Mutation<'_>>,
278        build: impl FnOnce(&mut ObjectGroup<'_, C>) -> Result<()> + Send,
279    ) -> Result<Vec<ObjectReceipt>> {
280        let _writer = tracing::Instrument::instrument(self.inner.object_writes.lock(), tracing::trace_span!("storage.lock.wait")).await;
281        self.admit_object_group_locked(max_bytes, count, mutation, build).await
282    }
283
284    #[tracing::instrument(name = "storage.objects.admit_object_group_locked", level = "debug", skip_all)]
285    pub(super) async fn admit_object_group_locked(
286        &self, max_bytes: NonZeroUsize, count: NonZeroUsize, mutation: Option<super::admissions::Mutation<'_>>,
287        build: impl FnOnce(&mut ObjectGroup<'_, C>) -> Result<()> + Send,
288    ) -> Result<Vec<ObjectReceipt>> {
289        let positions = count.get().checked_mul(2).ok_or_else(|| invalid("object group count overflow"))?;
290        let mut receipts = None;
291        let last = self
292            .admit_mutation(Domain::Data, max_bytes.get(), positions, mutation, false, |first| {
293                let slots = count
294                    .get()
295                    .checked_mul(
296                        std::mem::size_of::<StorageSchema>()
297                            + std::mem::size_of::<ObjectEntry>()
298                            + std::mem::size_of::<ObjectReceipt>()
299                            + 2 * std::mem::size_of::<super::StorageBatch<StorageSchema>>()
300                            + 2 * ENTRY_OVERHEAD,
301                    )
302                    .ok_or_else(|| invalid("object group size overflow"))?;
303                if slots > max_bytes.get() {
304                    return Err(invalid("object group metadata exceeds allowance"));
305                }
306                let mut group = ObjectGroup {
307                    manager: self,
308                    first,
309                    count: count.get(),
310                    allowance: max_bytes.get(),
311                    used: slots,
312                    schemas: Vec::with_capacity(count.get()),
313                    objects: Vec::with_capacity(count.get()),
314                    receipts: Vec::with_capacity(count.get()),
315                };
316                build(&mut group)?;
317                if group.objects.len() != count.get() {
318                    return Err(invalid("object group count differs from admission"));
319                }
320                receipts = Some(group.receipts);
321                Ok(Operation::ObjectGroup {
322                    schemas: super::StorageBatch::new(group.schemas),
323                    objects: super::StorageBatch::new(group.objects),
324                    bytes: group.used,
325                })
326            })
327            .await?;
328        if let Some(receipts) = receipts {
329            return Ok(receipts);
330        }
331        // A Matching Retry Reconstructs The Same Revision Tokens From Its Fixed
332        // Original Group Frontier. No Payload Is Revalidated Or Readmitted.
333        let first = last.checked_sub(positions as u64 - 2).ok_or_else(|| invalid("invalid object group receipt"))?;
334        let mut receipts = Vec::with_capacity(count.get());
335        for index in 0..count.get() {
336            let position = self.position(first + (index * 2) as u64);
337            receipts.push(ObjectReceipt { version: ObjectVersionToken::new(position.clone()), position });
338        }
339        Ok(receipts)
340    }
341}
342
343/* === Public Functions === */
344
345/* === Private Functions === */
346
347pub(super) fn invalid(reason: &'static str) -> crate::Error {
348    harmos_domain::model::storage::invalid(reason).into()
349}
350
351/* === Tests === */
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356    use crate::test_backend::{Data, Recovery, storage_settings};
357    use std::sync::Arc;
358
359    #[test]
360    fn receipt_bound_history() {
361        let mut store = String::with_capacity(4096);
362        store.push_str("producer-store");
363        let mut epoch = String::with_capacity(4096);
364        epoch.push_str("producer-epoch");
365        let history = crate::StoreHistory { store: crate::Identifier::new(store).unwrap(), epoch: crate::Identifier::new(epoch).unwrap() };
366        let recovery = Recovery::new(history.clone());
367        let expected = history.clone();
368        let storage = StorageManager::<()>::new(
369            crate::StorageBackends { primary: Arc::new(Data::<()>::new(history)), state: Arc::new(recovery) },
370            storage_settings(),
371        )
372        .unwrap();
373        storage.inner.state.lock().unwrap().status.saved.history.store = crate::Identifier::new("different-cursor").unwrap();
374        for count in [1, 2, 17] {
375            let bound = storage.object_receipts_memory(NonZeroUsize::new(count).unwrap()).unwrap();
376            let mut receipts = Vec::with_capacity(count);
377            for index in 0..count {
378                let position = storage.position(index as u64 + 1);
379                receipts.push(ObjectReceipt { version: ObjectVersionToken::new(position.clone()), position });
380            }
381            assert_eq!(receipts.capacity(), count);
382            assert_eq!(receipts.memory_size(), bound);
383            assert_eq!(receipts[0].position.history, expected);
384        }
385        assert!(matches!(
386            storage.object_receipts_memory(NonZeroUsize::new(usize::MAX).unwrap()),
387            Err(Error::ResourceExhausted { resource: "object receipt memory" })
388        ));
389    }
390}