1use std::collections::BTreeMap;
22use std::ops::Bound;
23use std::sync::Arc;
24
25use anyhow::Context;
26use futures::StreamExt;
27use risingwave_common::bitmap::Bitmap;
28use risingwave_common::catalog::TableId;
29use risingwave_common::row::{CompactedRow, OwnedRow, Row, RowExt};
30use risingwave_common::types::{DataType, ScalarImpl};
31use risingwave_common::util::epoch::EpochPair;
32use risingwave_common::util::row_serde::OrderedRowSerde;
33use risingwave_common::util::sort_util::OrderType;
34use risingwave_common_estimate_size::{EstimateSize, KvSize};
35use risingwave_storage::StateStore;
36use risingwave_storage::store::PrefetchOptions;
37
38use super::hash_join::{JoinEntryError, JoinHashMapMetrics, TableInner};
39use super::join_row_set::JoinRowSet;
40use crate::cache::ManagedLruCache;
41use crate::common::metrics::MetricsInfo;
42use crate::common::table::state_table::{RowStream, StateTable, StateTablePostCommit};
43use crate::consistency::{consistency_error, enable_strict_consistency};
44use crate::executor::error::StreamExecutorResult;
45use crate::executor::monitor::StreamingMetrics;
46use crate::task::{ActorId, AtomicU64Ref, FragmentId};
47
48type InequalKeyBytes = Vec<u8>;
50type PkSuffixBytes = Vec<u8>;
51
52pub trait AsOfRowEncoding: 'static + Send + Sync {
55 type Encoded: EstimateSize + Clone + Send + Sync;
56 fn encode(row: &OwnedRow) -> Self::Encoded;
57 fn decode(encoded: &Self::Encoded, data_types: &[DataType]) -> OwnedRow;
59}
60
61pub struct AsOfCpuEncoding;
63
64impl AsOfRowEncoding for AsOfCpuEncoding {
65 type Encoded = OwnedRow;
66
67 fn encode(row: &OwnedRow) -> OwnedRow {
68 row.clone()
70 }
71
72 fn decode(encoded: &OwnedRow, _data_types: &[DataType]) -> OwnedRow {
73 encoded.clone()
75 }
76}
77
78pub struct AsOfMemoryEncoding;
80
81impl AsOfRowEncoding for AsOfMemoryEncoding {
82 type Encoded = CompactedRow;
83
84 fn encode(row: &OwnedRow) -> CompactedRow {
85 row.into()
86 }
87
88 fn decode(encoded: &CompactedRow, data_types: &[DataType]) -> OwnedRow {
89 encoded
90 .deserialize(data_types)
91 .expect("failed to decode compacted row in AsOf join cache")
92 }
93}
94
95pub struct AsOfJoinCacheEntry<V: EstimateSize + Clone> {
101 inner: BTreeMap<InequalKeyBytes, JoinRowSet<PkSuffixBytes, V>>,
103 kv_heap_size: KvSize,
105}
106
107impl<V: EstimateSize + Clone> Default for AsOfJoinCacheEntry<V> {
108 fn default() -> Self {
109 Self {
110 inner: BTreeMap::new(),
111 kv_heap_size: KvSize::new(),
112 }
113 }
114}
115
116impl<V: EstimateSize + Clone> EstimateSize for AsOfJoinCacheEntry<V> {
117 fn estimated_heap_size(&self) -> usize {
118 self.kv_heap_size.size()
120 }
121}
122
123impl<V: EstimateSize + Clone> AsOfJoinCacheEntry<V> {
124 fn upper_bound(&self, bound: Bound<&[u8]>) -> Option<&V> {
127 self.inner
128 .range::<[u8], _>((Bound::Unbounded, bound))
129 .next_back()
130 .and_then(|(_, pk_map)| pk_map.first_key_sorted().and_then(|k| pk_map.get(k)))
131 }
132
133 fn lower_bound(&self, bound: Bound<&[u8]>) -> Option<&V> {
136 self.inner
137 .range::<[u8], _>((bound, Bound::Unbounded))
138 .next()
139 .and_then(|(_, pk_map)| pk_map.first_key_sorted().and_then(|k| pk_map.get(k)))
140 }
141
142 fn range(
144 &self,
145 range: (Bound<&[u8]>, Bound<&[u8]>),
146 ) -> impl Iterator<Item = &V> + '_ + use<'_, V> {
147 self.inner
148 .range::<[u8], _>(range)
149 .flat_map(|(_, pk_map)| pk_map.values())
150 }
151
152 fn first_by_inequality(&self, ineq_key_bytes: &[u8]) -> Option<&V> {
154 self.inner
155 .get(ineq_key_bytes)
156 .and_then(|pk_map| pk_map.first_key_sorted().and_then(|k| pk_map.get(k)))
157 }
158
159 fn first_two_by_inequality(&self, ineq_key_bytes: &[u8]) -> (Option<&V>, Option<&V>) {
161 if let Some(pk_map) = self.inner.get(ineq_key_bytes) {
162 let (first_key, second_key) = pk_map.first_two_key_sorted();
163 let first = first_key.and_then(|k| pk_map.get(k));
164 let second = second_key.and_then(|k| pk_map.get(k));
165 (first, second)
166 } else {
167 (None, None)
168 }
169 }
170
171 fn insert(
173 &mut self,
174 ineq_key_bytes: InequalKeyBytes,
175 pk_suffix_bytes: PkSuffixBytes,
176 val: V,
177 ) -> Result<(), JoinEntryError> {
178 use std::collections::btree_map::Entry;
179 let pk_map = match self.inner.entry(ineq_key_bytes) {
180 Entry::Vacant(e) => {
181 self.kv_heap_size.add_val(e.key());
182 e.insert(JoinRowSet::default())
183 }
184 Entry::Occupied(e) => e.into_mut(),
185 };
186 if !enable_strict_consistency()
187 && let Some(old_val) = pk_map.remove(&pk_suffix_bytes)
188 {
189 self.kv_heap_size.sub(&pk_suffix_bytes, &old_val);
190 }
191 self.kv_heap_size.add(&pk_suffix_bytes, &val);
192 let ret = pk_map.try_insert(pk_suffix_bytes.clone(), val);
193 if !enable_strict_consistency() {
194 assert!(ret.is_ok(), "we have removed existing entry, if any");
195 }
196 ret.map(|_| ()).map_err(|_| JoinEntryError::Occupied)
197 }
198
199 fn delete(
201 &mut self,
202 ineq_key_bytes: &InequalKeyBytes,
203 pk_suffix_bytes: &PkSuffixBytes,
204 ) -> Result<(), JoinEntryError> {
205 if let Some(pk_map) = self.inner.get_mut(ineq_key_bytes) {
206 if let Some(old_val) = pk_map.remove(pk_suffix_bytes) {
207 self.kv_heap_size.sub(pk_suffix_bytes, &old_val);
208 if pk_map.is_empty() {
209 self.kv_heap_size.sub_val(ineq_key_bytes);
210 self.inner.remove(ineq_key_bytes);
211 }
212 Ok(())
213 } else if enable_strict_consistency() {
214 Err(JoinEntryError::Remove)
215 } else {
216 consistency_error!(
217 ?pk_suffix_bytes,
218 "removing an asof join cache entry but it's not in the cache"
219 );
220 Ok(())
221 }
222 } else if enable_strict_consistency() {
223 Err(JoinEntryError::Remove)
224 } else {
225 consistency_error!(
226 ?pk_suffix_bytes,
227 "removing an asof join cache entry but it's not in the cache"
228 );
229 Ok(())
230 }
231 }
232}
233
234pub struct AsOfJoinHashMap<S: StateStore, E: AsOfRowEncoding> {
243 state: TableInner<S>,
245 inequality_key_idx: usize,
247 inequality_key_serializer: OrderedRowSerde,
249 pk_suffix_serializer: OrderedRowSerde,
251 pk_suffix_indices: Vec<usize>,
253 cache: Option<ManagedLruCache<OwnedRow, AsOfJoinCacheEntry<E::Encoded>>>,
255 join_key_indices: Vec<usize>,
257 state_all_data_types: Vec<DataType>,
259 metrics: JoinHashMapMetrics,
261}
262
263impl<S: StateStore, E: AsOfRowEncoding> AsOfJoinHashMap<S, E> {
264 #[expect(clippy::too_many_arguments)]
269 pub fn new(
270 state_join_key_indices: Vec<usize>,
271 state_table: StateTable<S>,
272 state_pk_indices: Vec<usize>,
273 inequality_key_idx: usize,
274 state_all_data_types: Vec<DataType>,
275 watermark_epoch: Option<AtomicU64Ref>,
276 metrics: Arc<StreamingMetrics>,
277 actor_id: ActorId,
278 fragment_id: FragmentId,
279 side: &'static str,
280 ) -> Self {
281 let join_table_id = state_table.table_id();
282
283 let inequality_key_serializer = OrderedRowSerde::new(
285 vec![state_all_data_types[inequality_key_idx].clone()],
286 vec![OrderType::ascending()],
287 );
288
289 let pk_suffix_serializer = OrderedRowSerde::new(
291 state_pk_indices
292 .iter()
293 .map(|idx| state_all_data_types[*idx].clone())
294 .collect(),
295 vec![OrderType::ascending(); state_pk_indices.len()],
296 );
297
298 let pk_suffix_indices = state_pk_indices.clone();
299 let join_key_indices = state_join_key_indices.clone();
300
301 let cache = watermark_epoch.map(|epoch_ref| {
302 let metrics_info = MetricsInfo::new(
303 metrics.clone(),
304 join_table_id,
305 actor_id,
306 format!("asof join {}", side),
307 );
308 ManagedLruCache::unbounded(epoch_ref, metrics_info)
309 });
310
311 let state = TableInner::new(state_pk_indices, state_join_key_indices, state_table, None);
312
313 Self {
314 state,
315 inequality_key_idx,
316 inequality_key_serializer,
317 pk_suffix_serializer,
318 pk_suffix_indices,
319 cache,
320 join_key_indices,
321 state_all_data_types,
322 metrics: JoinHashMapMetrics::new(&metrics, actor_id, fragment_id, side, join_table_id),
323 }
324 }
325
326 pub async fn init(&mut self, epoch: EpochPair) -> StreamExecutorResult<()> {
327 self.state.table.init_epoch(epoch).await?;
328 Ok(())
329 }
330
331 pub fn update_watermark(&mut self, watermark: ScalarImpl) {
332 self.state.table.update_watermark(watermark);
333 }
334
335 pub fn table_id(&self) -> TableId {
336 self.state.table.table_id()
337 }
338
339 pub fn entry_count(&self) -> usize {
342 self.cache.as_ref().map_or(0, |c| c.len())
343 }
344
345 fn serialize_inequal_key(&self, row: &impl Row) -> Vec<u8> {
348 row.memcmp_serialize(&self.inequality_key_serializer)
349 }
350
351 fn serialize_inequal_key_from_state_row(&self, row: &impl Row) -> Vec<u8> {
353 row.project(&[self.inequality_key_idx])
354 .memcmp_serialize(&self.inequality_key_serializer)
355 }
356
357 fn serialize_pk_suffix(&self, row: &impl Row) -> Vec<u8> {
359 row.project(&self.pk_suffix_indices)
360 .memcmp_serialize(&self.pk_suffix_serializer)
361 }
362
363 async fn ensure_cache_populated(&mut self, join_key: &OwnedRow) -> StreamExecutorResult<()> {
366 let needs_populate = match &self.cache {
367 Some(cache) => !cache.contains(join_key),
368 None => return Ok(()),
369 };
370 self.metrics.inc_lookup();
371 if !needs_populate {
372 return Ok(());
373 }
374 self.metrics.inc_lookup_miss();
375
376 let sub_range: (Bound<OwnedRow>, Bound<OwnedRow>) = (Bound::Unbounded, Bound::Unbounded);
378 let table_iter = self
379 .state
380 .table
381 .iter_with_prefix(join_key, &sub_range, PrefetchOptions::default())
382 .await?;
383 let mut pinned = std::pin::pin!(table_iter);
384 let mut entry = AsOfJoinCacheEntry::<E::Encoded>::default();
385 while let Some(row) = pinned.next().await.transpose()? {
386 let ineq_key = self.serialize_inequal_key_from_state_row(&row);
387 let pk_suffix = self.serialize_pk_suffix(&row);
388 entry
389 .insert(ineq_key, pk_suffix, E::encode(&row))
390 .with_context(|| format!("row: {}", row.display()))?;
391 }
392 self.cache.as_mut().unwrap().put(join_key.clone(), entry);
393 Ok(())
394 }
395
396 pub fn insert(&mut self, value: impl Row) -> StreamExecutorResult<()> {
398 if self.cache.is_some() {
399 let owned_row = value.to_owned_row();
401 let ineq_key = self.serialize_inequal_key_from_state_row(&owned_row);
402 let pk_suffix = self.serialize_pk_suffix(&owned_row);
403 let join_key = (&owned_row).project(&self.join_key_indices).to_owned_row();
405 if let Some(cache) = &mut self.cache
407 && let Some(mut entry) = cache.get_mut(&join_key)
408 {
409 entry
410 .insert(ineq_key, pk_suffix, E::encode(&owned_row))
411 .with_context(|| format!("row: {}", owned_row.display()))?;
412 } else {
413 self.metrics.inc_insert_cache_miss();
414 }
415 self.state.table.insert(owned_row);
416 } else {
417 self.state.table.insert(value);
418 }
419 Ok(())
420 }
421
422 pub fn delete(&mut self, value: impl Row) -> StreamExecutorResult<()> {
424 if self.cache.is_some() {
425 let owned_row = value.to_owned_row();
427 let ineq_key = self.serialize_inequal_key_from_state_row(&owned_row);
428 let pk_suffix = self.serialize_pk_suffix(&owned_row);
429 let join_key = (&owned_row).project(&self.join_key_indices).to_owned_row();
431 if let Some(cache) = &mut self.cache
433 && let Some(mut entry) = cache.get_mut(&join_key)
434 {
435 entry
436 .delete(&ineq_key, &pk_suffix)
437 .with_context(|| format!("row: {}", owned_row.display()))?;
438 }
439 self.state.table.delete(owned_row);
440 } else {
441 self.state.table.delete(value);
442 }
443 Ok(())
444 }
445
446 pub fn evict_cache(&mut self) {
448 if let Some(cache) = &mut self.cache {
449 cache.evict();
450 }
451 }
452
453 pub async fn flush(
455 &mut self,
456 epoch: EpochPair,
457 ) -> StreamExecutorResult<AsOfJoinHashMapPostCommit<'_, S, E>> {
458 self.metrics.flush();
459 if let Some(cache) = &mut self.cache {
460 cache.evict();
461 }
462 let state_post_commit = self.state.table.commit(epoch).await?;
463 Ok(AsOfJoinHashMapPostCommit {
464 state: state_post_commit,
465 cache: self.cache.as_mut(),
466 })
467 }
468
469 pub async fn try_flush(&mut self) -> StreamExecutorResult<()> {
470 self.state.table.try_flush().await?;
471 Ok(())
472 }
473
474 pub async fn upper_bound_by_inequality_with_jk_prefix(
475 &mut self,
476 join_key: impl Row,
477 bound: Bound<&impl Row>,
478 ) -> StreamExecutorResult<Option<OwnedRow>> {
479 if self.cache.is_some() {
480 let join_key_owned = join_key.to_owned_row();
481 let bound_bytes = match &bound {
483 Bound::Included(r) => Bound::Included(self.serialize_inequal_key(r)),
484 Bound::Excluded(r) => Bound::Excluded(self.serialize_inequal_key(r)),
485 Bound::Unbounded => Bound::Unbounded,
486 };
487 self.ensure_cache_populated(&join_key_owned).await?;
488 let cache = self.cache.as_mut().unwrap();
489 if let Some(entry) = cache.get(&join_key_owned) {
490 let bound_ref = match &bound_bytes {
491 Bound::Included(b) => Bound::Included(b.as_slice()),
492 Bound::Excluded(b) => Bound::Excluded(b.as_slice()),
493 Bound::Unbounded => Bound::Unbounded,
494 };
495 return Ok(entry
496 .upper_bound(bound_ref)
497 .map(|v| E::decode(v, &self.state_all_data_types)));
498 }
499 }
501 let sub_range = (Bound::<OwnedRow>::Unbounded, bound);
506 let table_iter = self
507 .state
508 .table
509 .rev_iter_with_prefix(&join_key, &sub_range, PrefetchOptions::default())
510 .await?;
511 let mut pinned_table_iter = std::pin::pin!(table_iter);
512 let Some(rev_row) = pinned_table_iter.next().await.transpose()? else {
513 return Ok(None);
514 };
515 let found_ineq_key = (&rev_row)
517 .project(&[self.inequality_key_idx])
518 .to_owned_row();
519 let exact_range = (
520 Bound::Included(&found_ineq_key),
521 Bound::Included(&found_ineq_key),
522 );
523 let fwd_iter = self
524 .state
525 .table
526 .iter_with_prefix(&join_key, &exact_range, PrefetchOptions::default())
527 .await?;
528 let mut pinned_fwd = std::pin::pin!(fwd_iter);
529 let row = pinned_fwd.next().await.transpose()?;
531 Ok(row.or(Some(rev_row)))
532 }
533
534 pub async fn lower_bound_by_inequality_with_jk_prefix(
535 &mut self,
536 join_key: &impl Row,
537 bound: Bound<&impl Row>,
538 ) -> StreamExecutorResult<Option<OwnedRow>> {
539 if self.cache.is_some() {
540 let join_key_owned = join_key.to_owned_row();
541 let bound_bytes = match &bound {
543 Bound::Included(r) => Bound::Included(self.serialize_inequal_key(r)),
544 Bound::Excluded(r) => Bound::Excluded(self.serialize_inequal_key(r)),
545 Bound::Unbounded => Bound::Unbounded,
546 };
547 self.ensure_cache_populated(&join_key_owned).await?;
548 let cache = self.cache.as_mut().unwrap();
549 if let Some(entry) = cache.get(&join_key_owned) {
550 let bound_ref = match &bound_bytes {
551 Bound::Included(b) => Bound::Included(b.as_slice()),
552 Bound::Excluded(b) => Bound::Excluded(b.as_slice()),
553 Bound::Unbounded => Bound::Unbounded,
554 };
555 return Ok(entry
556 .lower_bound(bound_ref)
557 .map(|v| E::decode(v, &self.state_all_data_types)));
558 }
559 }
560 let sub_range = (bound, Bound::<OwnedRow>::Unbounded);
562 let table_iter = self
563 .state
564 .table
565 .iter_with_prefix(join_key, &sub_range, PrefetchOptions::default())
566 .await?;
567 let mut pinned_table_iter = std::pin::pin!(table_iter);
568 let row = pinned_table_iter.next().await.transpose()?;
569 Ok(row)
570 }
571
572 async fn table_iter_by_inequality_with_jk_prefix<'a>(
573 &'a self,
574 join_key: &'a impl Row,
575 range: &'a (Bound<impl Row>, Bound<impl Row>),
576 ) -> StreamExecutorResult<impl RowStream<'a>> {
577 self.state
578 .table
579 .iter_with_prefix(join_key, range, PrefetchOptions::default())
580 .await
581 }
582
583 pub async fn first_by_inequality_with_jk_prefix(
584 &mut self,
585 join_key: &impl Row,
586 inequality_key: &impl Row,
587 ) -> StreamExecutorResult<Option<OwnedRow>> {
588 if self.cache.is_some() {
589 let join_key_owned = join_key.to_owned_row();
590 let ineq_key_bytes = self.serialize_inequal_key(inequality_key);
592 self.ensure_cache_populated(&join_key_owned).await?;
593 let cache = self.cache.as_mut().unwrap();
594 if let Some(entry) = cache.get(&join_key_owned) {
595 return Ok(entry
596 .first_by_inequality(&ineq_key_bytes)
597 .map(|v| E::decode(v, &self.state_all_data_types)));
598 }
599 }
600 let range = (
602 Bound::Included(inequality_key),
603 Bound::Included(inequality_key),
604 );
605 let table_iter = self
606 .table_iter_by_inequality_with_jk_prefix(join_key, &range)
607 .await?;
608 let mut pinned_table_iter = std::pin::pin!(table_iter);
609 let row = pinned_table_iter.next().await.transpose()?;
610 Ok(row)
611 }
612
613 pub async fn first_two_by_inequality_with_jk_prefix(
617 &mut self,
618 join_key: &impl Row,
619 inequality_key: &impl Row,
620 ) -> StreamExecutorResult<(Option<OwnedRow>, Option<OwnedRow>)> {
621 if self.cache.is_some() {
622 let join_key_owned = join_key.to_owned_row();
623 let ineq_key_bytes = self.serialize_inequal_key(inequality_key);
624 self.ensure_cache_populated(&join_key_owned).await?;
625 let cache = self.cache.as_mut().unwrap();
626 if let Some(entry) = cache.get(&join_key_owned) {
627 let (first, second) = entry.first_two_by_inequality(&ineq_key_bytes);
628 return Ok((
629 first.map(|v| E::decode(v, &self.state_all_data_types)),
630 second.map(|v| E::decode(v, &self.state_all_data_types)),
631 ));
632 }
633 }
634 let range = (
636 Bound::Included(inequality_key),
637 Bound::Included(inequality_key),
638 );
639 let table_iter = self
640 .table_iter_by_inequality_with_jk_prefix(join_key, &range)
641 .await?;
642 let mut pinned_table_iter = std::pin::pin!(table_iter);
643 let first = pinned_table_iter.next().await.transpose()?;
644 let second = if first.is_some() {
645 pinned_table_iter.next().await.transpose()?
646 } else {
647 None
648 };
649 Ok((first, second))
650 }
651
652 pub async fn range_by_inequality_with_jk_prefix<'a>(
653 &'a mut self,
654 join_key: &'a impl Row,
655 inequality_key_range: &'a (Bound<impl Row>, Bound<impl Row>),
656 ) -> StreamExecutorResult<impl RowStream<'a>> {
657 if self.cache.is_some() {
658 let join_key_owned = join_key.to_owned_row();
659 let bound_lo = match &inequality_key_range.0 {
661 Bound::Included(r) => Bound::Included(self.serialize_inequal_key(r)),
662 Bound::Excluded(r) => Bound::Excluded(self.serialize_inequal_key(r)),
663 Bound::Unbounded => Bound::Unbounded,
664 };
665 let bound_hi = match &inequality_key_range.1 {
666 Bound::Included(r) => Bound::Included(self.serialize_inequal_key(r)),
667 Bound::Excluded(r) => Bound::Excluded(self.serialize_inequal_key(r)),
668 Bound::Unbounded => Bound::Unbounded,
669 };
670 self.ensure_cache_populated(&join_key_owned).await?;
671 let cache = self.cache.as_mut().unwrap();
672 if let Some(entry) = cache.get(&join_key_owned) {
673 let range = (
674 bound_lo.as_ref().map(|b| b.as_slice()),
675 bound_hi.as_ref().map(|b| b.as_slice()),
676 );
677 let data_types = &self.state_all_data_types;
679 return Ok(futures::future::Either::Left(futures::stream::iter(
680 entry.range(range).map(|v| Ok(E::decode(v, data_types))),
681 )));
682 }
683 }
684 let table_iter = self
686 .state
687 .table
688 .iter_with_prefix(join_key, inequality_key_range, PrefetchOptions::default())
689 .await?;
690 Ok(futures::future::Either::Right(table_iter))
691 }
692
693 pub fn check_inequal_key_null(&self, row: &impl Row) -> bool {
695 row.datum_at(self.inequality_key_idx).is_none()
696 }
697
698 pub fn get_pk_from_row<'a>(&'a self, row: impl Row + 'a) -> impl Row + 'a {
699 row.project(&self.state.pk_indices)
700 }
701}
702
703#[must_use]
704pub struct AsOfJoinHashMapPostCommit<'a, S: StateStore, E: AsOfRowEncoding> {
705 state: StateTablePostCommit<'a, S>,
706 cache: Option<&'a mut ManagedLruCache<OwnedRow, AsOfJoinCacheEntry<E::Encoded>>>,
707}
708
709impl<S: StateStore, E: AsOfRowEncoding> AsOfJoinHashMapPostCommit<'_, S, E> {
710 pub async fn post_yield_barrier(
711 self,
712 vnode_bitmap: Option<Arc<Bitmap>>,
713 ) -> StreamExecutorResult<Option<bool>> {
714 let cache_may_stale = self.state.post_yield_barrier(vnode_bitmap.clone()).await?;
715 let keyed_cache_may_stale = cache_may_stale.map(|(_, changed)| changed);
716 if keyed_cache_may_stale.unwrap_or(false)
718 && let Some(cache) = self.cache
719 {
720 cache.clear();
721 }
722 Ok(keyed_cache_may_stale)
723 }
724}