Skip to main content

harmos_storage/
opened_capture.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10// Capture An Opened Source Into One Retained Producer.
11
12/* === Modules === */
13
14/* === Re-Exports === */
15
16/* === Imports === */
17
18use crate::{ByteRange, CaptureConsistency, FileChunk, FileReader, FileReference, Result, StorageFile, StorageManager};
19use std::{
20    future::Future,
21    pin::Pin,
22    sync::{
23        Arc, Mutex,
24        atomic::{AtomicU64, Ordering},
25    },
26};
27
28// Standard Library Imports
29
30// Third Party Imports
31
32// Local Imports
33
34/* === Constants === */
35
36/* === Macros === */
37
38/* === Type Aliases === */
39
40type Pending = Pin<Box<dyn Future<Output = (State, Result<Step>)> + Send>>;
41
42/* === Structs === */
43
44/// Retains the original opened source, producer and exact in-flight copy across cancellation.
45pub struct OpenedCapture {
46    state: Option<State>,
47    pending: Mutex<Option<Pending>>,
48    acknowledged: Option<Arc<AtomicU64>>,
49    initial_offset: u64,
50    request: Option<Request>,
51}
52
53struct State {
54    session: crate::StorageManager<crate::ErasedCatalog>,
55    file: std::fs::File,
56    range: ByteRange,
57    reader: Option<FileReader>,
58    target: StorageFile,
59    consistency: CaptureConsistency,
60    chunk: Option<FileChunk>,
61    offset: u64,
62    acknowledged: Option<Arc<AtomicU64>>,
63    prefix: Option<crate::ProductionFollower>,
64    remaining_prefix: u64,
65    matched: usize,
66    conflict: bool,
67    request: Request,
68    action: Action,
69    in_round: bool,
70    observed: Option<(u64, std::time::SystemTime)>,
71    sealed: bool,
72    finished: Option<FileReference>,
73    _frozen: Option<crate::FrozenFile>,
74}
75
76/* === Enums === */
77
78#[derive(Clone, Copy, PartialEq, Eq)]
79enum Request {
80    Finish,
81    Synchronize(u64),
82    FinishAt(u64),
83    Interrupt,
84}
85
86#[derive(Clone, Copy)]
87enum Action {
88    Recover,
89    Open,
90    Prefix,
91    Read,
92    Match,
93    Check,
94    Write,
95    Seal,
96}
97
98enum Step {
99    Continue,
100    Synchronized(bool),
101    Finished,
102}
103
104/* === Traits === */
105
106/* === Implementations === */
107
108impl<C> StorageManager<C> {
109    pub fn capture_frozen_file(&self, source: crate::FrozenFile, target: StorageFile) -> Result<OpenedCapture> {
110        let file = source.opened_file()?;
111        let range = ByteRange { offset: 0, limit: Some(source.length()) };
112        let mut capture = self.capture_opened_file(file, target, range, CaptureConsistency::Stable);
113        let state = capture.state.as_mut().expect("new capture retains source state");
114        if Arc::ptr_eq(&self.inner, &source.session.inner) {
115            state.session = source.session.clone();
116        }
117        state._frozen = Some(source);
118        Ok(capture)
119    }
120
121    pub fn capture_opened_file(
122        &self, file: std::fs::File, target: StorageFile, range: ByteRange, consistency: CaptureConsistency,
123    ) -> OpenedCapture {
124        let offset = target.length();
125        OpenedCapture {
126            state: Some(State {
127                session: self.erased(),
128                file,
129                range,
130                reader: None,
131                target,
132                consistency,
133                chunk: None,
134                offset,
135                acknowledged: None,
136                prefix: None,
137                remaining_prefix: offset,
138                matched: 0,
139                conflict: false,
140                request: Request::Finish,
141                action: Action::Recover,
142                in_round: false,
143                observed: None,
144                sealed: false,
145                finished: None,
146                _frozen: None,
147            }),
148            pending: Mutex::new(None),
149            acknowledged: None,
150            initial_offset: offset,
151            request: None,
152        }
153    }
154}
155
156impl OpenedCapture {
157    pub fn acknowledged(&self) -> u64 {
158        self.acknowledged.as_ref().map_or(self.initial_offset, |value| value.load(Ordering::Acquire))
159    }
160
161    pub async fn finish(&mut self) -> Result<FileReference> {
162        match self.run(Request::Finish).await? {
163            Step::Finished => Ok(self.finished()),
164            _ => unreachable!("terminal capture returns its reference"),
165        }
166    }
167
168    /// Admits the selected visible prefix without sealing the original producer.
169    /// False discards an unstable, unaccepted observation; accepted bytes remain retained.
170    pub async fn synchronize(&mut self, visible_length: u64) -> Result<bool> {
171        match self.run(Request::Synchronize(visible_length)).await? {
172            Step::Synchronized(stable) => Ok(stable),
173            _ => unreachable!("nonterminal capture returns its observation"),
174        }
175    }
176
177    /// Verifies and seals one quiescent, range-relative visible prefix.
178    pub async fn finish_at(&mut self, visible_length: u64) -> Result<FileReference> {
179        match self.run(Request::FinishAt(visible_length)).await? {
180            Step::Finished => Ok(self.finished()),
181            _ => unreachable!("terminal capture returns its reference"),
182        }
183    }
184
185    /// Settles an accepted append and seals that prefix without another source read.
186    pub async fn interrupt(&mut self) -> Result<FileReference> {
187        match self.run(Request::Interrupt).await? {
188            Step::Finished => Ok(self.finished()),
189            _ => unreachable!("interrupted capture returns its reference"),
190        }
191    }
192
193    fn finished(&mut self) -> FileReference {
194        self.state.as_mut().expect("capture state retained").finished.take().expect("finished capture retains its reference")
195    }
196
197    async fn run(&mut self, request: Request) -> Result<Step> {
198        let changed = self.request != Some(request);
199        if changed && self.pending.get_mut().expect("capture pending owner is healthy").is_some() {
200            // Resume Only The Already Accepted Step. Interruption Must Not Start
201            // Another Source Read Or Append From The Cancelled Observation.
202            let settled = self.step().await;
203            if request != Request::Interrupt {
204                settled?;
205            }
206        }
207        if self.state.as_ref().is_some_and(|state| changed || !state.in_round) {
208            self.state.as_mut().expect("capture state retained").reset(request)?;
209            self.request = Some(request);
210        }
211        loop {
212            match self.step().await? {
213                Step::Continue => {}
214                outcome => return Ok(outcome),
215            }
216        }
217    }
218
219    async fn step(&mut self) -> Result<Step> {
220        let pending = self.pending.get_mut().expect("capture pending owner is healthy");
221        if pending.is_none() {
222            if self.acknowledged.is_none() {
223                let acknowledged = Arc::new(AtomicU64::new(self.initial_offset));
224                self.state.as_mut().expect("capture owns its state").acknowledged = Some(acknowledged.clone());
225                self.acknowledged = Some(acknowledged);
226            }
227            let state = self.state.take().expect("capture owns its state");
228            *pending = Some(Box::pin(work(state)));
229        }
230        let (state, result) = pending.as_mut().expect("accepted capture retained").await;
231        *pending = None;
232        self.state = Some(state);
233        result
234    }
235}
236
237impl State {
238    fn reset(&mut self, request: Request) -> Result<()> {
239        self.finished = None;
240        if self.sealed && matches!(request, Request::Synchronize(_)) {
241            return Err(crate::Error::InvalidInput {
242                field: "opened capture",
243                reason: "producer is already sealed",
244            });
245        }
246        if self.sealed && matches!(request, Request::FinishAt(length) if length != self.offset) {
247            return Err(conflict());
248        }
249        self.request = request;
250        self.in_round = true;
251        self.action = if self.sealed { Action::Seal } else { Action::Recover };
252        self.reader = None;
253        self.prefix = None;
254        self.chunk = None;
255        self.matched = 0;
256        self.remaining_prefix = 0;
257        Ok(())
258    }
259
260    async fn advance(&mut self) -> Result<Step> {
261        if self.conflict && self.request != Request::Interrupt {
262            return Err(conflict());
263        }
264        match self.action {
265            Action::Recover => {
266                self.target.retry_append().await?;
267                self.offset = self.target.length();
268                self.acknowledged.as_ref().expect("acknowledgement retained").store(self.offset, Ordering::Release);
269                self.action = if self.request == Request::Interrupt { Action::Seal } else { Action::Open };
270            }
271            Action::Open => {
272                let mut range = self.range;
273                if let Request::Synchronize(length) | Request::FinishAt(length) = self.request {
274                    if length < self.offset {
275                        self.conflict = true;
276                        return Err(conflict());
277                    }
278                    if range.limit.is_some_and(|limit| length > limit) {
279                        return Err(crate::Error::InvalidInput {
280                            field: "opened capture",
281                            reason: "visible length exceeds selected range",
282                        });
283                    }
284                    range.limit = Some(length);
285                }
286                let file = self.file.try_clone().map_err(crate::local::io_error)?;
287                let mut reader = self.session.read_opened_file(file, range).await?;
288                self.remaining_prefix = self.offset;
289                if let Request::Synchronize(length) | Request::FinishAt(length) = self.request {
290                    let observation = reader.observation().expect("opened capture reads its original handle");
291                    let retained_end = self.range.offset.checked_add(self.offset).ok_or_else(conflict)?;
292                    if observation.0 < retained_end {
293                        self.conflict = true;
294                        return Err(conflict());
295                    }
296                    let visible_end = self.range.offset.checked_add(length).ok_or_else(conflict)?;
297                    if observation.0 < visible_end {
298                        return Err(crate::Error::InvalidInput {
299                            field: "opened capture",
300                            reason: "visible range exceeds opened source",
301                        });
302                    }
303                    if matches!(self.request, Request::Synchronize(_)) && self.observed == Some(observation) {
304                        reader.start_opened(retained_end)?;
305                        self.remaining_prefix = 0;
306                    }
307                }
308                self.reader = Some(reader);
309                self.action = if self.remaining_prefix == 0 { Action::Read } else { Action::Prefix };
310            }
311            Action::Prefix => {
312                let token = self.target.session().ok_or(crate::Error::InvalidInput {
313                    field: "opened capture",
314                    reason: "existing producer is not resumable",
315                })?;
316                let staging = crate::StagedFile {
317                    history: token.history.clone(),
318                    id: token.id.clone(),
319                };
320                self.prefix = Some(self.session.follow_production(&staging, 0).await?);
321                self.action = Action::Read;
322            }
323            Action::Read => {
324                let Some(batch) = self.reader.as_mut().expect("opened source retained").next_raw(false).await? else {
325                    if self.remaining_prefix != 0 {
326                        self.conflict = true;
327                        return Err(conflict());
328                    }
329                    self.reader.as_ref().expect("opened source retained").completion()?;
330                    self.action = Action::Check;
331                    return Ok(Step::Continue);
332                };
333                self.chunk = Some(FileChunk::new(batch.bytes));
334                self.action = if self.remaining_prefix == 0 { Action::Check } else { Action::Match };
335            }
336            Action::Match => {
337                let chunk = self.chunk.as_ref().expect("exact capture bytes retained");
338                let count = (chunk.len() - self.matched).min(usize::try_from(self.remaining_prefix).unwrap_or(usize::MAX));
339                let batch = self
340                    .prefix
341                    .as_mut()
342                    .expect("retained prefix reader")
343                    .next_batch(std::num::NonZeroUsize::new(count).unwrap())
344                    .await?
345                    .ok_or_else(conflict)?;
346                let count = batch.bytes().len();
347                if batch.bytes() != &chunk.as_slice()[self.matched..self.matched + count] {
348                    self.conflict = true;
349                    return Err(conflict());
350                }
351                self.matched += count;
352                self.remaining_prefix -= count as u64;
353                if self.matched == chunk.len() {
354                    self.chunk = None;
355                    self.matched = 0;
356                    self.action = Action::Read;
357                } else if self.remaining_prefix == 0 {
358                    self.action = Action::Check;
359                }
360                if self.remaining_prefix == 0 {
361                    self.prefix = None;
362                }
363            }
364            Action::Check => {
365                let reader = self.reader.as_ref().expect("opened source retained");
366                if matches!(self.request, Request::Synchronize(_) | Request::FinishAt(_)) {
367                    if !reader.observation_unchanged().await? {
368                        self.reader = None;
369                        self.prefix = None;
370                        self.chunk = None;
371                        self.matched = 0;
372                        self.in_round = false;
373                        if matches!(self.request, Request::FinishAt(_)) {
374                            return Err(crate::Error::InvalidInput {
375                                field: "opened capture",
376                                reason: "source changed during final capture",
377                            });
378                        }
379                        return Ok(Step::Synchronized(false));
380                    }
381                    if self.chunk.is_none() {
382                        self.observed = reader.observation();
383                        if matches!(self.request, Request::Synchronize(_)) {
384                            self.in_round = false;
385                            self.reader = None;
386                            return Ok(Step::Synchronized(true));
387                        }
388                    }
389                } else if self.chunk.is_none() && self.consistency == CaptureConsistency::Stable {
390                    reader.check_unchanged().await?;
391                }
392                self.action = if self.chunk.is_some() { Action::Write } else { Action::Seal };
393            }
394            Action::Write => {
395                let chunk = self.chunk.as_mut().expect("exact capture bytes retained");
396                let suffix = chunk.slice(self.matched..chunk.len());
397                self.target.write_retained_at(self.offset, suffix.clone()).await?;
398                self.offset += suffix.len() as u64;
399                self.acknowledged.as_ref().expect("acknowledgement retained").store(self.offset, Ordering::Release);
400                self.chunk = None;
401                self.matched = 0;
402                self.action = Action::Read;
403            }
404            Action::Seal => {
405                let result = self.target.finish().await;
406                if result.is_err() && self.reader.is_some() && self.request != Request::Interrupt {
407                    // A Returned Failure Starts A Retry At Source Validation;
408                    // Cancellation Still Resumes The Exact Accepted Seal Step.
409                    self.action = Action::Check;
410                }
411                self.finished = Some(result?);
412                self.sealed = true;
413                self.action = if self.request == Request::Finish && self.reader.is_some() { Action::Check } else { Action::Seal };
414                return Ok(Step::Finished);
415            }
416        }
417        Ok(Step::Continue)
418    }
419}
420
421/* === Public Functions === */
422
423/* === Private Functions === */
424
425async fn work(mut state: State) -> (State, Result<Step>) {
426    let result = state.advance().await;
427    (state, result)
428}
429
430fn conflict() -> crate::Error {
431    crate::Error::InvalidInput {
432        field: "opened capture",
433        reason: "source differs from retained producer prefix",
434    }
435}
436
437/* === Tests === */