Skip to main content

feldera_types/
checkpoint.rs

1use std::time::Duration;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use utoipa::ToSchema;
6use uuid::Uuid;
7
8use crate::suspend::TemporarySuspendError;
9
10/// Checkpoint status returned by the `/checkpoint_status` endpoint.
11#[derive(Clone, Debug, Default, Serialize, Deserialize, ToSchema)]
12pub struct CheckpointStatus {
13    /// Most recently successful checkpoint.
14    pub success: Option<u64>,
15
16    /// Most recently failed checkpoint, and the associated error.
17    ///
18    /// This tracks transient checkpoint failures (e.g. I/O errors during
19    /// writing).  A subsequent successful checkpoint will not clear this
20    /// field — it always reflects the *last* failure that occurred.
21    pub failure: Option<CheckpointFailure>,
22}
23
24/// Current checkpoint activity state.
25#[derive(Clone, Debug, Default, Serialize, Deserialize, ToSchema)]
26#[serde(tag = "status", rename_all = "snake_case")]
27pub enum CheckpointActivity {
28    /// No checkpoint is pending or in progress.
29    #[default]
30    Idle,
31
32    /// A checkpoint has been requested but is delayed for temporary reasons
33    /// (e.g. replaying, bootstrapping, transaction in progress, or input
34    /// endpoint barriers that require the coordinator to run steps).
35    Delayed {
36        /// Why the checkpoint cannot proceed yet.
37        reasons: Vec<TemporarySuspendError>,
38        /// When the delay started (serialized as ISO 8601).
39        delayed_since: DateTime<Utc>,
40    },
41
42    /// A checkpoint is currently being written to storage.
43    InProgress {
44        /// When the checkpoint write started (serialized as ISO 8601).
45        started_at: DateTime<Utc>,
46    },
47}
48
49/// Information about a failed checkpoint.
50#[derive(Clone, Debug, Default, Serialize, Deserialize, ToSchema)]
51pub struct CheckpointFailure {
52    /// Sequence number of the failed checkpoint.
53    pub sequence_number: u64,
54
55    /// Error message associated with the failure.
56    pub error: String,
57
58    /// When the failure occurred (serialized as ISO 8601).
59    pub failed_at: DateTime<Utc>,
60}
61
62/// Response to a checkpoint request.
63#[derive(Clone, Debug, Default, Serialize, Deserialize, ToSchema)]
64pub struct CheckpointResponse {
65    pub checkpoint_sequence_number: u64,
66
67    /// Pass back to `/checkpoint_status` to detect pipeline restarts.
68    ///
69    /// Only `None` if sent by an older pipeline.
70    pub incarnation_uuid: Option<Uuid>,
71}
72
73impl CheckpointResponse {
74    pub fn new(checkpoint_sequence_number: u64, incarnation_uuid: Uuid) -> Self {
75        Self {
76            checkpoint_sequence_number,
77            incarnation_uuid: Some(incarnation_uuid),
78        }
79    }
80}
81
82/// Response to a sync checkpoint request.
83#[derive(Clone, Debug, Default, Serialize, Deserialize, ToSchema)]
84pub struct CheckpointSyncResponse {
85    pub checkpoint_uuid: Uuid,
86
87    /// Pass back to `/checkpoint/sync_status` to detect pipeline restarts.
88    ///
89    /// Only `None` if sent by an older pipeline.
90    pub incarnation_uuid: Option<Uuid>,
91}
92
93impl CheckpointSyncResponse {
94    pub fn new(checkpoint_uuid: Uuid, incarnation_uuid: Uuid) -> Self {
95        Self {
96            checkpoint_uuid,
97            incarnation_uuid: Some(incarnation_uuid),
98        }
99    }
100}
101
102/// Query parameters for `/checkpoint_status` and `/checkpoint/sync_status`.
103#[derive(Clone, Debug, Default, Serialize, Deserialize)]
104pub struct CheckpointStatusQuery {
105    /// Pass the value from the initial response, to detect pipeline restarts.
106    ///
107    /// Optional to allow older clients to continue working.
108    pub incarnation_uuid: Option<Uuid>,
109}
110
111/// Checkpoint status returned by the `/checkpoint/sync_status` endpoint.
112#[derive(Clone, Debug, Default, Serialize, Deserialize, ToSchema)]
113pub struct CheckpointSyncStatus {
114    /// Most recently successful checkpoint sync.
115    pub success: Option<Uuid>,
116
117    /// Most recently failed checkpoint sync, and the associated error.
118    pub failure: Option<CheckpointSyncFailure>,
119
120    /// Most recently successful automated periodic checkpoint sync.
121    pub periodic: Option<Uuid>,
122}
123
124/// Information about a failed checkpoint sync.
125#[derive(Clone, Debug, Default, Serialize, Deserialize, ToSchema)]
126pub struct CheckpointSyncFailure {
127    /// UUID of the failed checkpoint.
128    pub uuid: Uuid,
129
130    /// Error message associated with the failure.
131    pub error: String,
132}
133
134/// Holds meta-data about a checkpoint that was taken for persistent storage
135/// and recovery of a circuit's state.
136#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
137pub struct CheckpointMetadata {
138    /// A unique identifier for the given checkpoint.
139    ///
140    /// This is used to identify the checkpoint in the file-system hierarchy.
141    pub uuid: Uuid,
142    /// An optional name for the checkpoint.
143    pub identifier: Option<String>,
144    /// Fingerprint of the circuit at the time of the checkpoint.
145    // Uses the full 64-bit range.
146    #[schema(format = "uint64")]
147    pub fingerprint: u64,
148    /// Total size of the checkpoint files in bytes.
149    pub size: Option<u64>,
150    /// Total number of steps made.
151    pub steps: Option<u64>,
152    /// Total number of records processed.
153    pub processed_records: Option<u64>,
154}
155
156/// Identifies a host within a multihost pipeline.
157///
158/// Used to scope checkpoint sync operations (push/pull) to the correct
159/// remote subdirectory.
160#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
161pub struct HostInfo {
162    /// Zero-based index of this host in the pipeline layout.
163    pub host_idx: usize,
164    /// Total number of hosts in the pipeline layout.
165    pub n_hosts: usize,
166}
167
168impl HostInfo {
169    /// Returns the remote storage subdirectory prefix for this host,
170    /// e.g. `"host0"` for index 0.
171    pub fn prefix(&self) -> String {
172        if self.host_idx >= self.n_hosts {
173            log::warn!(
174                "HostInfo::prefix: host_idx {} >= n_hosts {}",
175                self.host_idx,
176                self.n_hosts
177            );
178        }
179        format!("host{}", self.host_idx)
180    }
181}
182
183/// Format of `pspine-batches-*.dat` in storage.
184///
185/// These files exist to be a simple format for higher-level code and outside
186/// tools to parse.  The spine itself writes them for that purpose, but it does
187/// not read them.
188#[derive(Debug, Serialize, Deserialize)]
189pub struct PSpineBatches {
190    pub files: Vec<String>,
191}
192
193/// Serialized form of `dependencies.json` on disk.
194///
195/// Two formats. New checkpoints write the struct form (`V2`) carrying both
196/// the batch list referenced at the storage root *and* the list of per-operator
197/// state files inside the checkpoint dir. Old checkpoints stored only the
198/// batch-filename array (`V1`); they remain readable so a rolling upgrade
199/// across in-flight checkpoints is safe.
200#[derive(Debug, Deserialize)]
201#[serde(untagged)]
202pub enum CheckpointDependencies {
203    V2 {
204        /// Batch filenames at the storage root (`w*.feldera`) that the
205        /// checkpoint references for GC retention.
206        batches: Vec<String>,
207        /// Per-operator state filenames inside the checkpoint dir
208        /// (e.g. `pspine-*.dat`, `z1-*.dat`, `CHECKPOINT`). Consumed by
209        /// restore-time verification. Defaulted to empty for forward compat.
210        #[serde(default)]
211        state_files: Vec<String>,
212    },
213    /// Legacy form: JSON array of batch filenames at the storage root
214    /// (`w*.feldera`). No state-file manifest.
215    V1(Vec<String>),
216}
217
218impl CheckpointDependencies {
219    /// Batch files the checkpoint references at the storage root
220    /// (`w*.feldera`). Present in both V1 and V2 checkpoints.
221    pub fn batches(&self) -> &[String] {
222        match self {
223            CheckpointDependencies::V2 { batches, .. } => batches,
224            CheckpointDependencies::V1(batches) => batches,
225        }
226    }
227
228    /// Per-operator state files the checkpoint owned at commit time. These
229    /// live inside the checkpoint dir (e.g. `pspine-*.dat`, `z1-*.dat`).
230    /// Empty for V1 checkpoints, which predate the state-file manifest.
231    pub fn state_files(&self) -> &[String] {
232        match self {
233            CheckpointDependencies::V2 { state_files, .. } => state_files,
234            CheckpointDependencies::V1(_) => &[],
235        }
236    }
237}
238
239/// Serialized form written to `dependencies.json`.  Always emits V2.
240#[derive(Debug, Serialize)]
241pub struct CheckpointDependenciesWrite<'a> {
242    pub batches: &'a [String],
243    pub state_files: &'a [String],
244}
245
246/// A checkpoint that exists in remote object storage.
247#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
248pub struct RemoteCheckpoint {
249    /// UUID of the checkpoint.
250    pub uuid: Uuid,
251}
252
253#[derive(Debug)]
254pub struct CheckpointSyncMetrics {
255    pub duration: Duration,
256    pub speed: u64,
257    pub bytes: u64,
258}
259
260/// Status of a `POST /coordination/checkpoint/pull` operation.
261///
262/// Returned by `GET /coordination/checkpoint/pull_status`.
263#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
264#[serde(tag = "status", rename_all = "snake_case")]
265pub enum CheckpointPullStatus {
266    /// No pull has been requested yet.
267    #[default]
268    NotRequested,
269    /// A pull is currently in progress.
270    InProgress,
271    /// The pull completed successfully.
272    Ok,
273    /// The pull failed.
274    Error { error: String },
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    /// A newer client's `CheckpointResponse`/`CheckpointSyncResponse` must
282    /// still parse a response from an older pipeline that predates the
283    /// `incarnation_uuid` field, yielding `None` rather than a
284    /// deserialization error.
285    #[test]
286    fn deserialize_response_missing_incarnation_uuid() {
287        let resp: CheckpointResponse =
288            serde_json::from_str(r#"{"checkpoint_sequence_number": 3}"#).unwrap();
289        assert_eq!(resp.checkpoint_sequence_number, 3);
290        assert_eq!(resp.incarnation_uuid, None);
291
292        let sync_resp: CheckpointSyncResponse =
293            serde_json::from_str(r#"{"checkpoint_uuid": "00000000-0000-0000-0000-000000000001"}"#)
294                .unwrap();
295        assert_eq!(sync_resp.incarnation_uuid, None);
296    }
297
298    /// Legacy bare-array dependencies.json from older checkpoints must still
299    /// parse, yielding an empty state-file list (no manifest verification).
300    #[test]
301    fn deserialize_v1_legacy_array() {
302        let raw = r#"["w0-aaa.feldera", "w1-bbb.feldera"]"#;
303        let deps: CheckpointDependencies = serde_json::from_str(raw).unwrap();
304        assert!(deps.state_files().is_empty());
305        assert_eq!(deps.batches(), &["w0-aaa.feldera", "w1-bbb.feldera"]);
306    }
307
308    /// Current struct form carries both lists.
309    #[test]
310    fn deserialize_v2_struct() {
311        let raw = r#"{
312            "batches": ["w0-aaa.feldera"],
313            "state_files": ["pspine-0-zzz.dat", "CHECKPOINT"]
314        }"#;
315        let deps: CheckpointDependencies = serde_json::from_str(raw).unwrap();
316        assert_eq!(deps.state_files(), &["pspine-0-zzz.dat", "CHECKPOINT"]);
317        assert_eq!(deps.batches(), &["w0-aaa.feldera"]);
318    }
319
320    /// V2 without `state_files` (partial writer, partial migration)
321    /// deserializes with an empty state-file list rather than failing.
322    #[test]
323    fn deserialize_v2_missing_state_files_defaults_to_empty() {
324        let raw = r#"{"batches": ["w0-aaa.feldera"]}"#;
325        let deps: CheckpointDependencies = serde_json::from_str(raw).unwrap();
326        assert!(deps.state_files().is_empty());
327        assert_eq!(deps.batches(), &["w0-aaa.feldera"]);
328    }
329
330    /// Writes emit V2 and round-trip back to the same content.
331    #[test]
332    fn write_v2_round_trips() {
333        let batches = vec!["w0-x.feldera".to_string()];
334        let state_files = vec!["pspine-0-y.dat".to_string()];
335        let json = serde_json::to_string(&CheckpointDependenciesWrite {
336            batches: &batches,
337            state_files: &state_files,
338        })
339        .unwrap();
340        let deps: CheckpointDependencies = serde_json::from_str(&json).unwrap();
341        assert_eq!(deps.state_files(), state_files.as_slice());
342        assert_eq!(deps.batches(), batches.as_slice());
343    }
344
345    #[test]
346    fn host_info_prefix_formats_index() {
347        assert_eq!(
348            HostInfo {
349                host_idx: 0,
350                n_hosts: 2
351            }
352            .prefix(),
353            "host0"
354        );
355        assert_eq!(
356            HostInfo {
357                host_idx: 1,
358                n_hosts: 2
359            }
360            .prefix(),
361            "host1"
362        );
363        assert_eq!(
364            HostInfo {
365                host_idx: 42,
366                n_hosts: 100
367            }
368            .prefix(),
369            "host42"
370        );
371    }
372}