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::{Held, 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.len()))
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 value = value.clone();
126        let bytes = metadata + value.item.memory_size();
127        self.append(value, graph, bytes)
128    }
129
130    fn schema_settings(&self) -> Result<harmos_domain::config::SchemaSettings> {
131        let mut settings = harmos_domain::config::SchemaSettings::default();
132        settings.preparation_bytes = NonZeroUsize::new(settings.preparation_bytes.get().min(self.allowance - self.used))
133            .ok_or_else(|| invalid("object schema preparation exceeds allowance"))?;
134        Ok(settings)
135    }
136
137    fn metadata(&self, schema: &super::SchemaKey, graph: &crate::schema::graph::Schema) -> Result<usize> {
138        let descriptor = StorageSchema::resident_bytes(schema, graph, self.manager.inner.primary.history())?;
139        let clone = graph.clone_memory_bytes().map_err(|_| invalid("object schema size overflow"))?;
140        let retained = graph.memory_size().checked_sub(clone).ok_or_else(|| invalid("object schema clone exceeds retained allocation"))?;
141        let identity = self.manager.charge(&(schema, self.manager.inner.primary.history()), 4)?;
142        descriptor.checked_add(retained).and_then(|bytes| bytes.checked_add(identity)).ok_or_else(|| invalid("object size overflow"))
143    }
144
145    fn append(&mut self, value: ObjectValue, graph: crate::schema::graph::Schema, metadata: usize) -> Result<()> {
146        let bytes = metadata.checked_add(value.payload.capacity()).ok_or_else(|| invalid("object size overflow"))?;
147        if bytes > self.allowance - self.used {
148            return Err(invalid("object payload exceeds allowance"));
149        }
150        let sequence = self.first + (self.objects.len() * 2) as u64;
151        let descriptor = StorageSchema::new(self.manager.position(sequence), value.schema.clone(), graph);
152        let position = self.manager.position(sequence + 1);
153        let version = ObjectVersionToken::new(position.clone());
154        self.used += bytes;
155        self.schemas.push(descriptor);
156        self.objects.push(ObjectEntry { position: position.clone(), version: version.clone(), value });
157        self.receipts.push(ObjectReceipt { position, version });
158        Ok(())
159    }
160}
161
162impl<C: ObjectCatalog> StorageManager<C> {
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    pub async fn write_object_group(
172        &self, max_bytes: NonZeroUsize, count: NonZeroUsize, build: impl FnOnce(&mut ObjectGroup<'_, C>) -> Result<()> + Send,
173    ) -> Result<Vec<ObjectReceipt>> {
174        self.admit_object_group(max_bytes, count, None, build).await
175    }
176
177    pub async fn read_object<T: StoredObject>(&self, key: &T::Key, version: ObjectVersion) -> Result<Option<ObjectRevision<T>>>
178    where
179        C: Contains<T>,
180    {
181        let schema = object_schema::<T>()?;
182        let budget = self.inner.settings.scan_bytes();
183        let point = ObjectPoint { schema: schema.id.clone(), item: object_key(key, budget)?, version };
184        let saved = self.cursor_saved();
185        if let ObjectVersion::Exact(version) = &point.version {
186            version.history().check(&saved.history)?;
187            if version.revision().get() > saved.sequence {
188                return Ok(None);
189            }
190        }
191
192        // Charge The Bounded Raw Read, Then Its Decoded Value. Growth Refused As
193        // A Certain Cycle Releases The Read And Waits For Both With Nothing Held.
194        let raw = budget.get().saturating_add(ENTRY_OVERHEAD);
195        let mut credit = raw;
196        let (object, _charge) = loop {
197            let held = Held::default();
198            let mut charge = Reservation::wait(&self.inner, Pool::Read, &held, credit, None).await.map_err(Refusal::into_error)?;
199            let snapshot = self.snapshot(&saved).await?;
200            let Some(object) = snapshot.read_object(&point).await? else {
201                return Ok(None);
202            };
203            if matches!(&point.version, ObjectVersion::Exact(version) if version != &object.version) {
204                return Err(invalid("backend returned a different object revision"));
205            }
206            let decoded = T::decoding_memory(&object.value.payload)
207                .map_err(|error| Error::backend("preflight object", crate::BackendFailure::Corrupt, error))?;
208            let total = raw.saturating_add(decoded);
209            match charge.grow(total.saturating_sub(charge.bytes()), None).await {
210                Ok(()) => break (object, charge),
211                Err(Refusal::Cycle) => credit = total,
212                Err(refusal) => return Err(refusal.into_error()),
213            }
214        };
215        let value = decode_admitted::<T>(&object.value, &schema)?;
216        Ok(Some(ObjectRevision { value, position: object.position, version: object.version }))
217    }
218}
219
220impl<C> StorageManager<C> {
221    pub(super) fn object_admission_bytes(&self) -> NonZeroUsize {
222        NonZeroUsize::new(
223            self.inner.settings.max_batch_bytes().get().saturating_mul(4).min(self.inner.settings.request.max_pending_write_bytes.get()),
224        )
225        .unwrap()
226    }
227
228    pub(super) async fn admit_object_group(
229        &self, max_bytes: NonZeroUsize, count: NonZeroUsize, mutation: Option<super::admissions::Mutation<'_>>,
230        build: impl FnOnce(&mut ObjectGroup<'_, C>) -> Result<()> + Send,
231    ) -> Result<Vec<ObjectReceipt>> {
232        let _writer = self.inner.object_writes.lock().await;
233        self.admit_object_group_locked(max_bytes, count, mutation, build).await
234    }
235
236    pub(super) async fn admit_object_group_locked(
237        &self, max_bytes: NonZeroUsize, count: NonZeroUsize, mutation: Option<super::admissions::Mutation<'_>>,
238        build: impl FnOnce(&mut ObjectGroup<'_, C>) -> Result<()> + Send,
239    ) -> Result<Vec<ObjectReceipt>> {
240        let positions = count.get().checked_mul(2).ok_or_else(|| invalid("object group count overflow"))?;
241        let mut receipts = None;
242        let last = self
243            .admit_mutation(Domain::Data, max_bytes.get(), positions, mutation, false, |first| {
244                let slots = count
245                    .get()
246                    .checked_mul(
247                        std::mem::size_of::<StorageSchema>()
248                            + std::mem::size_of::<ObjectEntry>()
249                            + std::mem::size_of::<ObjectReceipt>()
250                            + 2 * std::mem::size_of::<super::StoragePart<'_>>()
251                            + 2 * ENTRY_OVERHEAD,
252                    )
253                    .ok_or_else(|| invalid("object group size overflow"))?;
254                if slots > max_bytes.get() {
255                    return Err(invalid("object group metadata exceeds allowance"));
256                }
257                let mut group = ObjectGroup {
258                    manager: self,
259                    first,
260                    count: count.get(),
261                    allowance: max_bytes.get(),
262                    used: slots,
263                    schemas: Vec::with_capacity(count.get()),
264                    objects: Vec::with_capacity(count.get()),
265                    receipts: Vec::with_capacity(count.get()),
266                };
267                build(&mut group)?;
268                if group.objects.len() != count.get() {
269                    return Err(invalid("object group count differs from admission"));
270                }
271                receipts = Some(group.receipts);
272                Ok(Operation::ObjectGroup { schemas: group.schemas, objects: group.objects, bytes: group.used })
273            })
274            .await?;
275        if let Some(receipts) = receipts {
276            return Ok(receipts);
277        }
278        // A Matching Retry Reconstructs The Same Revision Tokens From Its Fixed
279        // Original Group Frontier. No Payload Is Revalidated Or Readmitted.
280        let first = last.checked_sub(positions as u64 - 2).ok_or_else(|| invalid("invalid object group receipt"))?;
281        Ok((0..count.get())
282            .map(|index| {
283                let position = self.position(first + (index * 2) as u64);
284                ObjectReceipt { version: ObjectVersionToken::new(position.clone()), position }
285            })
286            .collect())
287    }
288}
289
290/* === Public Functions === */
291
292/* === Private Functions === */
293
294pub(super) fn invalid(reason: &'static str) -> crate::Error {
295    harmos_domain::model::storage::invalid(reason).into()
296}
297
298/* === Tests === */