ifc_lite_server/routes/parse/
stream_progress.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! The per-batch progress checkpoints a live parquet-stream parse reported,
6//! cached so a later cache hit can replay the SAME numbers.
7//!
8//! `progress` events are in units of geometry JOBS, not meshes: the pipeline
9//! calls its batch callback with `processed_jobs` / `total_jobs`
10//! (`processing/src/processor/mod.rs`), and a job can produce several meshes
11//! or none. A replay that counted the meshes it emitted would therefore
12//! report different units AND a different total from a miss on the same file,
13//! over the same public event. Nothing in the geometry blob records the job
14//! counts, so the live path writes them here and the replay reads them back.
15
16use crate::services::cache::DiskCache;
17use serde::{Deserialize, Serialize};
18
19/// Cache key for the checkpoint sidecar, keyed alongside the geometry and
20/// metadata entries for the same request.
21///
22/// `v1` is `{ total_jobs, after_batch }`. Bump on any change to what those
23/// numbers mean; an entry that fails to deserialize is treated as absent, so
24/// a bump degrades to the mesh-unit fallback rather than to garbage.
25pub(super) fn stream_progress_cache_key(cache_key: &str) -> String {
26    format!("{cache_key}-parquet-progress-v1")
27}
28
29/// What a live parse reported, in job units.
30#[derive(Debug, Default, Clone, Serialize, Deserialize)]
31pub(super) struct StreamProgress {
32    /// The `total` every `progress` event carried (the pipeline's `total_jobs`).
33    pub total_jobs: usize,
34    /// `processed_jobs` as of each `batch` event, in batch order. One entry
35    /// per batch, so `after_batch.len()` must equal the replay's batch count
36    /// for the checkpoints to line up.
37    pub after_batch: Vec<usize>,
38}
39
40/// Collects [`StreamProgress`] from the live event stream.
41///
42/// The pipeline emits `Batch` then the `Progress` that accounts for it, so a
43/// batch's checkpoint is the value of the NEXT progress event. Progress
44/// events with no batch before them (the leading `processed: 0`, and any
45/// chunk that produced no meshes) carry no batch to attach to and are only
46/// used for `total_jobs`.
47#[derive(Default)]
48pub(super) struct StreamProgressRecorder {
49    progress: StreamProgress,
50    batch_pending: bool,
51}
52
53impl StreamProgressRecorder {
54    pub(super) fn on_batch(&mut self) {
55        self.batch_pending = true;
56    }
57
58    pub(super) fn on_progress(&mut self, processed: usize, total: usize) {
59        self.progress.total_jobs = total;
60        if self.batch_pending {
61            self.progress.after_batch.push(processed);
62            self.batch_pending = false;
63        }
64    }
65
66    /// Take what was recorded, leaving the recorder empty.
67    pub(super) fn take(&mut self) -> StreamProgress {
68        std::mem::take(&mut self.progress)
69    }
70}
71
72/// Store the checkpoints beside the geometry. Best-effort: a failure here
73/// costs the next cache hit its job-unit progress, not its correctness.
74pub(super) async fn cache_stream_progress(
75    cache: &DiskCache,
76    cache_key: &str,
77    progress: &StreamProgress,
78) {
79    let Ok(bytes) = serde_json::to_vec(progress) else {
80        return;
81    };
82    if let Err(e) = cache
83        .set_bytes(&stream_progress_cache_key(cache_key), &bytes)
84        .await
85    {
86        tracing::warn!(error = %e, "Failed to cache stream progress checkpoints");
87    }
88}
89
90/// Read back the checkpoints for a cache hit. `None` for an entry written
91/// before this sidecar existed, or one that no longer deserializes.
92pub(super) async fn load_stream_progress(
93    cache: &DiskCache,
94    cache_key: &str,
95) -> Option<StreamProgress> {
96    let bytes = cache
97        .get_bytes(&stream_progress_cache_key(cache_key))
98        .await
99        .ok()??;
100    serde_json::from_slice(&bytes).ok()
101}
102
103#[cfg(test)]
104#[path = "stream_progress_tests.rs"]
105mod stream_progress_tests;