sunchao commented on code in PR #6098:
URL: https://github.com/apache/datafusion-comet/pull/6098#discussion_r4075215243


##########
native/shuffle/src/ipc.rs:
##########
@@ -120,6 +123,29 @@ fn cache_schema(
     schema_message: &[u8],
     schema: SchemaRef,
 ) {
+    // Admission only affects reuse. Large valid schemas still decode, without 
evicting useful
+    // entries or retaining their serialized and parsed copies for the 
lifetime of the thread.
+    if schema_message.len() > SCHEMA_CACHE_ENTRY_RETAIN_LIMIT {
+        return;
+    }
+    let mut retained_size = schema_message
+        .len()
+        .saturating_add(std::mem::size_of_val(schema.as_ref()))
+        .saturating_add(schema.fields().size())
+        .saturating_add(
+            schema
+                .metadata()
+                .capacity()
+                .saturating_mul(std::mem::size_of::<(String, String)>()),
+        );
+    for (key, value) in schema.metadata() {
+        retained_size = retained_size

Review Comment:
   I kept the two calls for consistency with the surrounding size accounting. 
Combining these particular additions is also safe: each valid `String` capacity 
is at most `isize::MAX`, so the pair cannot overflow `usize`.



##########
native/shuffle/src/ipc.rs:
##########
@@ -46,11 +46,14 @@ const CONTINUATION_MARKER: [u8; 4] = [0xff; 4];
 /// from several shuffles, and a single entry would thrash.
 const SCHEMA_CACHE_CAPACITY: usize = 4;
 
+/// Maximum estimated serialized-plus-parsed size per cached schema, excluding 
allocator overhead.
+const SCHEMA_CACHE_ENTRY_RETAIN_LIMIT: usize = 1 << 20;

Review Comment:
   Addressed in feccb4df: the cache now has a shared 4 MiB budget, keeping the 
four-entry cap and evicting the least recently used entries until both limits 
are satisfied. This preserves the previous maximum estimated cache retention 
while allowing wider schemas to use unused space. I kept full 
serialized-plus-parsed accounting because the per-field allocations are real 
retained memory. Added 8,000-column cache/release coverage, byte-budget 
eviction and boundary tests, and an 8,000-column benchmark case; all 156 
shuffle tests pass.



##########
native/shuffle/src/ipc.rs:
##########
@@ -46,11 +46,14 @@ const CONTINUATION_MARKER: [u8; 4] = [0xff; 4];
 /// from several shuffles, and a single entry would thrash.
 const SCHEMA_CACHE_CAPACITY: usize = 4;
 
+/// Maximum estimated serialized-plus-parsed size per cached schema, excluding 
allocator overhead.
+const SCHEMA_CACHE_ENTRY_RETAIN_LIMIT: usize = 1 << 20;
+
 /// Metadata scratch larger than this is released after the block rather than 
kept for the thread.
-/// Real metadata is a few KiB even for wide schemas; only a corrupt length 
gets anywhere near.
 const SCRATCH_RETAIN_LIMIT: usize = 1 << 20;

Review Comment:
   Clarified that scratch has its own buffer-capacity retention limit, 
independent of the cache's estimated serialized-plus-parsed byte budget. The 
cache comment now explains the shared 4 MiB budget and gives an approximate 
8,000-column example; scratch remains at 1 MiB. These are retention limits, not 
peak decode-memory limits.



##########
native/shuffle/src/ipc.rs:
##########
@@ -695,6 +722,85 @@ mod tests {
         assert_eq!(schema_cache_stats(), stats(5, 6), "most recent stays");
     }
 
+    #[test]
+    fn promoting_a_schema_preserves_eviction_order() {
+        let blocks: Vec<_> = (1..=5)
+            .map(|columns| block_for(&n_column_batch(columns), b"NONE"))
+            .collect();
+        reset_schema_cache();
+        // A B C D A E D: promoting A must keep D newer than B and C, so E 
evicts B.
+        for index in [0, 1, 2, 3, 0, 4, 3] {
+            assert_eq!(
+                read_ipc_compressed(&blocks[index]).unwrap(),
+                n_column_batch(index + 1)
+            );
+        }
+        assert_eq!(schema_cache_stats(), stats(2, 5));
+        read_ipc_compressed(&blocks[1]).unwrap();
+        assert_eq!(schema_cache_stats(), stats(2, 6));
+    }
+
+    #[test]
+    #[cfg_attr(miri, ignore)] // Miri cannot call Zstd's C FFI.
+    fn oversized_schemas_decode_without_retention_or_eviction() {

Review Comment:
   Added the named schema case, readable codec, and validation mode to decode 
failures and per-decode assertions. Cache and scratch checks also identify the 
case and codec.



##########
native/shuffle/src/ipc.rs:
##########
@@ -120,6 +123,29 @@ fn cache_schema(
     schema_message: &[u8],
     schema: SchemaRef,
 ) {
+    // Admission only affects reuse. Large valid schemas still decode, without 
evicting useful
+    // entries or retaining their serialized and parsed copies for the 
lifetime of the thread.
+    if schema_message.len() > SCHEMA_CACHE_ENTRY_RETAIN_LIMIT {
+        return;
+    }
+    let mut retained_size = schema_message

Review Comment:
   Extracted `estimated_retained_size` and added direct coverage for spare 
string capacity in schema metadata and nested field metadata. The nested case 
also exercises recursive field sizing. Each cache entry stores its estimate so 
hits and eviction do not walk the schema again.



##########
native/shuffle/src/ipc.rs:
##########
@@ -695,6 +722,85 @@ mod tests {
         assert_eq!(schema_cache_stats(), stats(5, 6), "most recent stays");
     }
 
+    #[test]
+    fn promoting_a_schema_preserves_eviction_order() {
+        let blocks: Vec<_> = (1..=5)
+            .map(|columns| block_for(&n_column_batch(columns), b"NONE"))
+            .collect();
+        reset_schema_cache();
+        // A B C D A E D: promoting A must keep D newer than B and C, so E 
evicts B.
+        for index in [0, 1, 2, 3, 0, 4, 3] {
+            assert_eq!(
+                read_ipc_compressed(&blocks[index]).unwrap(),
+                n_column_batch(index + 1)
+            );
+        }
+        assert_eq!(schema_cache_stats(), stats(2, 5));
+        read_ipc_compressed(&blocks[1]).unwrap();
+        assert_eq!(schema_cache_stats(), stats(2, 6));
+    }
+
+    #[test]
+    #[cfg_attr(miri, ignore)] // Miri cannot call Zstd's C FFI.
+    fn oversized_schemas_decode_without_retention_or_eviction() {
+        let normal_blocks: Vec<_> = (1..=SCHEMA_CACHE_CAPACITY)
+            .map(|columns| block_for(&n_column_batch(columns), b"NONE"))
+            .collect();
+        let schemas = [
+            Schema::new(vec![Field::new(
+                "x".repeat(SCHEMA_CACHE_ENTRY_RETAIN_LIMIT + 1),
+                DataType::Int32,
+                false,
+            )]),
+            // Each wire message fits the limit, but its parsed copy pushes 
retention over it.
+            Schema::new(vec![Field::new(
+                "x".repeat(SCHEMA_CACHE_ENTRY_RETAIN_LIMIT / 2),
+                DataType::Int32,
+                false,
+            )]),
+            Schema::new(vec![Field::new("c", DataType::Int32, 
false)]).with_metadata(
+                HashMap::from([(
+                    "key".into(),
+                    "x".repeat(SCHEMA_CACHE_ENTRY_RETAIN_LIMIT / 2),
+                )]),
+            ),
+        ];
+        for (index, schema) in schemas.into_iter().enumerate() {
+            let batch = RecordBatch::try_new(
+                Arc::new(schema),
+                vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
+            )
+            .unwrap();
+            let ipc = ipc_bytes(&batch);
+            if index > 0 {

Review Comment:
   Replaced `index > 0` with named cases and an explicit `wire_fits` 
expectation. The wire-size assertion now covers all three cases, distinguishing 
an oversized serialized schema from schemas whose parsed copies push total 
retention over the budget.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to