Skip to main content

mito2/manifest/storage/
checkpoint.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashMap;
16use std::sync::Arc;
17
18use common_datasource::compression::CompressionType;
19use common_telemetry::{debug, warn};
20use object_store::{ErrorKind, ObjectStore};
21use serde::{Deserialize, Serialize};
22use snafu::{OptionExt, ResultExt};
23use store_api::ManifestVersion;
24
25use crate::cache::manifest_cache::ManifestCache;
26use crate::error::{
27    CompressObjectSnafu, DecompressObjectSnafu, ManifestCheckpointVanishedSnafu, OpenDalSnafu,
28    Result, SerdeJsonSnafu, Utf8Snafu,
29};
30use crate::manifest::storage::size_tracker::Tracker;
31use crate::manifest::storage::utils::{get_from_cache_decoded, write_and_put_cache};
32use crate::manifest::storage::{
33    FALL_BACK_COMPRESS_TYPE, LAST_CHECKPOINT_FILE, checkpoint_checksum, checkpoint_file, gen_path,
34    verify_checksum,
35};
36
37#[derive(Serialize, Deserialize, Debug)]
38pub(crate) struct CheckpointMetadata {
39    pub size: usize,
40    /// The latest version this checkpoint contains.
41    pub version: ManifestVersion,
42    pub checksum: Option<u32>,
43    pub extend_metadata: HashMap<String, String>,
44}
45
46impl CheckpointMetadata {
47    fn encode(&self) -> Result<Vec<u8>> {
48        Ok(serde_json::to_string(self)
49            .context(SerdeJsonSnafu)?
50            .into_bytes())
51    }
52
53    fn decode(bs: &[u8]) -> Result<Self> {
54        let data = std::str::from_utf8(bs).context(Utf8Snafu)?;
55
56        serde_json::from_str(data).context(SerdeJsonSnafu)
57    }
58}
59
60/// Handle checkpoint storage operations.
61#[derive(Debug, Clone)]
62pub(crate) struct CheckpointStorage<T: Tracker> {
63    object_store: ObjectStore,
64    compress_type: CompressionType,
65    path: String,
66    manifest_cache: Option<ManifestCache>,
67    size_tracker: Arc<T>,
68}
69
70impl<T: Tracker> CheckpointStorage<T> {
71    pub fn new(
72        path: String,
73        object_store: ObjectStore,
74        compress_type: CompressionType,
75        manifest_cache: Option<ManifestCache>,
76        size_tracker: Arc<T>,
77    ) -> Self {
78        Self {
79            object_store,
80            compress_type,
81            path,
82            manifest_cache,
83            size_tracker,
84        }
85    }
86
87    /// Returns the last checkpoint path, because the last checkpoint is not compressed,
88    /// so its path name has nothing to do with the compression algorithm used by `ManifestObjectStore`
89    pub(crate) fn last_checkpoint_path(&self) -> String {
90        format!("{}{}", self.path, LAST_CHECKPOINT_FILE)
91    }
92
93    /// Returns the checkpoint file path under the **current** compression algorithm
94    fn checkpoint_file_path(&self, version: ManifestVersion) -> String {
95        gen_path(&self.path, &checkpoint_file(version), self.compress_type)
96    }
97
98    pub(crate) async fn load_checkpoint(
99        &mut self,
100        metadata: CheckpointMetadata,
101    ) -> Result<Option<(ManifestVersion, Vec<u8>)>> {
102        let version = metadata.version;
103        let path = self.checkpoint_file_path(version);
104
105        // Try to get from cache first
106        if let Some(data) =
107            get_from_cache_decoded(self.manifest_cache.as_ref(), &path, self.compress_type).await
108        {
109            verify_checksum(&data, metadata.checksum)?;
110            self.record_cached_checkpoint_size(version, &path).await;
111            return Ok(Some((version, data)));
112        }
113
114        // Due to backward compatibility, it is possible that the user's checkpoint not compressed,
115        // so if we don't find file by compressed type. fall back to checkpoint not compressed find again.
116        let checkpoint_data = match self.object_store.read(&path).await {
117            Ok(checkpoint) => {
118                let checkpoint_size = checkpoint.len();
119                let decompress_data = self
120                    .compress_type
121                    .decode(checkpoint.clone())
122                    .await
123                    .with_context(|_| DecompressObjectSnafu {
124                        compress_type: self.compress_type,
125                        path: path.clone(),
126                    })?;
127                verify_checksum(&decompress_data, metadata.checksum)?;
128                // set the checkpoint size
129                self.size_tracker.record(version, checkpoint_size as u64);
130                // Add to cache
131                if let Some(cache) = &self.manifest_cache {
132                    cache.put_file(path, checkpoint.to_vec()).await;
133                }
134                Ok(Some(decompress_data))
135            }
136            Err(e) => {
137                if e.kind() == ErrorKind::NotFound {
138                    if self.compress_type != FALL_BACK_COMPRESS_TYPE {
139                        let fall_back_path = gen_path(
140                            &self.path,
141                            &checkpoint_file(version),
142                            FALL_BACK_COMPRESS_TYPE,
143                        );
144                        debug!(
145                            "Failed to load checkpoint from path: {}, fall back to path: {}",
146                            path, fall_back_path
147                        );
148
149                        // Try to get fallback from cache first
150                        if let Some(data) = get_from_cache_decoded(
151                            self.manifest_cache.as_ref(),
152                            &fall_back_path,
153                            FALL_BACK_COMPRESS_TYPE,
154                        )
155                        .await
156                        {
157                            verify_checksum(&data, metadata.checksum)?;
158                            self.record_cached_checkpoint_size(version, &fall_back_path)
159                                .await;
160                            return Ok(Some((version, data)));
161                        }
162
163                        match self.object_store.read(&fall_back_path).await {
164                            Ok(checkpoint) => {
165                                let checkpoint_size = checkpoint.len();
166                                let decompress_data = FALL_BACK_COMPRESS_TYPE
167                                    .decode(checkpoint.clone())
168                                    .await
169                                    .with_context(|_| DecompressObjectSnafu {
170                                        compress_type: FALL_BACK_COMPRESS_TYPE,
171                                        path: fall_back_path.clone(),
172                                    })?;
173                                verify_checksum(&decompress_data, metadata.checksum)?;
174                                self.size_tracker.record(version, checkpoint_size as u64);
175                                // Add fallback to cache
176                                if let Some(cache) = &self.manifest_cache {
177                                    cache.put_file(fall_back_path, checkpoint.to_vec()).await;
178                                }
179                                Ok(Some(decompress_data))
180                            }
181                            Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
182                            Err(e) => return Err(e).context(OpenDalSnafu),
183                        }
184                    } else {
185                        Ok(None)
186                    }
187                } else {
188                    Err(e).context(OpenDalSnafu)
189                }
190            }
191        }?;
192        Ok(checkpoint_data.map(|data| (version, data)))
193    }
194
195    /// Load the latest checkpoint.
196    /// Return manifest version and the raw [RegionCheckpoint](crate::manifest::action::RegionCheckpoint) content if any
197    pub async fn load_last_checkpoint(&mut self) -> Result<Option<(ManifestVersion, Vec<u8>)>> {
198        let Some(metadata) = self.read_last_checkpoint_metadata().await? else {
199            return Ok(None);
200        };
201        self.load_checkpoint(metadata).await
202    }
203
204    /// Loads the checkpoint that LIST found at `version` with `compress_type`.
205    ///
206    /// The checkpoint pointer only provides the checksum, when it points at the same version: the
207    /// checkpoint task of a stale writer can leave it at an older one.
208    ///
209    /// Fails if cleanup removed the checkpoint after the LIST, because the deltas it covered can
210    /// be gone too.
211    pub(crate) async fn load_listed_checkpoint(
212        &self,
213        version: ManifestVersion,
214        compress_type: CompressionType,
215    ) -> Result<(ManifestVersion, Vec<u8>)> {
216        let checksum = self
217            .read_last_checkpoint_metadata()
218            .await?
219            .filter(|metadata| metadata.version == version)
220            .and_then(|metadata| metadata.checksum);
221        let mut storage = Self::new(
222            self.path.clone(),
223            self.object_store.clone(),
224            compress_type,
225            self.manifest_cache.clone(),
226            self.size_tracker.clone(),
227        );
228        storage
229            .load_checkpoint(CheckpointMetadata {
230                size: 0,
231                version,
232                checksum,
233                extend_metadata: HashMap::new(),
234            })
235            .await?
236            .context(ManifestCheckpointVanishedSnafu {
237                version,
238                manifest_dir: &self.path,
239            })
240    }
241
242    /// Reads the small checkpoint pointer without loading its payload.
243    pub(crate) async fn read_last_checkpoint_metadata(&self) -> Result<Option<CheckpointMetadata>> {
244        let last_checkpoint_path = self.last_checkpoint_path();
245
246        // Fetch from remote object store without cache
247        let last_checkpoint_data = match self.object_store.read(&last_checkpoint_path).await {
248            Ok(data) => data.to_vec(),
249            Err(e) if e.kind() == ErrorKind::NotFound => {
250                return Ok(None);
251            }
252            Err(e) => {
253                return Err(e).context(OpenDalSnafu)?;
254            }
255        };
256
257        let checkpoint_metadata = CheckpointMetadata::decode(&last_checkpoint_data)?;
258
259        debug!(
260            "Load checkpoint in path: {}, metadata: {:?}",
261            last_checkpoint_path, checkpoint_metadata
262        );
263
264        Ok(Some(checkpoint_metadata))
265    }
266
267    /// Records the remote object size; accounting failures must not prevent recovery.
268    async fn record_cached_checkpoint_size(&self, version: ManifestVersion, path: &str) {
269        match self.object_store.stat(path).await {
270            Ok(metadata) => self.size_tracker.record(version, metadata.content_length()),
271            Err(error) => warn!(error; "Failed to account for cached checkpoint {}", path),
272        }
273    }
274
275    /// Records stored checkpoint bytes, including the legacy uncompressed path.
276    pub(crate) async fn record_checkpoint_size(&self, version: ManifestVersion) -> Result<()> {
277        let path = self.checkpoint_file_path(version);
278        let metadata = match self.object_store.stat(&path).await {
279            Ok(metadata) => metadata,
280            Err(error)
281                if error.kind() == ErrorKind::NotFound
282                    && self.compress_type != FALL_BACK_COMPRESS_TYPE =>
283            {
284                let path = gen_path(
285                    &self.path,
286                    &checkpoint_file(version),
287                    FALL_BACK_COMPRESS_TYPE,
288                );
289                self.object_store.stat(&path).await.context(OpenDalSnafu)?
290            }
291            Err(error) => return Err(error).context(OpenDalSnafu),
292        };
293        self.size_tracker.record(version, metadata.content_length());
294        Ok(())
295    }
296
297    /// Save the checkpoint manifest file.
298    pub(crate) async fn save_checkpoint(
299        &self,
300        version: ManifestVersion,
301        bytes: &[u8],
302    ) -> Result<()> {
303        let path = self.checkpoint_file_path(version);
304        let data = self
305            .compress_type
306            .encode(bytes)
307            .await
308            .context(CompressObjectSnafu {
309                compress_type: self.compress_type,
310                path: &path,
311            })?;
312        let checkpoint_size = data.len();
313        let checksum = checkpoint_checksum(bytes);
314
315        write_and_put_cache(
316            &self.object_store,
317            self.manifest_cache.as_ref(),
318            &path,
319            data,
320        )
321        .await?;
322
323        // Because last checkpoint file only contain size and version, which is tiny, so we don't compress it.
324        let last_checkpoint_path = self.last_checkpoint_path();
325
326        let checkpoint_metadata = CheckpointMetadata {
327            size: bytes.len(),
328            version,
329            checksum: Some(checksum),
330            extend_metadata: HashMap::new(),
331        };
332
333        debug!(
334            "Save checkpoint in path: {},  metadata: {:?}",
335            last_checkpoint_path, checkpoint_metadata
336        );
337
338        let bytes = checkpoint_metadata.encode()?;
339        self.object_store
340            .write(&last_checkpoint_path, bytes)
341            .await
342            .context(OpenDalSnafu)?;
343
344        self.size_tracker.record(version, checkpoint_size as u64);
345        Ok(())
346    }
347}
348
349#[cfg(test)]
350impl<T: Tracker> CheckpointStorage<T> {
351    pub async fn write_last_checkpoint(
352        &self,
353        version: ManifestVersion,
354        bytes: &[u8],
355    ) -> Result<()> {
356        let path = self.checkpoint_file_path(version);
357        let data = self
358            .compress_type
359            .encode(bytes)
360            .await
361            .context(CompressObjectSnafu {
362                compress_type: self.compress_type,
363                path: &path,
364            })?;
365
366        let checkpoint_size = data.len();
367
368        self.object_store
369            .write(&path, data)
370            .await
371            .context(OpenDalSnafu)?;
372
373        self.size_tracker.record(version, checkpoint_size as u64);
374
375        let last_checkpoint_path = self.last_checkpoint_path();
376        let checkpoint_metadata = CheckpointMetadata {
377            size: bytes.len(),
378            version,
379            checksum: Some(1218259706),
380            extend_metadata: HashMap::new(),
381        };
382
383        debug!(
384            "Rewrite checkpoint in path: {},  metadata: {:?}",
385            last_checkpoint_path, checkpoint_metadata
386        );
387
388        let bytes = checkpoint_metadata.encode()?;
389
390        // Overwrite the last checkpoint with the modified content
391        self.object_store
392            .write(&last_checkpoint_path, bytes.clone())
393            .await
394            .context(OpenDalSnafu)?;
395        Ok(())
396    }
397
398    pub fn set_compress_type(&mut self, compress_type: CompressionType) {
399        self.compress_type = compress_type;
400    }
401}