Skip to main content

risingwave_storage/hummock/event_handler/refiller/
foyer.rs

1// Copyright 2026 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Executes Foyer refill tasks from snapshots captured before spawning.
16
17use std::collections::HashSet;
18use std::hash::Hash;
19use std::ops::{Bound, Range};
20use std::time::Instant;
21
22use ::foyer::RangeBoundsExt;
23use futures::future::{join_all, try_join_all};
24use itertools::Itertools;
25use risingwave_common::bitmap::Bitmap;
26use risingwave_common::hash::VirtualNode;
27use risingwave_common::util::iter_util::ZipEqFast;
28use risingwave_hummock_sdk::compaction_group::hummock_version_ext::SstDeltaInfo;
29use risingwave_hummock_sdk::key::{FullKey, vnode_range};
30use risingwave_hummock_sdk::{HummockSstableObjectId, KeyComparator};
31use thiserror_ext::AsReport;
32use tokio::task::JoinHandle;
33
34use super::{CacheRefillContext, TableCacheRefillContext};
35use crate::hummock::{
36    Block, HummockError, HummockResult, RecentFilterTrait, Sstable, SstableBlockIndex, TableHolder,
37};
38use crate::monitor::{GLOBAL_CACHE_REFILL_METRICS, StoreLocalStatistic};
39
40pub(super) fn spawn_refill_task(
41    deltas: Vec<SstDeltaInfo>,
42    context: CacheRefillContext,
43) -> JoinHandle<()> {
44    let task = CacheRefillTask { deltas, context };
45    tokio::spawn(task.run())
46}
47
48fn vnode_range_overlaps_bitmap(vnode_range: (usize, usize), bitmap: &Bitmap) -> bool {
49    assert!(vnode_range.0 <= vnode_range.1);
50    let start = vnode_range.0.min(bitmap.len());
51    let end = vnode_range.1.min(bitmap.len());
52    if start == end || !bitmap.any() {
53        return false;
54    }
55    if bitmap.all() {
56        return true;
57    }
58    (start..end).any(|vnode| bitmap.is_set(vnode))
59}
60
61impl TableCacheRefillContext {
62    fn allows_normal_data_refill_block(&self, sstable: &Sstable, block_index: usize) -> bool {
63        if self.policy.is_unscoped_enabled() {
64            return true;
65        }
66
67        (self.policy.is_streaming_scoped()
68            && self.check_table_refill_streaming_vnodes(sstable, block_index))
69            || (self.policy.is_serving_scoped()
70                && self.check_table_refill_serving_vnodes(sstable, block_index))
71    }
72
73    fn allows_insert_only_data_refill_block(&self, sstable: &Sstable, block_index: usize) -> bool {
74        // Insert-only deltas have no delete-side evidence for recent/inheritance filters.
75        // Only serving-owned blocks need refill, because streaming writers already populated
76        // their local cache.
77        (self.policy.is_unscoped_enabled() || self.policy.is_serving_scoped())
78            && self.check_table_refill_serving_vnodes(sstable, block_index)
79    }
80
81    fn check_table_refill_streaming_vnodes(&self, sstable: &Sstable, block_index: usize) -> bool {
82        self.streaming_vnode_bitmap.as_ref().is_some_and(|bitmap| {
83            let vnode_range = block_vnode_range(sstable, block_index);
84            vnode_range_overlaps_bitmap(vnode_range, bitmap)
85        })
86    }
87
88    fn check_table_refill_serving_vnodes(&self, sstable: &Sstable, block_index: usize) -> bool {
89        self.serving_vnode_bitmap.as_ref().is_some_and(|bitmap| {
90            let vnode_range = block_vnode_range(sstable, block_index);
91            vnode_range_overlaps_bitmap(vnode_range, bitmap)
92        })
93    }
94}
95
96fn block_vnode_range(sstable: &Sstable, block_index: usize) -> (usize, usize) {
97    let block_meta = &sstable.meta.block_metas[block_index];
98    let block_smallest_key = FullKey::decode(&block_meta.smallest_key);
99    let table_key_end = match sstable.meta.block_metas.get(block_index + 1) {
100        // A table switch always starts a new block. The next table's smallest key has an
101        // unrelated vnode, so use the current table's terminal range instead.
102        Some(next_block_meta) if next_block_meta.table_id() != block_meta.table_id() => {
103            Bound::Unbounded
104        }
105        // Full-key versions of the same table key may span adjacent blocks. After projecting
106        // away the epoch, the boundary vnode therefore remains part of the current block.
107        Some(next_block_meta) => Bound::Included(
108            FullKey::decode(&next_block_meta.smallest_key)
109                .user_key
110                .table_key,
111        ),
112        // `SstableMeta::largest_key` is the actual last key, unlike the next block's smallest
113        // key above. Keep it inclusive, especially for singleton tables whose key contains only
114        // the vnode prefix.
115        None => Bound::Included(
116            FullKey::decode(&sstable.meta.largest_key)
117                .user_key
118                .table_key,
119        ),
120    };
121
122    let table_key_range = (
123        Bound::Included(block_smallest_key.user_key.table_key),
124        table_key_end,
125    );
126    // Block-meta separators may shorten the table key below the vnode prefix. They are valid
127    // full-key search boundaries but cannot identify a vnode, so fail open instead of panicking
128    // or dropping a block that may belong to this worker.
129    if match &table_key_range.0 {
130        Bound::Included(key) | Bound::Excluded(key) => key.as_ref().len() < VirtualNode::SIZE,
131        Bound::Unbounded => false,
132    } || match &table_key_range.1 {
133        Bound::Included(key) | Bound::Excluded(key) => key.as_ref().len() < VirtualNode::SIZE,
134        Bound::Unbounded => false,
135    } {
136        return (0, VirtualNode::MAX_REPRESENTABLE.to_index() + 1);
137    }
138    vnode_range(&table_key_range)
139}
140
141struct DataCacheRefillTaskGenerator<'a> {
142    context: &'a CacheRefillContext,
143    delta: &'a SstDeltaInfo,
144    ssts: &'a [TableHolder],
145}
146
147impl DataCacheRefillTaskGenerator<'_> {
148    fn generate_unfiltered_tasks(&self) -> Vec<DataCacheRefillTask> {
149        let mut tasks = Vec::new();
150
151        // Skip data cache refill if data disk cache is not enabled.
152        if !self.context.sstable_store.block_cache().is_hybrid() {
153            return tasks;
154        }
155
156        if self.delta.insert_sst_infos.is_empty() {
157            return tasks;
158        }
159
160        let has_parent_ssts = !self.delta.delete_sst_object_ids.is_empty();
161        // CN-written SSTs are appended to L0 without replacing parent SSTs. Other inserted SSTs
162        // need delete-side evidence for recent and inheritance filtering.
163        debug_assert!(has_parent_ssts || self.delta.insert_sst_level == 0);
164
165        // Return if the target level is not in the refill levels
166        if !self
167            .context
168            .config
169            .data_refill_levels
170            .contains(&self.delta.insert_sst_level)
171        {
172            return tasks;
173        }
174
175        // Cache refill units must not cross a table boundary. A logical SST projection still
176        // decides whether to admit each single-table unit.
177        let unit = self.context.config.unit;
178        assert!(unit > 0, "cache refill unit must be positive");
179        let table_cache_refill_context_map = &self.context.table_cache_refill_context_map;
180        for (sst_info, sst) in self.delta.insert_sst_infos.iter().zip_eq_fast(self.ssts) {
181            debug_assert_eq!(sst_info.object_id, sst.id);
182            debug_assert!(sst_info.table_ids.is_sorted());
183            let mut blk_start = 0;
184            while blk_start < sst.block_count() {
185                // SstableBuilder ends a block before the table ID changes, so block metadata
186                // defines the exact physical boundary. `table_ids` below only admits logical
187                // projections and must not make a unit span another table.
188                let table_id = sst.meta.block_metas[blk_start].table_id();
189                let mut blk_end = std::cmp::min(sst.block_count(), blk_start + unit);
190                if let Some(table_boundary) = (blk_start + 1..blk_end)
191                    .find(|&block_index| sst.meta.block_metas[block_index].table_id() != table_id)
192                {
193                    blk_end = table_boundary;
194                }
195
196                let should_refill = sst_info.table_ids.binary_search(&table_id).is_ok()
197                    && (blk_start..blk_end).any(|block_index| {
198                        table_cache_refill_context_map
199                            .get(&table_id)
200                            .is_some_and(|context| {
201                                if has_parent_ssts {
202                                    context.allows_normal_data_refill_block(sst, block_index)
203                                } else {
204                                    context.allows_insert_only_data_refill_block(sst, block_index)
205                                }
206                            })
207                    });
208                if should_refill {
209                    tasks.push(DataCacheRefillTask {
210                        sst: sst.clone(),
211                        blks: blk_start..blk_end,
212                    });
213                }
214                blk_start = blk_end;
215            }
216        }
217
218        if tasks.is_empty() {
219            return tasks;
220        }
221
222        // Policy/vnode ownership defines refill responsibility first, but it does not bypass
223        // recent admission for normal insert+delete refill.
224        if has_parent_ssts
225            && !self.context.config.skip_recent_filter
226            && !self.filter_by_recent_filter()
227        {
228            GLOBAL_CACHE_REFILL_METRICS
229                .data_refill_filtered_total
230                .inc_by(self.delta.delete_sst_object_ids.len() as u64);
231            return vec![];
232        }
233
234        tasks
235    }
236
237    async fn filter_by_inheritance_if_needed(
238        &self,
239        tasks: Vec<DataCacheRefillTask>,
240    ) -> Vec<DataCacheRefillTask> {
241        // Skipping the recent filter selects full refill. Inheritance filtering only applies to
242        // non-L0 normal refill after real recent-filter admission.
243        let should_filter_by_inheritance = !tasks.is_empty()
244            && !self.delta.delete_sst_object_ids.is_empty()
245            && self.delta.insert_sst_level != 0
246            && !self.context.config.skip_recent_filter
247            && !self.context.config.skip_inheritance_filter;
248        if should_filter_by_inheritance {
249            self.filter_by_inheritance_filter(tasks).await
250        } else {
251            tasks
252        }
253    }
254
255    // Return if recent filter is required and no deleted sst ids are in the recent filter.
256    fn filter_by_recent_filter(&self) -> bool {
257        let recent_filter = self.context.sstable_store.recent_filter();
258        let targets = self
259            .delta
260            .delete_sst_object_ids
261            .iter()
262            .map(|id| (*id, usize::MAX))
263            .collect_vec();
264        recent_filter.contains_any(targets.iter())
265    }
266
267    async fn filter_by_inheritance_filter(
268        &self,
269        originals: Vec<DataCacheRefillTask>,
270    ) -> Vec<DataCacheRefillTask> {
271        // Get parent sst metas from cache.
272        let sstable_store = self.context.sstable_store.clone();
273        let futures = self.delta.delete_sst_object_ids.iter().map(|sst_obj_id| {
274            let store = &sstable_store;
275            async move {
276                let res = store.sstable_cached(*sst_obj_id).await;
277                match res {
278                    Ok(Some(_)) => GLOBAL_CACHE_REFILL_METRICS
279                        .data_refill_parent_meta_lookup_hit_total
280                        .inc(),
281                    Ok(None) => GLOBAL_CACHE_REFILL_METRICS
282                        .data_refill_parent_meta_lookup_miss_total
283                        .inc(),
284                    _ => {}
285                }
286                res
287            }
288        });
289        let parent_ssts = match try_join_all(futures).await {
290            Ok(parent_ssts) => parent_ssts.into_iter().flatten(),
291            Err(e) => {
292                tracing::error!(error = %e.as_report(), "get old meta from cache error");
293                return vec![];
294            }
295        };
296
297        // assert units in asc order
298        if cfg!(debug_assertions) {
299            originals.iter().tuple_windows().for_each(|(a, b)| {
300                debug_assert_ne!(
301                    KeyComparator::compare_encoded_full_key(a.largest_key(), b.smallest_key()),
302                    std::cmp::Ordering::Greater
303                )
304            });
305        }
306
307        let mut filtered: HashSet<SstableUnit> = HashSet::default();
308        let recent_filter = self.context.sstable_store.recent_filter();
309        for psst in parent_ssts {
310            for pblk in 0..psst.block_count() {
311                let pleft = &psst.meta.block_metas[pblk].smallest_key;
312                let pright = if pblk + 1 == psst.block_count() {
313                    // `largest_key` can be included or excluded, both are treated as included here
314                    &psst.meta.largest_key
315                } else {
316                    &psst.meta.block_metas[pblk + 1].smallest_key
317                };
318
319                // partition point: unit.right < pblk.left
320                let uleft = originals.partition_point(|task| {
321                    KeyComparator::compare_encoded_full_key(task.largest_key(), pleft)
322                        == std::cmp::Ordering::Less
323                });
324                // partition point: unit.left <= pblk.right
325                let uright = originals.partition_point(|task| {
326                    KeyComparator::compare_encoded_full_key(task.smallest_key(), pright)
327                        != std::cmp::Ordering::Greater
328                });
329
330                // overlapping: uleft..uright
331                for task in originals.iter().take(uright).skip(uleft) {
332                    let unit = task.unit();
333                    if filtered.contains(&unit) {
334                        continue;
335                    }
336                    if recent_filter.contains(&(psst.id, pblk)) {
337                        filtered.insert(unit);
338                    }
339                }
340            }
341        }
342
343        let hit = filtered.len();
344        let miss = originals.len() - hit;
345        GLOBAL_CACHE_REFILL_METRICS
346            .data_refill_unit_inheritance_hit_total
347            .inc_by(hit as u64);
348        GLOBAL_CACHE_REFILL_METRICS
349            .data_refill_unit_inheritance_miss_total
350            .inc_by(miss as u64);
351
352        originals
353            .into_iter()
354            .filter(|task| filtered.contains(&task.unit()))
355            .collect()
356    }
357}
358
359#[derive(Debug)]
360struct DataCacheRefillTask {
361    sst: TableHolder,
362    blks: Range<usize>,
363}
364
365impl DataCacheRefillTask {
366    fn unit(&self) -> SstableUnit {
367        SstableUnit {
368            sst_obj_id: self.sst.id,
369            blks: self.blks.clone(),
370        }
371    }
372
373    fn smallest_key(&self) -> &[u8] {
374        &self.sst.meta.block_metas[self.blks.start].smallest_key
375    }
376
377    fn largest_key(&self) -> &[u8] {
378        if self.blks.end == self.sst.block_count() {
379            &self.sst.meta.largest_key
380        } else {
381            &self.sst.meta.block_metas[self.blks.end].smallest_key
382        }
383    }
384}
385
386struct CacheRefillTask {
387    deltas: Vec<SstDeltaInfo>,
388    context: CacheRefillContext,
389}
390
391impl CacheRefillTask {
392    async fn run(self) {
393        let tasks = self
394            .deltas
395            .iter()
396            .map(|delta| {
397                let context = self.context.clone();
398                async move {
399                    let holders = match Self::meta_cache_refill(&context, delta).await {
400                        Ok(holders) => holders,
401                        Err(e) => {
402                            tracing::warn!(error = %e.as_report(), "meta cache refill error");
403                            return;
404                        }
405                    };
406                    let generator = DataCacheRefillTaskGenerator {
407                        context: &context,
408                        delta,
409                        ssts: &holders,
410                    };
411                    let tasks = generator.generate_unfiltered_tasks();
412
413                    // Main counts after recent admission but before inheritance.
414                    let unfiltered_block_count =
415                        tasks.iter().map(|task| task.blks.len() as u64).sum();
416                    GLOBAL_CACHE_REFILL_METRICS
417                        .data_refill_block_unfiltered_total
418                        .inc_by(unfiltered_block_count);
419
420                    let tasks = generator.filter_by_inheritance_if_needed(tasks).await;
421                    Self::data_cache_refill(&context, tasks).await;
422                }
423            })
424            .collect_vec();
425        let future = join_all(tasks);
426
427        let _ = tokio::time::timeout(self.context.config.timeout, future).await;
428    }
429
430    async fn meta_cache_refill(
431        context: &CacheRefillContext,
432        delta: &SstDeltaInfo,
433    ) -> HummockResult<Vec<TableHolder>> {
434        let tasks = delta
435            .insert_sst_infos
436            .iter()
437            .map(|info| async {
438                let mut stats = StoreLocalStatistic::default();
439                GLOBAL_CACHE_REFILL_METRICS.meta_refill_attempts_total.inc();
440
441                let permit = if let Some(c) = &context.meta_refill_concurrency {
442                    Some(c.acquire().await.unwrap())
443                } else {
444                    None
445                };
446
447                let now = Instant::now();
448                let res = context.sstable_store.sstable(info, &mut stats).await;
449                stats.discard();
450                if res.is_ok() {
451                    GLOBAL_CACHE_REFILL_METRICS
452                        .meta_refill_success_duration
453                        .observe(now.elapsed().as_secs_f64());
454                }
455                drop(permit);
456
457                res
458            })
459            .collect_vec();
460        let holders = try_join_all(tasks).await?;
461        Ok(holders)
462    }
463
464    async fn data_cache_refill(context: &CacheRefillContext, tasks: Vec<DataCacheRefillTask>) {
465        let mut futures = Vec::with_capacity(tasks.len());
466        for task in tasks {
467            // update filter for sst id only
468            context
469                .sstable_store
470                .recent_filter()
471                .insert((task.sst.id, usize::MAX));
472
473            let blocks = task.blks.len();
474            let mut contexts = Vec::with_capacity(blocks);
475            let mut admits = 0;
476
477            let (range_first, _) = task.sst.calculate_block_info(task.blks.start);
478            let (range_last, _) = task.sst.calculate_block_info(task.blks.end - 1);
479            let range = range_first.start..range_last.end;
480
481            let size = range.size().unwrap();
482
483            GLOBAL_CACHE_REFILL_METRICS
484                .data_refill_ideal_bytes
485                .inc_by(size as _);
486
487            for blk in task.blks {
488                let (range, uncompressed_capacity) = task.sst.calculate_block_info(blk);
489                let key = SstableBlockIndex {
490                    sst_id: task.sst.id,
491                    block_idx: blk as u64,
492                };
493
494                let mut writer = context.sstable_store.block_cache().storage_writer(key);
495
496                if writer.filter(size).is_admitted() {
497                    admits += 1;
498                }
499
500                contexts.push((writer, range, uncompressed_capacity))
501            }
502
503            if admits as f64 / contexts.len() as f64 >= context.config.threshold {
504                let sstable_store = context.sstable_store.clone();
505                let context = context.clone();
506                let future = async move {
507                    GLOBAL_CACHE_REFILL_METRICS.data_refill_attempts_total.inc();
508
509                    let permit = context.concurrency.acquire().await.unwrap();
510
511                    GLOBAL_CACHE_REFILL_METRICS.data_refill_started_total.inc();
512
513                    let now = Instant::now();
514
515                    let data = sstable_store
516                        .store()
517                        .read(&sstable_store.get_sst_data_path(task.sst.id), range.clone())
518                        .await?;
519                    let mut apply_disk_cache_futures = vec![];
520                    for (w, r, uc) in contexts {
521                        let offset = r.start - range.start;
522                        let len = r.end - r.start;
523                        let bytes = data.slice(offset..offset + len);
524                        let future = async move {
525                            let value = Box::new(Block::decode(bytes, uc)?);
526                            // The entry should always be `Some(..)`, use if here for compatible.
527                            if let Some(_entry) = w.force().insert(value) {
528                                GLOBAL_CACHE_REFILL_METRICS
529                                    .data_refill_success_bytes
530                                    .inc_by(len as u64);
531                                GLOBAL_CACHE_REFILL_METRICS
532                                    .data_refill_block_success_total
533                                    .inc();
534                            }
535                            Ok::<_, HummockError>(())
536                        };
537                        apply_disk_cache_futures.push(future);
538                    }
539                    try_join_all(apply_disk_cache_futures)
540                        .await
541                        .map_err(HummockError::file_cache)?;
542
543                    GLOBAL_CACHE_REFILL_METRICS
544                        .data_refill_success_duration
545                        .observe(now.elapsed().as_secs_f64());
546                    drop(permit);
547
548                    Ok::<_, HummockError>(())
549                };
550                futures.push(future);
551            }
552        }
553
554        let futures = futures.into_iter().map(|future| async move {
555            if let Err(e) = future.await {
556                tracing::error!(error = %e.as_report(), "data cache refill task error");
557            }
558        });
559
560        join_all(futures).await;
561    }
562}
563
564#[derive(Debug, Hash, PartialEq, Eq)]
565pub struct SstableUnit {
566    pub sst_obj_id: HummockSstableObjectId,
567    pub blks: Range<usize>,
568}
569
570impl Ord for SstableUnit {
571    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
572        match self.sst_obj_id.cmp(&other.sst_obj_id) {
573            std::cmp::Ordering::Equal => {}
574            ord => return ord,
575        }
576        match self.blks.start.cmp(&other.blks.start) {
577            std::cmp::Ordering::Equal => {}
578            ord => return ord,
579        }
580        self.blks.end.cmp(&other.blks.end)
581    }
582}
583
584impl PartialOrd for SstableUnit {
585    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
586        Some(self.cmp(other))
587    }
588}
589
590#[cfg(test)]
591mod tests;