risingwave_storage/hummock/event_handler/refiller/
foyer.rs1use 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 (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 Some(next_block_meta) if next_block_meta.table_id() != block_meta.table_id() => {
103 Bound::Unbounded
104 }
105 Some(next_block_meta) => Bound::Included(
108 FullKey::decode(&next_block_meta.smallest_key)
109 .user_key
110 .table_key,
111 ),
112 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 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 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 debug_assert!(has_parent_ssts || self.delta.insert_sst_level == 0);
164
165 if !self
167 .context
168 .config
169 .data_refill_levels
170 .contains(&self.delta.insert_sst_level)
171 {
172 return tasks;
173 }
174
175 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 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 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 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 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 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 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 &psst.meta.largest_key
315 } else {
316 &psst.meta.block_metas[pblk + 1].smallest_key
317 };
318
319 let uleft = originals.partition_point(|task| {
321 KeyComparator::compare_encoded_full_key(task.largest_key(), pleft)
322 == std::cmp::Ordering::Less
323 });
324 let uright = originals.partition_point(|task| {
326 KeyComparator::compare_encoded_full_key(task.smallest_key(), pright)
327 != std::cmp::Ordering::Greater
328 });
329
330 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 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 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 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;