Skip to main content

harmos_storage/state/
admission.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
16use crate::storage::manager::RecordWriteBytes;
17// Standard Library Imports
18use std::sync::Arc;
19// Third Party Imports
20
21// Local Imports
22use super::{RecoveryEntry, RecoverySnapshot};
23use crate::storage::manager::{
24    ENTRY_OVERHEAD, Inner, Operation, Pending, Target, encoded_size, recovery_preparation_bytes, recovery_resident_bytes,
25};
26use crate::storage::{ReadLimits, RecordInput, StorageCursor, StorageManager, StoredRecord};
27use crate::{Error, Result};
28use harmos_domain::state::Position;
29
30/* === Constants === */
31
32/* === Macros === */
33
34/* === Type Aliases === */
35
36/* === Structs === */
37
38pub struct Permit {
39    context: crate::execution::Context,
40    inner: Arc<Inner>,
41    bytes: usize,
42    target: Target,
43    first_record: StorageCursor,
44    records: Vec<StoredRecord>,
45    record_bytes: usize,
46    capture_bytes: usize,
47    payload: Vec<u8>,
48    active: bool,
49    snapshot: bool,
50}
51
52/* === Enums === */
53
54/* === Traits === */
55
56/* === Implementations === */
57
58impl<C> StorageManager<C> {
59    #[tracing::instrument(name = "storage.state.admission.reserve_transition", level = "debug", skip_all)]
60    pub async fn reserve_transition(&self, payload_bytes: usize, records: Option<ReadLimits>) -> Result<Permit> {
61        self.reserve_recovery(payload_bytes, records, false).await
62    }
63
64    #[tracing::instrument(name = "storage.state.admission.reserve_snapshot", level = "debug", skip_all)]
65    pub async fn reserve_snapshot(&self, payload_bytes: usize) -> Result<Permit> {
66        self.reserve_recovery(payload_bytes, None, true).await
67    }
68
69    #[tracing::instrument(name = "storage.state.admission.reserve_recovery", level = "debug", skip_all)]
70    async fn reserve_recovery(&self, payload_bytes: usize, records: Option<ReadLimits>, snapshot: bool) -> Result<Permit> {
71        let recovery_bytes = recovery_preparation_bytes(payload_bytes, self.inner.recovery.history(), self.inner.primary.history())?;
72        let bytes = recovery_bytes;
73        let batches = 1 + usize::from(records.is_some());
74        if batches > self.inner.settings.request.max_pending_write_batches.get() {
75            return Err(Error::InvalidInput { field: "managed capture", reason: "requires two queue slots" });
76        }
77        if bytes > self.inner.settings.request.max_pending_write_bytes.get() {
78            return Err(Error::InvalidInput { field: "managed recovery", reason: "group exceeds memory budget" });
79        }
80        let mut changed = self.inner.changed.subscribe();
81        loop {
82            {
83                let mut state = self.inner.state.lock().unwrap();
84                if !snapshot && recovery_bytes > state.journal_limit {
85                    return Err(Error::InvalidInput { field: "managed recovery", reason: "group exceeds recovery read bound" });
86                }
87                if state.status.closing {
88                    return Err(Error::Closing { domain: "storage" });
89                }
90                if !state.ready || !state.status.running {
91                    return Err(Error::NotReady { domain: "storage" });
92                }
93                if let Some(source) = &state.status.failure {
94                    return Err(Error::Persistence { source: source.clone() });
95                }
96                state.collect();
97                if state.reserved == 0
98                    && state.finishing.is_none()
99                    && state.status.pending_bytes <= self.inner.settings.request.max_pending_write_bytes.get() - bytes
100                    && state.status.pending_batches <= self.inner.settings.request.max_pending_write_batches.get() - batches
101                {
102                    state
103                        .next
104                        .checked_add(batches as u64)
105                        .ok_or(Error::InvalidInput { field: "managed capture", reason: "queue order overflow" })?;
106                    let mut first_record = state.status.accepted.clone();
107                    if let Some(limits) = records {
108                        first_record
109                            .sequence
110                            .checked_add(limits.rows.get() as u64)
111                            .ok_or(Error::InvalidInput { field: "managed capture", reason: "position overflow" })?;
112                        first_record.sequence += 1;
113                    }
114                    let target = Target {
115                        index: state
116                            .next
117                            .checked_add(1)
118                            .ok_or(Error::InvalidInput { field: "managed recovery", reason: "queue order overflow" })?,
119                        sequence: state
120                            .status
121                            .recovery_accepted
122                            .sequence
123                            .checked_add(u64::from(!snapshot))
124                            .ok_or(Error::InvalidInput { field: "managed recovery", reason: "position overflow" })?,
125                    };
126                    state.reserved = bytes;
127                    state.reserved_batches = batches;
128                    state.collect();
129                    return Ok(Permit {
130                        context: crate::execution::Context::current(),
131                        inner: self.inner.clone(),
132                        bytes: recovery_bytes,
133                        target,
134                        first_record,
135                        records: Vec::new(),
136                        record_bytes: 0,
137                        capture_bytes: 0,
138                        payload: Vec::new(),
139                        active: true,
140                        snapshot,
141                    });
142                }
143                self.inner.relieve_pressure(&mut state, bytes, batches);
144            }
145            changed.changed().await.map_err(|_| Error::NotReady { domain: "storage" })?;
146        }
147    }
148
149    #[tracing::instrument(name = "storage.state.admission.settle_transition", level = "debug", skip_all)]
150    pub async fn settle_transition(&self) {
151        let mut changed = self.inner.changed.subscribe();
152        loop {
153            if self.inner.state.lock().unwrap().reserved == 0 {
154                return;
155            }
156            if changed.changed().await.is_err() {
157                return;
158            }
159        }
160    }
161}
162
163impl Permit {
164    pub fn snapshot_payload(&mut self, payload: Vec<u8>) -> Result<()> {
165        let context = self.context.clone();
166        context.sync(|| {
167            let _stage = tracing::debug_span!("storage.state.snapshot_payload").entered();
168            debug_assert!(self.snapshot);
169            let bytes = recovery_resident_bytes(payload.capacity(), self.inner.recovery.history(), self.inner.primary.history())?;
170            if bytes > self.bytes {
171                return Err(Error::InvalidInput { field: "state snapshot", reason: "payload exceeds reservation" });
172            }
173            self.payload = payload;
174            self.bytes = bytes;
175            let mut state = self.inner.state.lock().unwrap();
176            state.reserved = bytes;
177            state.collect();
178            self.inner.notify();
179            Ok(())
180        })
181    }
182
183    pub fn projected(&self) -> &[StoredRecord] {
184        &self.records
185    }
186
187    #[tracing::instrument(name = "storage.state.admission.push", level = "debug", skip_all)]
188    pub async fn push(&mut self, record: RecordInput, limits: ReadLimits) -> Result<()> {
189        let context = self.context.clone();
190        let span = context.sync(|| tracing::debug_span!("storage.state.project"));
191        let future = async {
192            let invalid = || Error::InvalidInput { field: "record projection", reason: "capture limit exceeded" };
193            if self.records.len() >= limits.rows.get() {
194                return Err(invalid());
195            }
196            let capture = encoded_size(&record.borrowed())?
197                .checked_add(encoded_size(&self.first_record.history)?)
198                .and_then(|bytes| bytes.checked_add(ENTRY_OVERHEAD))
199                .ok_or_else(invalid)?;
200            let capture_total = self.capture_bytes.checked_add(capture).ok_or_else(invalid)?;
201            // An incremental Vec may retain spare slots or briefly own both old and
202            // replacement allocations. Four inline slots per live record cover
203            // initial minimum capacity and geometric growth before copying.
204            let bytes = record
205                .resident_bytes(&self.first_record.history)?
206                .checked_add(std::mem::size_of::<StoredRecord>() * 3)
207                .ok_or_else(invalid)?;
208            let total = self.record_bytes.checked_add(bytes).ok_or_else(invalid)?;
209            // Refuse a group that cannot fit even after all accepted work drains.
210            // Waiting with a partial permit in that case would deadlock itself.
211            if capture_total > limits.bytes.get()
212                || self.bytes.checked_add(total).is_none_or(|total| total > self.inner.settings.request.max_pending_write_bytes.get())
213            {
214                return Err(invalid());
215            }
216            let mut position = self.first_record.clone();
217            position.sequence = position.sequence.checked_add(self.records.len() as u64).ok_or_else(invalid)?;
218            let capacity = self.inner.settings.request.max_pending_write_bytes.get();
219            self.inner
220                .admit_when(|state| {
221                    super::super::capacity::admitting(state, super::super::capacity::Pool::Write)?;
222                    state.collect();
223                    if state.status.pending_bytes <= capacity - bytes {
224                        state.reserved += bytes;
225                        state.collect();
226                        return Ok(Some(()));
227                    }
228                    // File finalization also needs this exclusive permit to end.
229                    // Only queued writes and saver scratch can drain underneath us;
230                    // never wait on bytes held by our own permit or staged owners.
231                    let held = state.status.pending_bytes.saturating_sub(state.queue.bytes()).saturating_sub(state.scratch_bytes);
232                    if held > capacity - bytes {
233                        return Err(invalid());
234                    }
235                    // Flush accepted work only; this permit owns the uncommitted
236                    // position and partial projection until the whole group settles.
237                    self.inner.relieve_pressure(state, bytes, 0);
238                    Ok(None)
239                })
240                .await?;
241            self.records.push(record.stored(position));
242            self.record_bytes = total;
243            self.capture_bytes = capture_total;
244            Ok(())
245        };
246        context.scope(tracing::Instrument::instrument(future, span)).await
247    }
248
249    pub fn encode(&mut self, value: &impl serde::Serialize) -> Result<()> {
250        let context = self.context.clone();
251        context.sync(|| {
252            let _stage = tracing::debug_span!("storage.state.encode").entered();
253            self.payload =
254                serde_json::to_vec(value).map_err(|_| Error::InvalidInput { field: "managed recovery", reason: "cannot encode group" })?;
255            // Encoding runs under the original conservative scratch reservation.
256            // Only the encoded buffer survives commit; release unused framing and
257            // growth allowance before the group becomes queued work.
258            let bytes = recovery_resident_bytes(self.payload.capacity(), self.inner.recovery.history(), self.inner.primary.history())?;
259            if bytes > self.bytes {
260                return Err(Error::InvalidInput { field: "managed recovery", reason: "encoded allocation exceeds reservation" });
261            }
262            self.bytes = bytes;
263            let mut state = self.inner.state.lock().unwrap();
264            state.reserved = self
265                .bytes
266                .checked_add(self.record_bytes)
267                .ok_or(Error::InvalidInput { field: "managed recovery", reason: "size overflow" })?;
268            state.reserved_batches = 1 + usize::from(!self.records.is_empty());
269            state.collect();
270            self.inner.notify();
271            Ok(())
272        })
273    }
274
275    pub fn commit(mut self, frontier: Position) {
276        let context = self.context.clone();
277        context.sync(|| {
278            let _stage = tracing::debug_span!("storage.state.commit").entered();
279            let mut state = self.inner.state.lock().unwrap();
280            let required_data = if self.snapshot {
281                state.journal_required_data.clone()
282            } else {
283                self.records.last().map_or_else(|| state.status.saved.clone(), |record| record.position.clone())
284            };
285            if required_data.sequence > state.journal_required_data.sequence {
286                state.journal_required_data = required_data.clone();
287            }
288            if let Some(last) = self.records.last() {
289                state.status.accepted = last.position.clone();
290                state.data_accepted = Target { index: self.target.index, sequence: last.position.sequence };
291                state.queue.push_back(Arc::new(Pending::new(
292                    &self.inner,
293                    None,
294                    self.target.index,
295                    self.record_bytes,
296                    Operation::Records(crate::StorageBatch::new(std::mem::take(&mut self.records))),
297                )));
298                self.target.index += 1; // Both slots were checked before mutation.
299            }
300            let position = crate::storage::RecoveryPosition {
301                history: state.status.recovery_accepted.history.clone(),
302                sequence: self.target.sequence,
303            };
304            state.status.recovery_accepted = position.clone();
305            state.recovery_accepted = self.target;
306            state.next = self.target.index;
307            state.queue.push_back(Arc::new(Pending::new(
308                &self.inner,
309                (!self.snapshot).then_some(frontier),
310                self.target.index,
311                self.bytes,
312                if self.snapshot {
313                    Operation::Snapshot(crate::StorageBatch::new(vec![RecoverySnapshot {
314                        required_data: required_data.clone(),
315                        through: position,
316                        payload: std::mem::take(&mut self.payload),
317                    }]))
318                } else {
319                    Operation::Recovery(crate::StorageBatch::new(vec![RecoveryEntry {
320                        required_data,
321                        position,
322                        payload: std::mem::take(&mut self.payload),
323                    }]))
324                },
325            )));
326            state.reserved = 0;
327            state.reserved_batches = 0;
328            self.active = false;
329            state.collect();
330            self.inner.notify();
331        })
332    }
333}
334
335impl Drop for Permit {
336    fn drop(&mut self) {
337        if self.active {
338            let mut state = self.inner.state.lock().unwrap();
339            state.reserved = 0;
340            state.reserved_batches = 0;
341            state.collect();
342            self.inner.notify();
343        }
344    }
345}
346
347/* === Public Functions === */
348
349/* === Private Functions === */
350
351/* === Tests === */