mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-06 05:17:42 +00:00
fix(cache): stop the GET body-cache hook from bypassing ReadPlan (#4654)
* fix(cache): gate GET body-cache hook to preserve ReadPlan output The ecstore GET body-cache hook serves cached full-object plaintext directly, bypassing ReadPlan/ReadTransform. That is only sound when the normal read path returns that same plaintext byte-for-byte. Two probe conditions were missing, plus two usecase-layer planner gaps. ODC-01 (backlog#1108): raw/data-movement reads. ReadPlan::build returns the STORED representation for raw_data_movement_read (e.g. compressed bytes, length = oi.size), but the cache holds the post-decompression body. Decommission (raw_data_movement_read: true) would receive decompressed plaintext where raw compressed bytes are required, silently corrupting the destination pool. ODC-02 (backlog#1109): compressed objects. ReadTransform::Compressed rewrites object_info.size to the decompressed length; on a hook hit object_info is returned unchanged, so object_info.size is the compressed size while the stream carries the decompressed body. UploadPartCopy then uses src_info.size as the copy length and truncates the part. Fix: gate the hook probe with should_probe_body_cache_hook, refusing raw_data_movement_read, data_movement, and compressed objects, mirroring the conditions get_small_object_direct_memory_decision already applies. ODC-33 (backlog#1138): build_get_object_body_cache_plan lacked the is_remote() exclusion the ecstore hook enforces; add it so transitioned (remote-tier) objects are excluded uniformly. ODC-C1 (backlog#1142): zero-length bodies save no I/O (ecstore returns an empty body before the hook probe) yet the planner admitted them; change the guard to response_content_length <= 0 so they plan Skip, mirroring should_buffer_get_object_in_memory_with_threshold. Tests: body_cache_hook_gate_tests (4) cover plain-probe plus raw/data-movement/compressed skips; planner gains plan_skips_remote_transitioned_objects and plan_skips_zero_length_objects. Co-Authored-By: heihutu <heihutu@gmail.com> * fix(cache): allow compressed bodies via a fail-closed read allow-list The body-cache hook probe was gated by a deny-list that refused compressed objects outright, which cost the cache every compressed body — a growing share of stored data. Replace it with an allow-list that returns the exact plaintext length a hit may serve, or None. full_object_plaintext_len() answers a single question: would the normal ReadPlan produce this object's complete plaintext, and under which size? Compressed objects now qualify, and the hit site publishes the returned length as object_info.size, reproducing the contract ReadTransform:: Compressed establishes. A hit whose body length disagrees is refused and falls through to the erasure read. This also closes a gate the deny-list only covered by accident: a restore read forces ReadPlan down the Plain branch, so a compressed object yields STORED bytes under its compressed size. Refusing compressed objects hid that; admitting them exposes it, so restore reads are refused explicitly. Being fail-closed, a newly added ReadPlan branch bypasses the cache by default rather than silently serving the wrong representation — the structural defect behind both backlog#1108 and backlog#1109. Refs: backlog#1108, backlog#1109 Co-Authored-By: heihutu <heihutu@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
use crate::app::object_data_cache::ObjectDataCacheAdapter;
|
||||
use crate::app::storage_api::object_usecase::StorageObjectInfo;
|
||||
use crate::storage::storage_api::ecstore_bucket::lifecycle::bucket_lifecycle_ops::LifecycleOps as _;
|
||||
use rustfs_object_data_cache::{ObjectDataCacheBodyVariant, ObjectDataCacheGetPlan, ObjectDataCacheGetRequest};
|
||||
|
||||
/// App-layer GET request snapshot used for cache planning.
|
||||
@@ -53,7 +54,14 @@ pub(crate) fn build_get_object_body_cache_plan(
|
||||
|| request.info.delete_marker
|
||||
|| request.info.version_only
|
||||
|| request.info.metadata_only
|
||||
|| request.response_content_length < 0
|
||||
// Remote (transitioned) objects are served from the warm tier; the
|
||||
// ecstore hook already refuses them (hook.rs is_remote()), so the
|
||||
// usecase-layer planner must exclude them too for a uniform contract.
|
||||
|| request.info.is_remote()
|
||||
// Zero-length bodies save no I/O — ecstore returns an empty body before
|
||||
// the hook probe — so admitting them only creates useless entries and
|
||||
// inflates hit metrics. Mirrors should_buffer_get_object_in_memory_with_threshold.
|
||||
|| request.response_content_length <= 0
|
||||
{
|
||||
return GetObjectBodyCachePlan::Skip;
|
||||
}
|
||||
@@ -178,6 +186,62 @@ mod tests {
|
||||
assert!(matches!(plan, GetObjectBodyCachePlan::Skip));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_skips_remote_transitioned_objects() {
|
||||
// backlog#1138: transitioned (remote-tier) objects are served from the
|
||||
// warm backend; the ecstore hook already refuses them, so the
|
||||
// usecase-layer planner must exclude them too for a uniform contract.
|
||||
let adapter = enabled_adapter();
|
||||
let mut info = crate::storage::storage_api::StorageObjectInfo {
|
||||
etag: Some("etag".to_string()),
|
||||
size: 4,
|
||||
..Default::default()
|
||||
};
|
||||
info.transitioned_object.status = "complete".to_string();
|
||||
|
||||
let plan = build_get_object_body_cache_plan(
|
||||
&adapter,
|
||||
GetObjectBodyCacheRequest {
|
||||
bucket: "bucket",
|
||||
key: "object",
|
||||
info: &info,
|
||||
response_content_length: 4,
|
||||
has_range: false,
|
||||
part_number: None,
|
||||
encryption_applied: false,
|
||||
},
|
||||
);
|
||||
|
||||
assert!(matches!(plan, GetObjectBodyCachePlan::Skip));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_skips_zero_length_objects() {
|
||||
// backlog#1142: ecstore returns an empty body before the hook probe, so
|
||||
// a zero-length GET saves no I/O; admitting it only inflates hit metrics.
|
||||
let adapter = enabled_adapter();
|
||||
let info = crate::storage::storage_api::StorageObjectInfo {
|
||||
etag: Some("etag".to_string()),
|
||||
size: 0,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let plan = build_get_object_body_cache_plan(
|
||||
&adapter,
|
||||
GetObjectBodyCacheRequest {
|
||||
bucket: "bucket",
|
||||
key: "object",
|
||||
info: &info,
|
||||
response_content_length: 0,
|
||||
has_range: false,
|
||||
part_number: None,
|
||||
encryption_applied: false,
|
||||
},
|
||||
);
|
||||
|
||||
assert!(matches!(plan, GetObjectBodyCachePlan::Skip));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_is_cacheable_for_plain_full_object() {
|
||||
let adapter = enabled_adapter();
|
||||
|
||||
Reference in New Issue
Block a user