-
Notifications
You must be signed in to change notification settings - Fork 38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement partition store restore-from-snapshot #2353
base: feat/snapshot-upload
Are you sure you want to change the base?
Changes from all commits
841da61
c15efbb
6c35e25
f1a2de4
6c33f39
aceb786
08c1ad2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -21,11 +21,18 @@ use object_store::aws::AmazonS3Builder; | |||||||||||||||||||||||||||||||||||||||||||||||||||||
use object_store::{MultipartUpload, ObjectStore, PutMode, PutOptions, PutPayload}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
use serde::{Deserialize, Serialize}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
use serde_with::serde_as; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
use tempfile::TempDir; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
use tokio::io; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
use tokio::io::AsyncReadExt; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
use tokio::sync::Semaphore; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
use tokio::task::JoinSet; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
use tokio_util::io::StreamReader; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
use tracing::{debug, info, instrument, trace}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
use url::Url; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
use restate_partition_store::snapshots::{PartitionSnapshotMetadata, SnapshotFormatVersion}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
use restate_partition_store::snapshots::{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
LocalPartitionSnapshot, PartitionSnapshotMetadata, SnapshotFormatVersion, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
use restate_types::config::SnapshotsOptions; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
use restate_types::identifiers::{PartitionId, SnapshotId}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
use restate_types::logs::Lsn; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -49,8 +56,16 @@ pub struct SnapshotRepository { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
object_store: Arc<dyn ObjectStore>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
destination: Url, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
prefix: String, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
base_dir: PathBuf, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// S3 and other stores require a certain minimum size for the parts of a multipart upload. It is an | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// API error to attempt a multipart put below this size, apart from the final segment. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const MULTIPART_UPLOAD_CHUNK_SIZE_BYTES: usize = 5 * 1024 * 1024; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// Maximum number of concurrent downloads when getting snapshots from the repository. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const DOWNLOAD_CONCURRENCY_LIMIT: usize = 8; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
#[serde_as] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
#[derive(Clone, Debug, Serialize, Deserialize)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
pub struct LatestSnapshot { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -92,7 +107,7 @@ impl SnapshotRepository { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
.into_os_string() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
.into_string() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
.map(|path| format!("file://{path}")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
.map_err(|e| anyhow!("Unable to convert path to string: {:?}", e))? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
.map_err(|path| anyhow!("Unable to convert path to string: {:?}", path))? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let destination = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Url::parse(&destination).context("Failed parsing snapshot repository URL")?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -138,6 +153,7 @@ impl SnapshotRepository { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
object_store, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
destination, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
prefix, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
base_dir, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -306,11 +322,133 @@ impl SnapshotRepository { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
Ok(()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// S3 and other stores require a certain minimum size for the parts of a multipart upload. It is an | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// API error to attempt a multipart put below this size, apart from the final segment. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const MULTIPART_UPLOAD_CHUNK_SIZE_BYTES: usize = 5 * 1024 * 1024; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// Discover and download the latest snapshot available. Dropping the returned | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// `LocalPartitionSnapshot` will delete the local snapshot data files. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+326
to
+327
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it because the files are stored in a temp directory? On There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the temp dir also the mechanism to clean things up if downloading it failed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is exactly right! I'm not 100% in love with it - it works but it's a big magical as deletion happens implicitly when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The description seems off though. If I understand the code correctly, then dropping |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
#[instrument( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
level = "debug", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
skip_all, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
err, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
fields(%partition_id), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
pub(crate) async fn get_latest( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
&self, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
partition_id: PartitionId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
) -> anyhow::Result<Option<LocalPartitionSnapshot>> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe something for the future: It feels as if callers might be interested in why |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
let latest_path = object_store::path::Path::from(format!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
"{prefix}{partition_id}/latest.json", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
prefix = self.prefix, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
partition_id = partition_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
let latest = self.object_store.get(&latest_path).await; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
let latest = match latest { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Ok(result) => result, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Err(object_store::Error::NotFound { .. }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
debug!("Latest snapshot data not found in repository"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return Ok(None); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Err(e) => return Err(e.into()), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
let latest: LatestSnapshot = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
serde_json::from_slice(latest.bytes().await?.iter().as_slice())?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
trace!("Latest snapshot metadata: {:?}", latest); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
let snapshot_metadata_path = object_store::path::Path::from(format!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
"{prefix}{partition_id}/{path}/metadata.json", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These paths are probably used on the write and read path. Should we share them through a function. That makes it easier to keep them in sync between the two paths. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
prefix = self.prefix, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
partition_id = partition_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
path = latest.path, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let snapshot_metadata = self.object_store.get(&snapshot_metadata_path).await; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
let snapshot_metadata = match snapshot_metadata { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Ok(result) => result, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Err(object_store::Error::NotFound { .. }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
info!("Latest snapshot points to a snapshot that was not found in the repository!"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return Ok(None); // arguably this could also be an error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+370
to
+371
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am wondering whether this does not denote a "corruption" of our snapshots and therefore might even warrant a panic? I mean we might still be lucky and don't encounter a trim gap because a) we haven't trimmed yet or b) our applied index is still after the trim point. So I guess this might have been the motivation to return |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Err(e) => return Err(e.into()), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
let mut snapshot_metadata: PartitionSnapshotMetadata = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
serde_json::from_slice(snapshot_metadata.bytes().await?.iter().as_slice())?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably works as well:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
if snapshot_metadata.version != SnapshotFormatVersion::V1 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return Err(anyhow!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
"Unsupported snapshot format version: {:?}", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
snapshot_metadata.version | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
// The snapshot ingest directory should be on the same filesystem as the partition store | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
// to minimize IO and disk space usage during import. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let snapshot_dir = TempDir::with_prefix_in( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
format!("{}-", snapshot_metadata.snapshot_id), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
&self.base_dir, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
)?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
debug!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
snapshot_id = %snapshot_metadata.snapshot_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
path = ?snapshot_dir.path(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
"Getting snapshot data", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
let directory = snapshot_dir.path().to_string_lossy().to_string(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let concurrency_limiter = Arc::new(Semaphore::new(DOWNLOAD_CONCURRENCY_LIMIT)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
let mut downloads = JoinSet::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
for file in &mut snapshot_metadata.files { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let filename = file.name.trim_start_matches("/"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let key = object_store::path::Path::from(format!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
"{prefix}{partition_id}/{path}/{filename}", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
prefix = self.prefix, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
partition_id = partition_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
path = latest.path, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
filename = filename, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let file_path = snapshot_dir.path().join(filename); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let concurrency_limiter = Arc::clone(&concurrency_limiter); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let object_store = Arc::clone(&self.object_store); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
downloads.spawn(async move { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let _permit = concurrency_limiter.acquire().await?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let mut file_data = StreamReader::new(object_store.get(&key).await?.into_stream()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let mut snapshot_file = tokio::fs::File::create_new(&file_path).await?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let size = io::copy(&mut file_data, &mut snapshot_file).await?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe a sanity check that the downloaded file size matches what is expected from the snapshot metadata There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this solution for copying the file in streaming fashion :-) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
trace!(%key, ?size, "Downloaded snapshot data file to {:?}", file_path); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
anyhow::Ok(()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
// patch the directory path to reflect the actual location on the restoring node | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
file.directory = directory.clone(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
loop { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
match downloads.join_next().await { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
None => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Some(Err(e)) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
downloads.abort_all(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return Err(e.into()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+431
to
+432
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While I think it does not make a difference right now for correctness, I would still recommend to drain |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Some(Ok(_)) => {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it intentional to not handle errors returned by the download routine ? the Check suggestion below There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you are right and we need to handle the inner error case as well. Right now, we might accept an incomplete snapshot as complete if any of the file downloads fails with an error and not a panic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably also want to include in the error message which file failed the download process. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+425
to
+436
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
info!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
snapshot_id = %snapshot_metadata.snapshot_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
path = ?snapshot_dir.path(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
"Downloaded partition snapshot", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Ok(Some(LocalPartitionSnapshot { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
base_dir: snapshot_dir.into_path(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
min_applied_lsn: snapshot_metadata.min_applied_lsn, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
db_comparator_name: snapshot_metadata.db_comparator_name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
files: snapshot_metadata.files, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
key_range: snapshot_metadata.key_range.clone(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
})) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
async fn put_snapshot_object( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
snapshot_path: &Path, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good check :-)