Skip to main content

harmos_storage/
worker.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::future::Future;
18use std::sync::Arc;
19use std::task::Poll;
20
21// Third Party Imports
22
23// Local Imports
24use super::batching::{Batch, Progress, write_data};
25use super::manager::{Domain, Inner, Operation, Pending, Target};
26use super::snapshot::SnapshotAction;
27use super::staging::{FileAction, FileCompleted};
28use super::{RecoveryPosition, StorageManager, StoragePart};
29use crate::{BackendFailure, Error, Result};
30
31/* === Constants === */
32
33/* === Macros === */
34
35/* === Type Aliases === */
36
37/* === Structs === */
38
39pub struct Worker(pub(crate) StorageManager<crate::storage::ErasedCatalog>);
40
41struct Running(Arc<Inner>);
42
43/* === Enums === */
44
45enum Action {
46    Write(Arc<Pending>),
47    Snapshot(SnapshotAction),
48    File(FileAction),
49    Delete(Arc<super::deletion::DeleteWork>),
50    Recovery(Target),
51    Prune(super::StorageCursor),
52}
53
54enum Completed {
55    Write(Progress),
56    Opened(u64, Result<super::BackendSnapshot>),
57    Released(u64),
58    File(FileCompleted),
59    Deleted,
60    Recovery(RecoveryPosition),
61    Pruned(bool),
62}
63
64/* === Traits === */
65
66/* === Implementations === */
67
68impl Worker {
69    pub async fn cleanup(&mut self) -> Result<()> {
70        // StorageManager owns physical backend teardown before this worker's
71        // lease retires. An early stop cannot claim that teardown completed.
72        if !self.0.inner.state.lock().unwrap().status.closed {
73            return Err(Error::NotReady { domain: "storage cleanup" });
74        }
75        Ok(())
76    }
77
78    pub async fn run(&mut self) -> Result<()> {
79        let inner = &self.0.inner;
80        let _running = Running(inner.clone());
81        let mut changed = inner.changed.subscribe();
82        let mut next_flush = inner.execution.now() + super::config::FLUSH_INTERVAL;
83        let mut retry_at = None;
84        let mut failures = 0usize;
85        let mut retry_generation = 0;
86        let mut prefer_control = true;
87        let mut prefer_files = true;
88        let mut prefer_pruning = true;
89        let mut prefer_state = true;
90        loop {
91            changed.borrow_and_update();
92            let now = inner.execution.now();
93            let action = {
94                let mut state = inner.state.lock().unwrap();
95                if state.status.closed {
96                    return Ok(());
97                }
98                if state.retry != retry_generation {
99                    retry_generation = state.retry;
100                    failures = 0;
101                    retry_at = None;
102                    state.status.failure = None;
103                }
104                if retry_at.is_some_and(|deadline| now >= deadline) {
105                    retry_at = None;
106                    state.status.failure = None;
107                }
108                if now >= next_flush {
109                    state.request_flush();
110                    next_flush = now + super::config::FLUSH_INTERVAL;
111                }
112                state.expire_files();
113                state.request_deletions();
114                state.collect();
115                if state.queue.bytes() >= inner.settings.target_batch_bytes() {
116                    state.request_flush();
117                }
118                // Already registered snapshots acquire their pins before maintenance.
119                let prune_through = if state.snapshot_opening() { 0 } else { state.status.saved.sequence };
120                let mut prune = (inner.has_objects
121                    && state.ready
122                    && !state.status.closing
123                    && state.status.failure.is_none()
124                    && prune_through > state.objects_pruned)
125                    .then(|| Action::Prune(self.0.position(prune_through)));
126                let data = if prefer_pruning && prune.is_some() {
127                    prune.take()
128                } else if !state.ready || state.status.failure.is_some() {
129                    None
130                } else if let Some(deletion) = state.deletion_action() {
131                    Some(Action::Delete(deletion))
132                } else if let Some(target) = state.recovery_request.filter(|target| target.index <= state.recovery_written.index) {
133                    Some(Action::Recovery(target))
134                } else if let Some(boundary) = state.write_boundary() {
135                    // A primary barrier does not require recovery I/O. Keep
136                    // both domains in the same queue, but prefer its data while
137                    // preserving the earliest explicitly requested boundary.
138                    let mut pending = state.queue.iter().filter(|entry| entry.index <= boundary && state.needs_write(entry));
139                    let first = pending.next();
140                    let selected =
141                        if state.data_request.is_some() && first.is_some_and(|entry| entry.operation.domain() == Domain::Recovery) {
142                            pending.find(|entry| entry.operation.domain() == Domain::Data).or(first)
143                        } else {
144                            first
145                        };
146                    // Journal Order Is Independent Of Unrelated Measurements.
147                    // Only The Earliest Unwritten State Entry May Bypass Data,
148                    // And Its Declared Evidence Dependency Must Already Be Durable.
149                    let ready_state = state
150                        .recovery_request
151                        .and_then(|target| {
152                            state.queue.iter().find(|entry| {
153                                entry.index <= target.index && entry.operation.domain() == Domain::Recovery && state.needs_write(entry)
154                            })
155                        })
156                        .filter(|entry| {
157                            entry.operation.required_data().is_some_and(|required| state.status.saved.covers(required).unwrap_or(false))
158                        });
159                    let selected = if prefer_state { ready_state.or(selected) } else { selected };
160                    selected.cloned().map(Action::Write)
161                } else {
162                    None
163                };
164                let data = data.or(prune);
165                // Alternate persistence with bounded control work; rotate files
166                // and views so neither producer churn nor reads can starve a peer.
167                let selected = if prefer_control || data.is_none() {
168                    let healthy = state.status.failure.is_none();
169                    let file = if prefer_files && healthy { state.file_action().map(Action::File) } else { None };
170                    let control = file
171                        .or_else(|| state.snapshot_action(healthy).map(Action::Snapshot))
172                        .or_else(|| if healthy { state.file_action().map(Action::File) } else { None });
173                    if let Some(action) = &control {
174                        prefer_files = !matches!(action, Action::File(_));
175                    }
176                    control.or(data)
177                } else {
178                    data
179                };
180                state.pruning_objects = matches!(selected, Some(Action::Prune(_)));
181                if state.pruning_objects {
182                    state.objects_prune_owed = false;
183                }
184                selected
185            };
186            if let Some(action) = action {
187                if let Action::Write(entry) = &action {
188                    prefer_state = entry.operation.domain() != Domain::Recovery;
189                }
190                prefer_pruning = !matches!(action, Action::Prune(_));
191                prefer_control = !matches!(action, Action::Snapshot(_) | Action::File(_));
192                let result = perform(inner, &action).await;
193                if matches!(action, Action::Prune(_)) {
194                    inner.state.lock().unwrap().pruning_objects = false;
195                }
196                if let Ok(Completed::Opened(id, snapshot)) = result {
197                    inner.state.lock().unwrap().snapshot_opened(id, snapshot);
198                    inner.notify();
199                    continue;
200                }
201                match result {
202                    Ok(completed) => {
203                        failures = 0;
204                        let mut state = inner.state.lock().unwrap();
205                        state.status.failure = None;
206                        match (action, completed) {
207                            (Action::Delete(work), Completed::Deleted) => {
208                                state.receipts.complete(&work.receipt.mutation, work.receipt.value.clone());
209                                state.deletions.remove(&work.receipt.mutation);
210                            }
211                            (Action::Prune(through), Completed::Pruned(done)) => {
212                                if done && !state.objects_prune_owed {
213                                    state.objects_pruned = through.sequence;
214                                }
215                            }
216                            (Action::File(action), Completed::File(result)) => {
217                                state.file_completed(action, result, super::config::FILE_SESSION_TIMEOUT)
218                            }
219                            (Action::Snapshot(SnapshotAction::Release(..)), Completed::Released(id)) => state.snapshot_removed(id),
220                            (Action::Write(_), Completed::Write(progress)) => {
221                                if let Some(target) = progress.data {
222                                    let saved = progress.saved.expect("data write returns durability");
223                                    let through = if saved.sequence == target.sequence {
224                                        target.index
225                                    } else {
226                                        state
227                                            .queue
228                                            .iter()
229                                            .filter(|entry| {
230                                                entry.operation.domain() == Domain::Data && entry.operation.sequence() <= saved.sequence
231                                            })
232                                            .map(|entry| entry.index)
233                                            .max()
234                                            .unwrap_or(state.data_flushed)
235                                    };
236                                    state.status.saved = saved;
237                                    state.data_flushed = through;
238                                    state.retire_data();
239                                    if state.data_request.is_some_and(|request| request.index <= through) {
240                                        state.data_request = None;
241                                    }
242                                }
243                                if let Some(target) = progress.recovery {
244                                    state.recovery_written.index = target.index;
245                                    state.recovery_written.sequence = state.recovery_written.sequence.max(target.sequence);
246                                }
247                            }
248                            (Action::Recovery(target), Completed::Recovery(saved)) => {
249                                if let Some(progress) = &state.journal_saved
250                                    && let Some(frontier) = state
251                                        .queue
252                                        .iter()
253                                        .filter(|entry| entry.index <= target.index)
254                                        .filter_map(|entry| entry.journal)
255                                        .max()
256                                {
257                                    progress.send_replace(frontier);
258                                }
259                                state.status.recovery_saved = saved;
260                                state.recovery_flushed = target.index;
261                                state.queue.retire(Domain::Recovery, target.index);
262                                state.recovery_request = None;
263                            }
264                            _ => unreachable!("completion matches its storage action"),
265                        }
266                        state.collect();
267                    }
268                    Err(error) => {
269                        failures = failures.saturating_add(1);
270                        // Receipt-Addressed Calls Use The Shared Operation Retry Policy.
271                        // Do Not Multiply Its Three Retries By Another Physical Budget.
272                        let retries = match &action {
273                            Action::Delete(_) => 0,
274                            Action::Write(pending)
275                                if pending.operation.domain() == Domain::Data && {
276                                    let state = inner.state.lock().unwrap();
277                                    state.deleting_data(pending.index)
278                                } =>
279                            {
280                                0
281                            }
282                            Action::File(file) if file.caller_retries(&inner.state.lock().unwrap()) => 0,
283                            _ => super::config::MAX_RETRIES,
284                        };
285                        retry_at = if transient(&error) && failures <= retries {
286                            let multiplier = 1u32.checked_shl((failures - 1).min(31) as u32).unwrap_or(u32::MAX);
287                            Some(inner.execution.now() + super::config::RETRY_BASE.saturating_mul(multiplier).min(super::config::RETRY_CAP))
288                        } else {
289                            None
290                        };
291                        let source = Arc::new(error);
292                        let mut state = inner.state.lock().unwrap();
293                        if let Action::File(action) = &action {
294                            state.file_failed(action, source.clone(), retry_at.is_some());
295                        }
296                        match &action {
297                            Action::Delete(work) => state.deletion_failed(&work.receipt.mutation, source.clone()),
298                            Action::Write(pending) if pending.operation.domain() == Domain::Data => {
299                                state.deletion_data_failed(pending.index, source.clone())
300                            }
301                            _ => {}
302                        }
303                        state.status.failure = Some(source);
304                    }
305                }
306                inner.notify();
307            } else {
308                let deadline = retry_at.map_or(next_flush, |retry| retry.min(next_flush));
309                let deadline = inner.state.lock().unwrap().file_deadline().map_or(deadline, |file| file.min(deadline));
310                let mut sleep = Box::pin(inner.execution.sleep(deadline.saturating_duration_since(inner.execution.now())));
311                let mut wake = Box::pin(changed.changed());
312                let wait = std::future::poll_fn(|cx| {
313                    if wake.as_mut().poll(cx).is_ready() || sleep.as_mut().poll(cx).is_ready() { Poll::Ready(()) } else { Poll::Pending }
314                });
315                wait.await;
316            }
317        }
318    }
319}
320
321impl Drop for Running {
322    fn drop(&mut self) {
323        let mut state = self.0.state.lock().unwrap();
324        state.status.running = false;
325        state.pruning_objects = false;
326        if !state.status.closed {
327            state.status.failure = Some(Arc::new(Error::NotReady { domain: "storage worker" }));
328        }
329        self.0.notify();
330    }
331}
332
333impl Operation {
334    pub(super) fn encoded_batch_bytes(&self) -> Result<usize> {
335        let sum = |parts: &[StoragePart<'_>]| -> Result<usize> {
336            parts.iter().flat_map(|part| part.entries()).try_fold(0usize, |bytes, entry| {
337                bytes
338                    .checked_add(entry.encoded_bytes()?)
339                    .ok_or(Error::InvalidInput { field: "storage admission", reason: "encoded batch size overflow" })
340            })
341        };
342        match self {
343            Self::Schema(value) => sum(&[StoragePart::Schemas(std::slice::from_ref(value))]),
344            Self::ObjectGroup { schemas, objects, .. } => {
345                let mut bytes = 0usize;
346                for (schema, object) in schemas.iter().zip(objects) {
347                    let next =
348                        sum(&[StoragePart::Schemas(std::slice::from_ref(schema)), StoragePart::Objects(std::slice::from_ref(object))])?;
349                    bytes = bytes
350                        .checked_add(next)
351                        .ok_or(Error::InvalidInput { field: "storage admission", reason: "encoded batch size overflow" })?;
352                }
353                Ok(bytes)
354            }
355            Self::Records(values) => sum(&[StoragePart::Records(values)]),
356            Self::Stream(values) => sum(&[StoragePart::Stream(values)]),
357            Self::File(value) => sum(&[StoragePart::Files(std::slice::from_ref(value))]),
358            Self::Recovery(_) | Self::Snapshot(_) => Ok(0),
359        }
360    }
361
362    pub(super) fn check_entry_bytes(&self, maximum: std::num::NonZeroUsize) -> Result<()> {
363        if self.encoded_batch_bytes()? > maximum.get() {
364            return Err(Error::InvalidInput {
365                field: "storage admission",
366                reason: "atomic batch exceeds max_batch_bytes; split before submission",
367            });
368        }
369        Ok(())
370    }
371
372    pub(super) fn required_data(&self) -> Option<&super::StorageCursor> {
373        match self {
374            Self::Recovery(entries) => entries.iter().max_by_key(|entry| entry.required_data.sequence).map(|entry| &entry.required_data),
375            Self::Snapshot(snapshot) => Some(&snapshot.required_data),
376            _ => None,
377        }
378    }
379
380    pub(super) fn domain(&self) -> Domain {
381        match self {
382            Self::Recovery(_) | Self::Snapshot(_) => Domain::Recovery,
383            _ => Domain::Data,
384        }
385    }
386
387    pub(super) fn sequence(&self) -> u64 {
388        match self {
389            Self::Schema(schema) => schema.position.sequence,
390            Self::File(file) => file.position.sequence,
391            Self::ObjectGroup { objects, .. } => objects.last().unwrap().position.sequence,
392            Self::Records(records) => records.last().unwrap().position.sequence,
393            Self::Stream(rows) => rows.last().unwrap().record.position.sequence,
394            Self::Recovery(entries) => entries.last().unwrap().position.sequence,
395            Self::Snapshot(snapshot) => snapshot.through.sequence,
396        }
397    }
398}
399
400/* === Public Functions === */
401
402/* === Private Functions === */
403
404async fn perform(inner: &Arc<Inner>, action: &Action) -> Result<Completed> {
405    match action {
406        Action::Delete(work) => work.perform(inner).await.map(|()| Completed::Deleted),
407        Action::Prune(through) => {
408            inner.bounded(inner.primary.prune_objects(through, super::config::OBJECT_PRUNE_ENTRIES)).await.map(Completed::Pruned)
409        }
410        Action::File(action) => super::staging::perform(inner, action).await.map(Completed::File),
411        Action::Snapshot(SnapshotAction::Open(id, at)) => {
412            let opened = inner.bounded(inner.primary.open_snapshot(at)).await;
413            Ok(Completed::Opened(*id, opened))
414        }
415        Action::Snapshot(SnapshotAction::Release(id, snapshot)) => {
416            inner.bounded(inner.primary.release_snapshot(snapshot)).await?;
417            Ok(Completed::Released(*id))
418        }
419        Action::Write(entry) => {
420            if let Some(batch) = Batch::prepare(inner, entry) {
421                return batch.write(inner).await.map(Completed::Write);
422            }
423            let mut progress = Progress::default();
424            progress.include(entry);
425            let part = match &entry.operation {
426                Operation::File(file) => Some(StoragePart::Files(std::slice::from_ref(file))),
427                Operation::Schema(schema) => Some(StoragePart::Schemas(std::slice::from_ref(schema))),
428                Operation::Records(records) => Some(StoragePart::Records(records)),
429                Operation::ObjectGroup { schemas, objects, .. } => {
430                    let mut parts = Vec::with_capacity(schemas.len() * 2);
431                    for (schema, object) in schemas.iter().zip(objects) {
432                        parts.push(StoragePart::Schemas(std::slice::from_ref(schema)));
433                        parts.push(StoragePart::Objects(std::slice::from_ref(object)));
434                    }
435                    progress.saved = Some(write_data(inner, &parts).await?);
436                    return Ok(Completed::Write(progress));
437                }
438                Operation::Stream(rows) => Some(StoragePart::Stream(rows)),
439                Operation::Recovery(entries) => {
440                    let saved = inner.state.lock().unwrap().status.saved.clone();
441                    for entry in entries {
442                        if !saved.covers(&entry.required_data)? {
443                            return Err(Error::InvalidInput {
444                                field: "state evidence dependency",
445                                reason: "journal cannot precede required primary persistence",
446                            });
447                        }
448                    }
449                    inner.bounded(inner.recovery.write_entries(entries)).await?;
450                    None
451                }
452                Operation::Snapshot(snapshot) => {
453                    if !inner.state.lock().unwrap().status.saved.covers(&snapshot.required_data)? {
454                        return Err(Error::InvalidInput {
455                            field: "state snapshot dependency",
456                            reason: "snapshot cannot precede required primary persistence",
457                        });
458                    }
459                    inner.bounded(inner.recovery.write_snapshot(snapshot)).await?;
460                    None
461                }
462            };
463            if let Some(part) = part {
464                progress.saved = Some(write_data(inner, &[part]).await?);
465            }
466            Ok(Completed::Write(progress))
467        }
468        Action::Recovery(target) => {
469            let through = RecoveryPosition { history: inner.recovery.history().clone(), sequence: target.sequence };
470            let saved = inner.bounded(inner.recovery.flush(&through)).await?;
471            let written = inner.state.lock().unwrap().recovery_written.sequence;
472            if !saved.covers(&through)? || saved.sequence > written {
473                return Err(Error::InvalidInput {
474                    field: "recovery flush receipt",
475                    reason: "receipt does not cover the requested written frontier",
476                });
477            }
478            Ok(Completed::Recovery(saved))
479        }
480    }
481}
482
483fn transient(error: &Error) -> bool {
484    match error {
485        Error::Backend { kind: BackendFailure::Transient, .. } => true,
486        Error::Persistence { source } => transient(source),
487        _ => false,
488    }
489}
490
491/* === Tests === */