@@ -2771,3 +2771,239 @@ fn tiered_cache_key_alignment_warmup_matches_query() {
27712771 assert ! ( block_on( tiered. get( & range_cache_key( "seg0/_0.parquet" , 7_500_000 , 9_000_000 ) ) ) . is_none( ) ) ;
27722772}
27732773
2774+
2775+
2776+ // ── try_get tests (FoyerCache) ──────────────────────────────────────────────
2777+
2778+ /// `try_get` HIT path: returns `Some(bytes)` AND bumps `hit_count` + `hit_bytes`,
2779+ /// matching the regular `get` HIT contract.
2780+ #[ test]
2781+ fn try_get_hit_updates_hit_stats ( ) {
2782+ let ( cache, _dir) = test_cache ( ) ;
2783+
2784+ let key = range_cache_key ( "seg0/_0.parquet" , 100 , 132 ) ;
2785+ let payload = Bytes :: from_static ( b"32_bytes_of_metadata____________" ) ;
2786+ assert_eq ! ( payload. len( ) , 32 ) ;
2787+ cache. put ( & key, payload. clone ( ) ) ;
2788+
2789+ let hits_before = cache. stats . hit_count . load ( Ordering :: Relaxed ) ;
2790+ let hit_bytes_before = cache. stats . hit_bytes . load ( Ordering :: Relaxed ) ;
2791+ let misses_before = cache. stats . miss_count . load ( Ordering :: Relaxed ) ;
2792+ let miss_bytes_before = cache. stats . miss_bytes . load ( Ordering :: Relaxed ) ;
2793+
2794+ let result = block_on ( cache. try_get ( & key) ) ;
2795+ assert_eq ! ( result. as_deref( ) , Some ( payload. as_ref( ) ) ) ;
2796+
2797+ assert_eq ! ( cache. stats. hit_count. load( Ordering :: Relaxed ) , hits_before + 1 , "hit_count must bump on hit" ) ;
2798+ assert_eq ! ( cache. stats. hit_bytes. load( Ordering :: Relaxed ) , hit_bytes_before + 32 , "hit_bytes must bump by entry size" ) ;
2799+ assert_eq ! ( cache. stats. miss_count. load( Ordering :: Relaxed ) , misses_before, "miss_count must NOT bump on hit" ) ;
2800+ assert_eq ! ( cache. stats. miss_bytes. load( Ordering :: Relaxed ) , miss_bytes_before, "miss_bytes must NOT bump on hit" ) ;
2801+ }
2802+
2803+ /// `try_get` MISS path: returns `None` and leaves `miss_count` + `miss_bytes` UNCHANGED.
2804+ /// This is the whole point of the method — a miss on the metadata-tier shortcut probe
2805+ /// is not a meaningful cache-failure signal and must not pollute miss stats.
2806+ #[ test]
2807+ fn try_get_miss_does_not_update_miss_stats ( ) {
2808+ let ( cache, _dir) = test_cache ( ) ;
2809+
2810+ let absent_key = range_cache_key ( "seg0/_0.parquet" , 0 , 1024 ) ;
2811+
2812+ let hits_before = cache. stats . hit_count . load ( Ordering :: Relaxed ) ;
2813+ let hit_bytes_before = cache. stats . hit_bytes . load ( Ordering :: Relaxed ) ;
2814+ let misses_before = cache. stats . miss_count . load ( Ordering :: Relaxed ) ;
2815+ let miss_bytes_before = cache. stats . miss_bytes . load ( Ordering :: Relaxed ) ;
2816+
2817+ let result = block_on ( cache. try_get ( & absent_key) ) ;
2818+ assert ! ( result. is_none( ) , "try_get on absent key returns None" ) ;
2819+
2820+ assert_eq ! ( cache. stats. hit_count. load( Ordering :: Relaxed ) , hits_before, "hit_count unchanged on miss" ) ;
2821+ assert_eq ! ( cache. stats. hit_bytes. load( Ordering :: Relaxed ) , hit_bytes_before, "hit_bytes unchanged on miss" ) ;
2822+ assert_eq ! ( cache. stats. miss_count. load( Ordering :: Relaxed ) , misses_before, "miss_count must NOT bump" ) ;
2823+ assert_eq ! ( cache. stats. miss_bytes. load( Ordering :: Relaxed ) , miss_bytes_before, "miss_bytes must NOT bump" ) ;
2824+ }
2825+
2826+ /// Direct comparison: `get` vs `try_get` on the same absent key. `get` bumps miss
2827+ /// stats; `try_get` leaves them alone. Makes the contract difference obvious.
2828+ #[ test]
2829+ fn try_get_vs_get_on_miss_diverge_on_miss_stats ( ) {
2830+ let ( cache, _dir) = test_cache ( ) ;
2831+
2832+ let key_for_get = range_cache_key ( "seg0/_0.parquet" , 0 , 1024 ) ;
2833+ let key_for_try_get = range_cache_key ( "seg0/_0.parquet" , 2048 , 3072 ) ;
2834+
2835+ let misses_before = cache. stats . miss_count . load ( Ordering :: Relaxed ) ;
2836+
2837+ // `get` on absent key bumps miss_count.
2838+ assert ! ( block_on( cache. get( & key_for_get) ) . is_none( ) ) ;
2839+ let after_get = cache. stats . miss_count . load ( Ordering :: Relaxed ) ;
2840+ assert_eq ! ( after_get, misses_before + 1 , "get on miss bumps miss_count" ) ;
2841+
2842+ // `try_get` on absent key leaves miss_count unchanged.
2843+ assert ! ( block_on( cache. try_get( & key_for_try_get) ) . is_none( ) ) ;
2844+ let after_try_get = cache. stats . miss_count . load ( Ordering :: Relaxed ) ;
2845+ assert_eq ! ( after_try_get, after_get, "try_get on miss must NOT bump miss_count" ) ;
2846+ }
2847+
2848+ // ── TieredBlockCache miss-stat isolation tests ──────────────────────────────
2849+
2850+ /// CORE FIX: a normal data-tier read (column chunk) hits the data cache and must
2851+ /// NOT pollute the metadata-tier miss stats. Before the `try_get` switch, every
2852+ /// data read bumped `metadata_cache.miss_count` because the tiered get probed
2853+ /// metadata first via `get` (which counts misses).
2854+ #[ test]
2855+ fn tiered_get_data_hit_does_not_bump_metadata_miss ( ) {
2856+ let ( tiered, _data_dir, _meta_dir) = tiered_test_cache ( ) ;
2857+
2858+ let col_chunk_key = range_cache_key ( "seg0/_0.parquet" , 0 , 4096 ) ;
2859+ tiered. put ( & col_chunk_key, Bytes :: from ( vec ! [ 0xAB ; 4096 ] ) ) ;
2860+
2861+ let meta_misses_before = tiered. metadata_cache ( ) . stats . miss_count . load ( Ordering :: Relaxed ) ;
2862+ let meta_miss_bytes_before = tiered. metadata_cache ( ) . stats . miss_bytes . load ( Ordering :: Relaxed ) ;
2863+ let data_hits_before = tiered. data_cache ( ) . stats . hit_count . load ( Ordering :: Relaxed ) ;
2864+
2865+ let result = block_on ( tiered. get ( & col_chunk_key) ) ;
2866+ assert_eq ! ( result. map( |b| b. len( ) ) , Some ( 4096 ) ) ;
2867+
2868+ // Data tier hit count goes up.
2869+ assert_eq ! (
2870+ tiered. data_cache( ) . stats. hit_count. load( Ordering :: Relaxed ) ,
2871+ data_hits_before + 1 ,
2872+ "data tier should record the hit"
2873+ ) ;
2874+ // Metadata tier miss stats stay UNCHANGED — that's the fix.
2875+ assert_eq ! (
2876+ tiered. metadata_cache( ) . stats. miss_count. load( Ordering :: Relaxed ) ,
2877+ meta_misses_before,
2878+ "metadata tier miss_count must NOT bump on data-tier hit"
2879+ ) ;
2880+ assert_eq ! (
2881+ tiered. metadata_cache( ) . stats. miss_bytes. load( Ordering :: Relaxed ) ,
2882+ meta_miss_bytes_before,
2883+ "metadata tier miss_bytes must NOT bump on data-tier hit"
2884+ ) ;
2885+ }
2886+
2887+ /// When BOTH tiers miss (cold key), only the data tier records the miss. The
2888+ /// metadata tier's miss stats remain at zero — every column-chunk-shaped key
2889+ /// would otherwise inflate them with non-meaningful misses.
2890+ #[ test]
2891+ fn tiered_get_full_miss_only_bumps_data_miss ( ) {
2892+ let ( tiered, _data_dir, _meta_dir) = tiered_test_cache ( ) ;
2893+
2894+ let cold_key = range_cache_key ( "seg0/_0.parquet" , 5_000_000 , 6_000_000 ) ;
2895+
2896+ let meta_misses_before = tiered. metadata_cache ( ) . stats . miss_count . load ( Ordering :: Relaxed ) ;
2897+ let data_misses_before = tiered. data_cache ( ) . stats . miss_count . load ( Ordering :: Relaxed ) ;
2898+
2899+ let result = block_on ( tiered. get ( & cold_key) ) ;
2900+ assert ! ( result. is_none( ) , "cold key must miss both tiers" ) ;
2901+
2902+ // Data tier records the real miss.
2903+ assert_eq ! (
2904+ tiered. data_cache( ) . stats. miss_count. load( Ordering :: Relaxed ) ,
2905+ data_misses_before + 1 ,
2906+ "data tier should record the miss"
2907+ ) ;
2908+ // Metadata tier miss stats stay at zero.
2909+ assert_eq ! (
2910+ tiered. metadata_cache( ) . stats. miss_count. load( Ordering :: Relaxed ) ,
2911+ meta_misses_before,
2912+ "metadata tier miss_count must NOT bump on full miss"
2913+ ) ;
2914+ }
2915+
2916+ /// Sanity check: a metadata HIT (warmup-promoted entry) still bumps
2917+ /// `metadata_cache.hit_count` and `hit_bytes`. The fix must only suppress the
2918+ /// MISS path; the HIT path is unaffected.
2919+ #[ test]
2920+ fn tiered_get_metadata_hit_still_bumps_metadata_hit_stats ( ) {
2921+ let ( tiered, _data_dir, _meta_dir) = tiered_test_cache ( ) ;
2922+
2923+ let footer_key = range_cache_key ( "seg0/_0.parquet" , 9_000_000 , 10_000_000 ) ;
2924+ let payload = Bytes :: from ( vec ! [ 0xCD ; 1_000_000 ] ) ;
2925+ tiered. put_metadata ( & footer_key, payload. clone ( ) ) ;
2926+
2927+ let meta_hits_before = tiered. metadata_cache ( ) . stats . hit_count . load ( Ordering :: Relaxed ) ;
2928+ let meta_hit_bytes_before = tiered. metadata_cache ( ) . stats . hit_bytes . load ( Ordering :: Relaxed ) ;
2929+ let meta_misses_before = tiered. metadata_cache ( ) . stats . miss_count . load ( Ordering :: Relaxed ) ;
2930+ let data_hits_before = tiered. data_cache ( ) . stats . hit_count . load ( Ordering :: Relaxed ) ;
2931+
2932+ let result = block_on ( tiered. get ( & footer_key) ) ;
2933+ assert_eq ! ( result. map( |b| b. len( ) ) , Some ( 1_000_000 ) ) ;
2934+
2935+ // Metadata tier hit stats bump.
2936+ assert_eq ! (
2937+ tiered. metadata_cache( ) . stats. hit_count. load( Ordering :: Relaxed ) ,
2938+ meta_hits_before + 1 ,
2939+ "metadata hit_count must bump on metadata HIT"
2940+ ) ;
2941+ assert_eq ! (
2942+ tiered. metadata_cache( ) . stats. hit_bytes. load( Ordering :: Relaxed ) ,
2943+ meta_hit_bytes_before + 1_000_000 ,
2944+ "metadata hit_bytes must bump by entry size on HIT"
2945+ ) ;
2946+ // Metadata miss + data tier counters untouched.
2947+ assert_eq ! (
2948+ tiered. metadata_cache( ) . stats. miss_count. load( Ordering :: Relaxed ) ,
2949+ meta_misses_before,
2950+ "metadata miss_count must stay unchanged"
2951+ ) ;
2952+ assert_eq ! (
2953+ tiered. data_cache( ) . stats. hit_count. load( Ordering :: Relaxed ) ,
2954+ data_hits_before,
2955+ "data tier untouched when metadata hits"
2956+ ) ;
2957+ }
2958+
2959+ /// Realistic mixed workload: warmup populates metadata for 1 footer, query reads
2960+ /// 1 metadata range + 5 column-chunk ranges (4 data-tier hits + 1 cold miss).
2961+ /// After the workload:
2962+ /// metadata: hits=1, misses=0 (the fix — was 5 with the old `get`)
2963+ /// data: hits=4, misses=1
2964+ #[ test]
2965+ fn tiered_get_mixed_workload_metadata_misses_stay_zero ( ) {
2966+ let ( tiered, _data_dir, _meta_dir) = tiered_test_cache ( ) ;
2967+
2968+ let path = "seg0/_0.parquet" ;
2969+
2970+ // ── Warmup: 1 footer goes to metadata tier ───────────────────────────
2971+ let footer_key = range_cache_key ( path, 9_000_000 , 10_000_000 ) ;
2972+ tiered. put_metadata ( & footer_key, Bytes :: from ( vec ! [ 0xCD ; 1_000_000 ] ) ) ;
2973+
2974+ // ── Pre-populate data tier: 4 column chunks ─────────────────────────
2975+ let chunk_keys: Vec < _ > = ( 0 ..4 )
2976+ . map ( |i| range_cache_key ( path, i * 100_000 , ( i + 1 ) * 100_000 ) )
2977+ . collect ( ) ;
2978+ for k in & chunk_keys {
2979+ tiered. put ( k, Bytes :: from ( vec ! [ 0x01 ; 100_000 ] ) ) ;
2980+ }
2981+
2982+ let cold_chunk_key = range_cache_key ( path, 8_000_000 , 8_100_000 ) ;
2983+
2984+ // ── Query: 1 metadata read + 4 data hits + 1 data miss ──────────────
2985+ assert ! ( block_on( tiered. get( & footer_key) ) . is_some( ) ,
2986+ "footer should hit metadata tier" ) ;
2987+ for k in & chunk_keys {
2988+ assert ! ( block_on( tiered. get( k) ) . is_some( ) ,
2989+ "chunk should hit data tier" ) ;
2990+ }
2991+ assert ! ( block_on( tiered. get( & cold_chunk_key) ) . is_none( ) ,
2992+ "cold chunk should miss both tiers" ) ;
2993+
2994+ // ── Assert clean metadata-tier stats ─────────────────────────────────
2995+ let m = tiered. metadata_cache ( ) ;
2996+ assert_eq ! ( m. stats. hit_count. load( Ordering :: Relaxed ) , 1 ,
2997+ "metadata tier should record exactly 1 hit" ) ;
2998+ assert_eq ! ( m. stats. miss_count. load( Ordering :: Relaxed ) , 0 ,
2999+ "metadata tier miss_count must be 0 — no pollution from data reads" ) ;
3000+ assert_eq ! ( m. stats. miss_bytes. load( Ordering :: Relaxed ) , 0 ,
3001+ "metadata tier miss_bytes must be 0" ) ;
3002+
3003+ // ── Data-tier stats look exactly like a normal cache ────────────────
3004+ let d = tiered. data_cache ( ) ;
3005+ assert_eq ! ( d. stats. hit_count. load( Ordering :: Relaxed ) , 4 ,
3006+ "data tier should record 4 hits" ) ;
3007+ assert_eq ! ( d. stats. miss_count. load( Ordering :: Relaxed ) , 1 ,
3008+ "data tier should record 1 real miss (the cold chunk)" ) ;
3009+ }
0 commit comments