Skip to content

Commit db3bc25

Browse files
Merge pull request #55 from gitmono-dev/perf/mst2-fixed-content-metadata
perf: avoid redundant blob reads for MST2 HEAD and cached chunks
2 parents b98a9ab + 5ab8e6f commit db3bc25

5 files changed

Lines changed: 1456 additions & 59 deletions

File tree

‎src/api/router/snapshot_content.rs‎

Lines changed: 160 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,141 @@ use super::{abs_view_path, internal, mst2_error_response, request::Mst2Bytes, tr
1717
use crate::ceres::snapshot::{
1818
chunks::{ChunkProjection, get_or_project},
1919
error::{SnapshotError, SnapshotErrorCode},
20-
pages::{WalkOutcome, base64_of, hex_of, resolve_abs},
20+
pages::{
21+
MetadataWalkOutcome, WalkOutcome, base64_of, fetch_raw_blob, hex_of, resolve_abs,
22+
resolve_abs_metadata,
23+
},
2124
resolver::FsKind,
2225
runtime::runtime,
2326
view::validate_scope_relative_path,
2427
};
2528

2629
/// One file resolved at a fixed path with verified content.
2730
struct ResolvedFile {
28-
fs_kind: FsKind,
2931
digest: [u8; 32],
3032
size: u64,
3133
raw: Vec<u8>,
3234
}
3335

36+
struct ResolvedFileMetadata {
37+
fs_kind: FsKind,
38+
oid: String,
39+
digest: [u8; 32],
40+
size: u64,
41+
}
42+
43+
#[allow(clippy::result_large_err)]
44+
async fn fixed_root_tree<T: crate::ceres::api_service::ApiHandler + ?Sized>(
45+
handler: &T,
46+
oid: &str,
47+
) -> Result<git_internal::internal::object::tree::Tree, Response> {
48+
let tree = handler.get_tree_by_hash(oid).await.map_err(internal)?;
49+
if tree.id.to_string() != oid {
50+
return Err(mst2_error_response(SnapshotError::new(
51+
SnapshotErrorCode::IntegrityError,
52+
"fetched fixed root tree identity mismatch",
53+
)));
54+
}
55+
Ok(tree)
56+
}
57+
58+
#[allow(clippy::result_large_err)]
59+
async fn resolve_file_metadata<T: crate::ceres::api_service::ApiHandler + ?Sized>(
60+
handler: &T,
61+
root_tree: &git_internal::internal::object::tree::Tree,
62+
scope: &str,
63+
path: &str,
64+
expected_digest: Option<&str>,
65+
) -> Result<ResolvedFileMetadata, Response> {
66+
validate_scope_relative_path(path).map_err(mst2_error_response)?;
67+
let (fs_kind, oid) = match resolve_abs_metadata(handler, root_tree, &abs_view_path(scope, path))
68+
.await
69+
.map_err(mst2_error_response)?
70+
{
71+
MetadataWalkOutcome::FoundFile { fs_kind, oid } => (fs_kind, oid),
72+
MetadataWalkOutcome::FoundDir => {
73+
return Err(mst2_error_response(SnapshotError::new(
74+
SnapshotErrorCode::NotDirectory,
75+
format!("{path} is a directory"),
76+
)));
77+
}
78+
MetadataWalkOutcome::Absent => {
79+
return Err(mst2_error_response(SnapshotError::new(
80+
SnapshotErrorCode::PathNotFound,
81+
format!("{path} absent in the fixed view"),
82+
)));
83+
}
84+
MetadataWalkOutcome::NotDirectory { symlink } => {
85+
return Err(mst2_error_response(SnapshotError::new(
86+
if symlink {
87+
SnapshotErrorCode::SymlinkTraversal
88+
} else {
89+
SnapshotErrorCode::NotDirectory
90+
},
91+
format!("{path}: intermediate component is not a directory"),
92+
)));
93+
}
94+
};
95+
let storage = handler.get_context();
96+
let mut verified = storage
97+
.mono_storage()
98+
.get_verified_blobs(vec![oid.clone()])
99+
.await
100+
.map_err(|error| {
101+
let code = if matches!(
102+
error,
103+
crate::common::errors::MegaError::ObjStorageInconsistent(_)
104+
) {
105+
SnapshotErrorCode::IntegrityError
106+
} else {
107+
SnapshotErrorCode::Internal
108+
};
109+
tracing::warn!(error = %error, "fixed content metadata lookup failed");
110+
mst2_error_response(SnapshotError::new(
111+
code,
112+
"fixed content metadata lookup failed",
113+
))
114+
})?;
115+
let fact = verified.remove(&oid).ok_or_else(|| {
116+
mst2_error_response(SnapshotError::new(
117+
SnapshotErrorCode::MetadataNotReady,
118+
"fixed object has no current verified size and digest fact",
119+
))
120+
})?;
121+
let digest: [u8; 32] = fact.raw_sha256.as_slice().try_into().map_err(|_| {
122+
mst2_error_response(SnapshotError::new(
123+
SnapshotErrorCode::IntegrityError,
124+
"invalid verified content digest",
125+
))
126+
})?;
127+
let size = u64::try_from(fact.size).map_err(|_| {
128+
mst2_error_response(SnapshotError::new(
129+
SnapshotErrorCode::IntegrityError,
130+
"invalid verified content size",
131+
))
132+
})?;
133+
if fs_kind == FsKind::Symlink && !(1..=4095).contains(&size) {
134+
return Err(mst2_error_response(SnapshotError::new(
135+
SnapshotErrorCode::IntegrityError,
136+
"verified symlink size is outside the fixed filesystem profile",
137+
)));
138+
}
139+
if let Some(expected) = expected_digest
140+
&& expected != format!("sha256:{}", hex_of(&digest))
141+
{
142+
return Err(mst2_error_response(SnapshotError::new(
143+
SnapshotErrorCode::DigestMismatch,
144+
format!("{path}: content does not match expected_digest"),
145+
)));
146+
}
147+
Ok(ResolvedFileMetadata {
148+
fs_kind,
149+
oid,
150+
digest,
151+
size,
152+
})
153+
}
154+
34155
pub(super) fn fs_kind_str(k: FsKind) -> &'static str {
35156
match k {
36157
FsKind::Regular => "regular",
@@ -57,11 +178,7 @@ async fn resolve_file<T: crate::ceres::api_service::ApiHandler + ?Sized>(
57178
.map_err(mst2_error_response)?
58179
{
59180
WalkOutcome::FoundFile {
60-
fs_kind,
61-
raw,
62-
size,
63-
digest,
64-
..
181+
raw, size, digest, ..
65182
} => {
66183
if let Some(expected) = expected_digest
67184
&& expected != format!("sha256:{}", hex_of(&digest))
@@ -71,12 +188,7 @@ async fn resolve_file<T: crate::ceres::api_service::ApiHandler + ?Sized>(
71188
format!("{path}: content does not match expected_digest"),
72189
)));
73190
}
74-
Ok(ResolvedFile {
75-
fs_kind,
76-
digest,
77-
size,
78-
raw,
79-
})
191+
Ok(ResolvedFile { digest, size, raw })
80192
}
81193
WalkOutcome::FoundDir => Err(mst2_error_response(SnapshotError::new(
82194
SnapshotErrorCode::NotDirectory,
@@ -128,11 +240,8 @@ pub(super) async fn blob_head(
128240
.api_handler(std::path::Path::new("/"))
129241
.await
130242
.map_err(internal)?;
131-
let root_tree = handler
132-
.get_tree_by_hash(&ctx.root_tree_oid)
133-
.await
134-
.map_err(internal)?;
135-
let f = resolve_file(
243+
let root_tree = fixed_root_tree(handler.as_ref(), &ctx.root_tree_oid).await?;
244+
let f = resolve_file_metadata(
136245
handler.as_ref(),
137246
&root_tree,
138247
&ctx.built.descriptor.scope,
@@ -144,6 +253,7 @@ pub(super) async fn blob_head(
144253
.header("content-length", f.size.to_string())
145254
.header("etag", format!("\"sha256:{}\"", hex_of(&f.digest)))
146255
.header("cache-control", "private, no-cache, no-transform")
256+
.header("vary", "Authorization, Accept")
147257
.header("x-mega-fs-kind", fs_kind_str(f.fs_kind))
148258
.header("x-mega-content-size", f.size.to_string())
149259
.body(axum::body::Body::empty())
@@ -201,10 +311,7 @@ pub(super) async fn objects(
201311
.api_handler(std::path::Path::new("/"))
202312
.await
203313
.map_err(internal)?;
204-
let root_tree = handler
205-
.get_tree_by_hash(&ctx.root_tree_oid)
206-
.await
207-
.map_err(internal)?;
314+
let root_tree = fixed_root_tree(handler.as_ref(), &ctx.root_tree_oid).await?;
208315
let scope = ctx.built.descriptor.scope.clone();
209316
// Copied references: each per-item future borrows the shared walk state
210317
// without moving it (the stream is an FnMut over owned items).
@@ -317,23 +424,40 @@ async fn project_for<T: crate::ceres::api_service::ApiHandler + ?Sized>(
317424
path: &str,
318425
expected_digest: Option<&str>,
319426
) -> Result<std::sync::Arc<ChunkProjection>, Response> {
320-
let f = resolve_file(handler, root_tree, scope, path, expected_digest).await?;
427+
let f = resolve_file_metadata(handler, root_tree, scope, path, expected_digest).await?;
321428
// The first request for a digest builds the projection from the fixed
322429
// Git object; later requests slice the cached representation. A miss
323430
// rebuilds, never errors with "missing chunk".
324431
let digest = f.digest;
325-
get_or_project(digest, || async move {
326-
let raw = f.raw;
327-
if raw.len() as u64 != f.size {
432+
let size = f.size;
433+
let projection = get_or_project(digest, || async move {
434+
let raw = fetch_raw_blob(handler, &f.oid).await?;
435+
if raw.len() as u64 != size {
328436
return Err(SnapshotError::new(
329-
SnapshotErrorCode::Internal,
330-
"resolved blob size disagrees with its length",
437+
SnapshotErrorCode::IntegrityError,
438+
"fixed blob length disagrees with its verified size fact",
331439
));
332440
}
333441
Ok(raw)
334442
})
335443
.await
336-
.map_err(mst2_error_response)
444+
.map_err(|error| {
445+
mst2_error_response(if error.code == SnapshotErrorCode::DigestMismatch {
446+
SnapshotError::new(
447+
SnapshotErrorCode::IntegrityError,
448+
"fixed blob digest disagrees with its verified fact",
449+
)
450+
} else {
451+
error
452+
})
453+
})?;
454+
if projection.map.file_size != size || projection.map.file_content_id != digest {
455+
return Err(mst2_error_response(SnapshotError::new(
456+
SnapshotErrorCode::IntegrityError,
457+
"cached projection disagrees with the fixed verified fact",
458+
)));
459+
}
460+
Ok(projection)
337461
}
338462

339463
#[allow(clippy::result_large_err)]
@@ -351,10 +475,7 @@ pub(super) async fn chunk_map(
351475
.api_handler(std::path::Path::new("/"))
352476
.await
353477
.map_err(internal)?;
354-
let root_tree = handler
355-
.get_tree_by_hash(&ctx.root_tree_oid)
356-
.await
357-
.map_err(internal)?;
478+
let root_tree = fixed_root_tree(handler.as_ref(), &ctx.root_tree_oid).await?;
358479
let scope = ctx.built.descriptor.scope.clone();
359480
let proj = project_for(
360481
handler.as_ref(),
@@ -419,10 +540,7 @@ pub(super) async fn chunk_map_pages(
419540
.api_handler(std::path::Path::new("/"))
420541
.await
421542
.map_err(internal)?;
422-
let root_tree = handler
423-
.get_tree_by_hash(&ctx.root_tree_oid)
424-
.await
425-
.map_err(internal)?;
543+
let root_tree = fixed_root_tree(handler.as_ref(), &ctx.root_tree_oid).await?;
426544
let scope = ctx.built.descriptor.scope.clone();
427545
let proj = project_for(
428546
handler.as_ref(),
@@ -528,10 +646,7 @@ pub(super) async fn chunks(
528646
.api_handler(std::path::Path::new("/"))
529647
.await
530648
.map_err(internal)?;
531-
let root_tree = handler
532-
.get_tree_by_hash(&ctx.root_tree_oid)
533-
.await
534-
.map_err(internal)?;
649+
let root_tree = fixed_root_tree(handler.as_ref(), &ctx.root_tree_oid).await?;
535650
let scope = ctx.built.descriptor.scope.clone();
536651
let mut planned: Vec<Planned> = Vec::new();
537652
let mut units: Vec<(String, u64)> = Vec::new();
@@ -659,3 +774,7 @@ fn ensure(state: &crate::api::MonoApiServiceState) -> Result<(), Response> {
659774
)))
660775
}
661776
}
777+
778+
#[cfg(test)]
779+
#[path = "snapshot_content_tests.rs"]
780+
mod tests;

0 commit comments

Comments
 (0)