1use std::cmp::Ordering;
22use std::collections::BTreeMap;
23use std::ops::Bound;
24use std::time::Duration;
25
26use either::Either;
27use itertools::Itertools;
28use multimap::MultiMap;
29use risingwave_common::array::Op;
30use risingwave_common::metrics::LabelGuardedHistogram;
31use risingwave_common::row::RowExt;
32use risingwave_common::util::epoch::EpochPair;
33use risingwave_common::util::sort_util::cmp_rows_ascending;
34use tokio::time::Instant;
35
36use super::barrier_align::*;
37use super::join::asof_join::*;
38use super::join::builder::JoinStreamChunkBuilder;
39use super::join::row::JoinRow;
40use super::join::*;
41use super::watermark::*;
42use crate::executor::join::builder::JoinChunkBuilder;
43use crate::executor::prelude::*;
44
45pub struct JoinParams {
46 pub join_key_indices: Vec<usize>,
48 pub deduped_pk_indices: Vec<usize>,
50}
51
52impl JoinParams {
53 pub fn new(join_key_indices: Vec<usize>, deduped_pk_indices: Vec<usize>) -> Self {
54 Self {
55 join_key_indices,
56 deduped_pk_indices,
57 }
58 }
59}
60
61struct JoinSide<S: StateStore, E: AsOfRowEncoding> {
62 ht: AsOfJoinHashMap<S, E>,
64 join_key_indices: Vec<usize>,
66 all_data_types: Vec<DataType>,
68 i2o_mapping: Vec<(usize, usize)>,
70 i2o_mapping_indexed: MultiMap<usize, usize>,
71 inequal_key_idx: usize,
73}
74
75impl<S: StateStore, E: AsOfRowEncoding> std::fmt::Debug for JoinSide<S, E> {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_struct("JoinSide")
78 .field("join_key_indices", &self.join_key_indices)
79 .field("col_types", &self.all_data_types)
80 .field("i2o_mapping", &self.i2o_mapping)
81 .finish()
82 }
83}
84
85impl<S: StateStore, E: AsOfRowEncoding> JoinSide<S, E> {
86 pub async fn init(&mut self, epoch: EpochPair) -> StreamExecutorResult<()> {
87 self.ht.init(epoch).await
88 }
89}
90
91pub struct AsOfJoinExecutor<S: StateStore, const T: AsOfJoinTypePrimitive, E: AsOfRowEncoding> {
97 ctx: ActorContextRef,
98 info: ExecutorInfo,
99
100 input_l: Option<Executor>,
102 input_r: Option<Executor>,
104 actual_output_data_types: Vec<DataType>,
106 side_l: JoinSide<S, E>,
108 side_r: JoinSide<S, E>,
110
111 null_safe: Vec<bool>,
113
114 metrics: Arc<StreamingMetrics>,
115 chunk_size: usize,
117 watermark_buffers: BTreeMap<usize, BufferedWatermarks<SideTypePrimitive>>,
119 asof_desc: AsOfDesc,
121 cnt_rows_received: u32,
123 join_cache_evict_interval_rows: u32,
125 high_join_amplification_threshold: usize,
127}
128
129impl<S: StateStore, const T: AsOfJoinTypePrimitive, E: AsOfRowEncoding> std::fmt::Debug
130 for AsOfJoinExecutor<S, T, E>
131{
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 f.debug_struct("AsOfJoinExecutor")
134 .field("join_type", &T)
135 .field("input_left", &self.input_l.as_ref().unwrap().identity())
136 .field("input_right", &self.input_r.as_ref().unwrap().identity())
137 .field("side_l", &self.side_l)
138 .field("side_r", &self.side_r)
139 .field("stream_key", &self.info.stream_key)
140 .field("schema", &self.info.schema)
141 .field("actual_output_data_types", &self.actual_output_data_types)
142 .finish()
143 }
144}
145
146impl<S: StateStore, const T: AsOfJoinTypePrimitive, E: AsOfRowEncoding> Execute
147 for AsOfJoinExecutor<S, T, E>
148{
149 fn execute(self: Box<Self>) -> BoxedMessageStream {
150 self.into_stream().boxed()
151 }
152}
153
154struct EqJoinArgs<'a, S: StateStore, E: AsOfRowEncoding> {
155 ctx: &'a ActorContextRef,
156 side_l: &'a mut JoinSide<S, E>,
157 side_r: &'a mut JoinSide<S, E>,
158 null_safe: &'a [bool],
159 asof_desc: &'a AsOfDesc,
160 actual_output_data_types: &'a [DataType],
161 chunk: StreamChunk,
162 chunk_size: usize,
163 cnt_rows_received: &'a mut u32,
164 join_cache_evict_interval_rows: u32,
165 high_join_amplification_threshold: usize,
166 join_matched_join_keys: &'a LabelGuardedHistogram,
168}
169
170impl<S: StateStore, const T: AsOfJoinTypePrimitive, E: AsOfRowEncoding> AsOfJoinExecutor<S, T, E> {
171 #[expect(clippy::too_many_arguments)]
172 pub fn new(
173 ctx: ActorContextRef,
174 info: ExecutorInfo,
175 input_l: Executor,
176 input_r: Executor,
177 params_l: JoinParams,
178 params_r: JoinParams,
179 null_safe: Vec<bool>,
180 output_indices: Vec<usize>,
181 state_table_l: StateTable<S>,
182 state_table_r: StateTable<S>,
183 watermark_epoch: AtomicU64Ref,
184 metrics: Arc<StreamingMetrics>,
185 chunk_size: usize,
186 asof_desc: AsOfDesc,
187 use_cache: bool,
188 high_join_amplification_threshold: usize,
189 ) -> Self {
190 let join_cache_evict_interval_rows = ctx
191 .config
192 .developer
193 .join_hash_map_evict_interval_rows
194 .max(1);
195 let cache_epoch = if use_cache {
196 Some(watermark_epoch)
197 } else {
198 None
199 };
200 let schema_fields = [
201 input_l.schema().fields.clone(),
202 input_r.schema().fields.clone(),
203 ]
204 .concat();
205
206 let original_output_data_types = schema_fields
207 .iter()
208 .map(|field| field.data_type())
209 .collect_vec();
210 let actual_output_data_types = output_indices
211 .iter()
212 .map(|&idx| original_output_data_types[idx].clone())
213 .collect_vec();
214
215 let state_all_data_types_l = input_l.schema().data_types();
216 let state_all_data_types_r = input_r.schema().data_types();
217
218 let state_join_key_indices_l = params_l.join_key_indices;
219 let state_join_key_indices_r = params_r.join_key_indices;
220
221 let join_key_data_types_l = state_join_key_indices_l
222 .iter()
223 .map(|idx| state_all_data_types_l[*idx].clone())
224 .collect_vec();
225
226 let join_key_data_types_r = state_join_key_indices_r
227 .iter()
228 .map(|idx| state_all_data_types_r[*idx].clone())
229 .collect_vec();
230
231 assert_eq!(join_key_data_types_l, join_key_data_types_r);
232
233 let (left_to_output, right_to_output) = {
234 let (left_len, right_len) = if is_left_semi_or_anti(T) {
235 (state_all_data_types_l.len(), 0usize)
236 } else if is_right_semi_or_anti(T) {
237 (0usize, state_all_data_types_r.len())
238 } else {
239 (state_all_data_types_l.len(), state_all_data_types_r.len())
240 };
241 JoinStreamChunkBuilder::get_i2o_mapping(&output_indices, left_len, right_len)
242 };
243
244 let l2o_indexed = MultiMap::from_iter(left_to_output.iter().copied());
245 let r2o_indexed = MultiMap::from_iter(right_to_output.iter().copied());
246
247 let watermark_buffers = BTreeMap::new();
248
249 let inequal_key_idx_l = asof_desc.left_idx;
250 let inequal_key_idx_r = asof_desc.right_idx;
251
252 Self {
253 ctx: ctx.clone(),
254 info,
255 input_l: Some(input_l),
256 input_r: Some(input_r),
257 actual_output_data_types,
258 null_safe,
259 side_l: JoinSide {
260 ht: AsOfJoinHashMap::new(
261 state_join_key_indices_l.clone(),
262 state_table_l,
263 params_l.deduped_pk_indices,
264 inequal_key_idx_l,
265 state_all_data_types_l.clone(),
266 cache_epoch.clone(),
267 metrics.clone(),
268 ctx.id,
269 ctx.fragment_id,
270 "left",
271 ),
272 join_key_indices: state_join_key_indices_l,
273 all_data_types: state_all_data_types_l,
274 i2o_mapping: left_to_output,
275 i2o_mapping_indexed: l2o_indexed,
276 inequal_key_idx: inequal_key_idx_l,
277 },
278 side_r: JoinSide {
279 ht: AsOfJoinHashMap::new(
280 state_join_key_indices_r.clone(),
281 state_table_r,
282 params_r.deduped_pk_indices,
283 inequal_key_idx_r,
284 state_all_data_types_r.clone(),
285 cache_epoch,
286 metrics.clone(),
287 ctx.id,
288 ctx.fragment_id,
289 "right",
290 ),
291 join_key_indices: state_join_key_indices_r,
292 all_data_types: state_all_data_types_r,
293 i2o_mapping: right_to_output,
294 i2o_mapping_indexed: r2o_indexed,
295 inequal_key_idx: inequal_key_idx_r,
296 },
297 metrics,
298 chunk_size,
299 watermark_buffers,
300 asof_desc,
301 cnt_rows_received: 0,
302 join_cache_evict_interval_rows,
303 high_join_amplification_threshold,
304 }
305 }
306
307 fn evict_cache(
309 side_l: &mut JoinSide<S, E>,
310 side_r: &mut JoinSide<S, E>,
311 cnt_rows_received: &mut u32,
312 join_cache_evict_interval_rows: u32,
313 ) {
314 *cnt_rows_received += 1;
315 if *cnt_rows_received >= join_cache_evict_interval_rows {
316 side_l.ht.evict_cache();
317 side_r.ht.evict_cache();
318 *cnt_rows_received = 0;
319 }
320 }
321
322 #[try_stream(ok = Message, error = StreamExecutorError)]
323 async fn into_stream(mut self) {
324 let input_l = self.input_l.take().unwrap();
325 let input_r = self.input_r.take().unwrap();
326 let aligned_stream = barrier_align(
327 input_l.execute(),
328 input_r.execute(),
329 self.ctx.id,
330 self.ctx.fragment_id,
331 self.metrics.clone(),
332 "Join",
333 );
334 pin_mut!(aligned_stream);
335 let actor_id = self.ctx.id;
336
337 let barrier = expect_first_barrier_from_aligned_stream(&mut aligned_stream).await?;
338 let first_epoch = barrier.epoch;
339 yield Message::Barrier(barrier);
340 self.side_l.init(first_epoch).await?;
341 self.side_r.init(first_epoch).await?;
342
343 let actor_id_str = self.ctx.id.to_string();
344 let fragment_id_str = self.ctx.fragment_id.to_string();
345
346 let join_actor_input_waiting_duration_ns = self
347 .metrics
348 .join_actor_input_waiting_duration_ns
349 .with_guarded_label_values(&[&actor_id_str, &fragment_id_str]);
350 let left_join_match_duration_ns = self
351 .metrics
352 .join_match_duration_ns
353 .with_guarded_label_values(&[actor_id_str.as_str(), fragment_id_str.as_str(), "left"]);
354 let right_join_match_duration_ns = self
355 .metrics
356 .join_match_duration_ns
357 .with_guarded_label_values(&[actor_id_str.as_str(), fragment_id_str.as_str(), "right"]);
358
359 let barrier_join_match_duration_ns = self
360 .metrics
361 .join_match_duration_ns
362 .with_guarded_label_values(&[
363 actor_id_str.as_str(),
364 fragment_id_str.as_str(),
365 "barrier",
366 ]);
367
368 let left_join_cached_entry_count = self
369 .metrics
370 .join_cached_entry_count
371 .with_guarded_label_values(&[actor_id_str.as_str(), fragment_id_str.as_str(), "left"]);
372
373 let right_join_cached_entry_count = self
374 .metrics
375 .join_cached_entry_count
376 .with_guarded_label_values(&[actor_id_str.as_str(), fragment_id_str.as_str(), "right"]);
377
378 let left_table_id_str = self.side_l.ht.table_id().to_string();
380 let right_table_id_str = self.side_r.ht.table_id().to_string();
381 let left_join_matched_join_keys = self
382 .metrics
383 .join_matched_join_keys
384 .with_guarded_label_values(&[
385 actor_id_str.as_str(),
386 fragment_id_str.as_str(),
387 left_table_id_str.as_str(),
388 ]);
389 let right_join_matched_join_keys = self
390 .metrics
391 .join_matched_join_keys
392 .with_guarded_label_values(&[
393 actor_id_str.as_str(),
394 fragment_id_str.as_str(),
395 right_table_id_str.as_str(),
396 ]);
397
398 let mut start_time = Instant::now();
399
400 while let Some(msg) = aligned_stream
401 .next()
402 .instrument_await("hash_join_barrier_align")
403 .await
404 {
405 join_actor_input_waiting_duration_ns.inc_by(start_time.elapsed().as_nanos() as u64);
406 match msg? {
407 AlignedMessage::WatermarkLeft(watermark) => {
408 for watermark_to_emit in self.handle_watermark(SideType::Left, watermark)? {
409 yield Message::Watermark(watermark_to_emit);
410 }
411 }
412 AlignedMessage::WatermarkRight(watermark) => {
413 for watermark_to_emit in self.handle_watermark(SideType::Right, watermark)? {
414 yield Message::Watermark(watermark_to_emit);
415 }
416 }
417 AlignedMessage::Left(chunk) => {
418 let mut left_time = Duration::from_nanos(0);
419 let mut left_start_time = Instant::now();
420 #[for_await]
421 for chunk in Self::eq_join_left(EqJoinArgs {
422 ctx: &self.ctx,
423 side_l: &mut self.side_l,
424 side_r: &mut self.side_r,
425 null_safe: &self.null_safe,
426 asof_desc: &self.asof_desc,
427 actual_output_data_types: &self.actual_output_data_types,
428 chunk,
429 chunk_size: self.chunk_size,
430 cnt_rows_received: &mut self.cnt_rows_received,
431 join_cache_evict_interval_rows: self.join_cache_evict_interval_rows,
432 high_join_amplification_threshold: self.high_join_amplification_threshold,
433 join_matched_join_keys: &left_join_matched_join_keys,
434 }) {
435 left_time += left_start_time.elapsed();
436 yield Message::Chunk(chunk?);
437 left_start_time = Instant::now();
438 }
439 left_time += left_start_time.elapsed();
440 left_join_match_duration_ns.inc_by(left_time.as_nanos() as u64);
441 self.try_flush_data().await?;
442 }
443 AlignedMessage::Right(chunk) => {
444 let mut right_time = Duration::from_nanos(0);
445 let mut right_start_time = Instant::now();
446 #[for_await]
447 for chunk in Self::eq_join_right(EqJoinArgs {
448 ctx: &self.ctx,
449 side_l: &mut self.side_l,
450 side_r: &mut self.side_r,
451 null_safe: &self.null_safe,
452 asof_desc: &self.asof_desc,
453 actual_output_data_types: &self.actual_output_data_types,
454 chunk,
455 chunk_size: self.chunk_size,
456 cnt_rows_received: &mut self.cnt_rows_received,
457 join_cache_evict_interval_rows: self.join_cache_evict_interval_rows,
458 high_join_amplification_threshold: self.high_join_amplification_threshold,
459 join_matched_join_keys: &right_join_matched_join_keys,
460 }) {
461 right_time += right_start_time.elapsed();
462 yield Message::Chunk(chunk?);
463 right_start_time = Instant::now();
464 }
465 right_time += right_start_time.elapsed();
466 right_join_match_duration_ns.inc_by(right_time.as_nanos() as u64);
467 self.try_flush_data().await?;
468 }
469 AlignedMessage::Barrier(barrier) => {
470 let barrier_start_time = Instant::now();
471 let (left_post_commit, right_post_commit) =
472 self.flush_data(barrier.epoch).await?;
473
474 let update_vnode_bitmap = barrier.as_update_vnode_bitmap(actor_id);
475 yield Message::Barrier(barrier);
476
477 right_post_commit
479 .post_yield_barrier(update_vnode_bitmap.clone())
480 .await?;
481 if left_post_commit
482 .post_yield_barrier(update_vnode_bitmap)
483 .await?
484 .unwrap_or(false)
485 {
486 self.watermark_buffers
487 .values_mut()
488 .for_each(|buffers| buffers.clear());
489 }
490
491 for (join_cached_entry_count, ht) in [
493 (&left_join_cached_entry_count, &self.side_l.ht),
494 (&right_join_cached_entry_count, &self.side_r.ht),
495 ] {
496 join_cached_entry_count.set(ht.entry_count() as i64);
497 }
498
499 barrier_join_match_duration_ns
500 .inc_by(barrier_start_time.elapsed().as_nanos() as u64);
501 }
502 }
503 start_time = Instant::now();
504 }
505 }
506
507 async fn flush_data(
508 &mut self,
509 epoch: EpochPair,
510 ) -> StreamExecutorResult<(
511 AsOfJoinHashMapPostCommit<'_, S, E>,
512 AsOfJoinHashMapPostCommit<'_, S, E>,
513 )> {
514 let left = self.side_l.ht.flush(epoch).await?;
515 let right = self.side_r.ht.flush(epoch).await?;
516 Ok((left, right))
517 }
518
519 async fn try_flush_data(&mut self) -> StreamExecutorResult<()> {
520 self.side_l.ht.try_flush().await?;
521 self.side_r.ht.try_flush().await?;
522 Ok(())
523 }
524
525 fn handle_watermark(
526 &mut self,
527 side: SideTypePrimitive,
528 watermark: Watermark,
529 ) -> StreamExecutorResult<Vec<Watermark>> {
530 let (side_update, side_match) = if side == SideType::Left {
531 (&mut self.side_l, &mut self.side_r)
532 } else {
533 (&mut self.side_r, &mut self.side_l)
534 };
535
536 let wm_in_jk = side_update
538 .join_key_indices
539 .iter()
540 .positions(|idx| *idx == watermark.col_idx);
541 let mut watermarks_to_emit = vec![];
542 let mut clean_watermark = None;
543 for idx in wm_in_jk {
544 let buffers = self
545 .watermark_buffers
546 .entry(idx)
547 .or_insert_with(|| BufferedWatermarks::with_ids([SideType::Left, SideType::Right]));
548 if let Some(selected_watermark) = buffers.handle_watermark(side, watermark.clone()) {
549 if idx == 0 {
550 clean_watermark = Some(selected_watermark.val.clone());
551 }
552 let empty_indices = vec![];
553 let output_indices = side_update
554 .i2o_mapping_indexed
555 .get_vec(&side_update.join_key_indices[idx])
556 .unwrap_or(&empty_indices)
557 .iter()
558 .chain(
559 side_match
560 .i2o_mapping_indexed
561 .get_vec(&side_match.join_key_indices[idx])
562 .unwrap_or(&empty_indices),
563 );
564 for output_idx in output_indices {
565 watermarks_to_emit.push(selected_watermark.clone().with_idx(*output_idx));
566 }
567 };
568 }
569 if let Some(watermark) = clean_watermark {
572 self.side_l.ht.update_watermark(watermark.clone());
573 self.side_r.ht.update_watermark(watermark);
574 }
575 Ok(watermarks_to_emit)
576 }
577
578 #[try_stream(ok = StreamChunk, error = StreamExecutorError)]
579 async fn eq_join_left(args: EqJoinArgs<'_, S, E>) {
580 let EqJoinArgs {
581 ctx,
582 side_l,
583 side_r,
584 null_safe,
585 asof_desc,
586 actual_output_data_types,
587 chunk,
588 chunk_size,
589 cnt_rows_received,
590 join_cache_evict_interval_rows,
591 high_join_amplification_threshold,
592 join_matched_join_keys,
593 } = args;
594
595 let (side_update, side_match) = (side_l, side_r);
596
597 let mut join_chunk_builder =
598 JoinChunkBuilder::<T, { SideType::Left }>::new(JoinStreamChunkBuilder::new(
599 chunk_size,
600 actual_output_data_types.to_vec(),
601 side_update.i2o_mapping.clone(),
602 side_match.i2o_mapping.clone(),
603 ));
604
605 let inequal_key_idx_update = [side_update.inequal_key_idx];
607
608 for r in chunk.rows_with_holes() {
609 let Some((op, row)) = r else {
610 continue;
611 };
612 Self::evict_cache(
613 side_update,
614 side_match,
615 cnt_rows_received,
616 join_cache_evict_interval_rows,
617 );
618
619 let join_key_null_not_safe = side_update
622 .join_key_indices
623 .iter()
624 .zip_eq(null_safe.iter())
625 .any(|(idx, ns)| !ns && row.datum_at(*idx).is_none());
626 if join_key_null_not_safe {
627 match op {
628 Op::Insert | Op::UpdateInsert => {
629 if let Some(chunk) =
630 join_chunk_builder.forward_if_not_matched(Op::Insert, row)
631 {
632 yield chunk;
633 }
634 }
635 Op::Delete | Op::UpdateDelete => {
636 if let Some(chunk) =
637 join_chunk_builder.forward_if_not_matched(Op::Delete, row)
638 {
639 yield chunk;
640 }
641 }
642 }
643 join_matched_join_keys.observe(0.0);
644 continue;
645 }
646
647 let join_key = row.project(&side_update.join_key_indices);
648
649 let inequal_key_is_null = side_update.ht.check_inequal_key_null(&row);
650 let inequal_key = row.project(&inequal_key_idx_update);
651
652 let mut join_matched_rows_cnt = 0;
653
654 if !inequal_key_is_null {
655 let matched_row_by_inequality = match asof_desc.inequality_type {
656 AsOfInequalityType::Lt => {
657 side_match
658 .ht
659 .lower_bound_by_inequality_with_jk_prefix(
660 &join_key,
661 Bound::Excluded(&inequal_key),
662 )
663 .await
664 }
665 AsOfInequalityType::Le => {
666 side_match
667 .ht
668 .lower_bound_by_inequality_with_jk_prefix(
669 &join_key,
670 Bound::Included(&inequal_key),
671 )
672 .await
673 }
674 AsOfInequalityType::Gt => {
675 side_match
676 .ht
677 .upper_bound_by_inequality_with_jk_prefix(
678 &join_key,
679 Bound::Excluded(&inequal_key),
680 )
681 .await
682 }
683 AsOfInequalityType::Ge => {
684 side_match
685 .ht
686 .upper_bound_by_inequality_with_jk_prefix(
687 &join_key,
688 Bound::Included(&inequal_key),
689 )
690 .await
691 }
692 }?
693 .map(|row| JoinRow::new(row, 0));
694 match op {
695 Op::Insert | Op::UpdateInsert => {
696 if let Some(matched_row) = matched_row_by_inequality {
697 join_matched_rows_cnt += 1;
698 if let Some(chunk) =
699 join_chunk_builder.with_match_on_insert(&row, &matched_row)
700 {
701 yield chunk;
702 }
703 } else if let Some(chunk) =
704 join_chunk_builder.forward_if_not_matched(Op::Insert, row)
705 {
706 yield chunk;
707 }
708 side_update.ht.insert(row)?;
709 }
710 Op::Delete | Op::UpdateDelete => {
711 if let Some(matched_row) = matched_row_by_inequality {
712 join_matched_rows_cnt += 1;
713 if let Some(chunk) =
714 join_chunk_builder.with_match_on_delete(&row, &matched_row)
715 {
716 yield chunk;
717 }
718 } else if let Some(chunk) =
719 join_chunk_builder.forward_if_not_matched(Op::Delete, row)
720 {
721 yield chunk;
722 }
723 side_update.ht.delete(row)?;
724 }
725 }
726 } else {
727 match op {
730 Op::Insert | Op::UpdateInsert => {
731 if let Some(chunk) =
732 join_chunk_builder.forward_if_not_matched(Op::Insert, row)
733 {
734 yield chunk;
735 }
736 }
737 Op::Delete | Op::UpdateDelete => {
738 if let Some(chunk) =
739 join_chunk_builder.forward_if_not_matched(Op::Delete, row)
740 {
741 yield chunk;
742 }
743 }
744 }
745 }
746 join_matched_join_keys.observe(join_matched_rows_cnt as _);
747 if join_matched_rows_cnt > high_join_amplification_threshold {
748 tracing::warn!(target: "high_join_amplification",
749 matched_rows_len = join_matched_rows_cnt,
750 update_table_id = %side_update.ht.table_id(),
751 match_table_id = %side_match.ht.table_id(),
752 join_key = ?join_key,
753 actor_id = %ctx.id,
754 fragment_id = %ctx.fragment_id,
755 "large rows matched for join key when AsOf join updating left side",
756 );
757 }
758 }
759 if let Some(chunk) = join_chunk_builder.take() {
760 yield chunk;
761 }
762 }
763
764 fn cmp_pk_rows(pk1: &impl Row, pk2: &impl Row) -> Ordering {
765 cmp_rows_ascending(pk1, pk2)
766 }
767
768 #[try_stream(ok = StreamChunk, error = StreamExecutorError)]
769 async fn eq_join_right(args: EqJoinArgs<'_, S, E>) {
770 let EqJoinArgs {
771 ctx,
772 side_l,
773 side_r,
774 null_safe,
775 asof_desc,
776 actual_output_data_types,
777 chunk,
778 chunk_size,
779 cnt_rows_received,
780 join_cache_evict_interval_rows,
781 high_join_amplification_threshold,
782 join_matched_join_keys,
783 } = args;
784
785 let (side_update, side_match) = (side_r, side_l);
786
787 let mut join_chunk_builder = JoinStreamChunkBuilder::new(
788 chunk_size,
789 actual_output_data_types.to_vec(),
790 side_update.i2o_mapping.clone(),
791 side_match.i2o_mapping.clone(),
792 );
793
794 let inequal_key_idx_update = [side_update.inequal_key_idx];
796
797 for r in chunk.rows_with_holes() {
798 let Some((op, row)) = r else {
799 continue;
800 };
801 Self::evict_cache(
802 side_update,
803 side_match,
804 cnt_rows_received,
805 join_cache_evict_interval_rows,
806 );
807
808 let join_key_null_not_safe = side_update
811 .join_key_indices
812 .iter()
813 .zip_eq(null_safe.iter())
814 .any(|(idx, ns)| !ns && row.datum_at(*idx).is_none());
815 if join_key_null_not_safe {
816 join_matched_join_keys.observe(0.0);
817 continue;
818 }
819
820 let join_key = row.project(&side_update.join_key_indices);
821
822 let inequal_key_is_null = side_update.ht.check_inequal_key_null(&row);
823 let inequal_key = row.project(&inequal_key_idx_update);
824
825 let mut join_matched_rows_cnt = 0;
826
827 if !inequal_key_is_null {
828 let (row_to_delete_r, row_to_insert_r) = {
829 let (first_row, second_row) = side_update
830 .ht
831 .first_two_by_inequality_with_jk_prefix(&join_key, &inequal_key)
832 .await?;
833 if let Some(first_row) = first_row {
834 let row_pk = side_update.ht.get_pk_from_row(row);
835 let first_pk = side_update.ht.get_pk_from_row(&first_row).to_owned_row();
836 match op {
837 Op::Insert | Op::UpdateInsert => {
838 if Self::cmp_pk_rows(&first_pk, &row_pk) == Ordering::Greater {
840 (Some(Either::Left(first_row)), Some(Either::Right(row)))
841 } else {
842 (None, None)
844 }
845 }
846 Op::Delete | Op::UpdateDelete => {
847 if Self::cmp_pk_rows(&first_pk, &row_pk) == Ordering::Equal {
848 if let Some(second_row) = second_row {
849 (Some(Either::Right(row)), Some(Either::Left(second_row)))
850 } else {
851 (Some(Either::Right(row)), None)
852 }
853 } else {
854 (None, None)
856 }
857 }
858 }
859 } else {
860 match op {
861 Op::Insert | Op::UpdateInsert => (None, Some(Either::Right(row))),
863 Op::Delete | Op::UpdateDelete => (Some(Either::Right(row)), None),
865 }
866 }
867 };
868 if row_to_delete_r.is_none() && row_to_insert_r.is_none() {
874 } else {
876 let prev_inequality_key = side_update
877 .ht
878 .upper_bound_by_inequality_with_jk_prefix(
879 &join_key,
880 Bound::Excluded(&inequal_key),
881 )
882 .await?
883 .map(|r| r.project(&inequal_key_idx_update));
884 let next_inequality_key = side_update
885 .ht
886 .lower_bound_by_inequality_with_jk_prefix(
887 &join_key,
888 Bound::Excluded(&inequal_key),
889 )
890 .await?
891 .map(|r| r.project(&inequal_key_idx_update));
892
893 let affected_inequality_key_r = match asof_desc.inequality_type {
894 AsOfInequalityType::Lt | AsOfInequalityType::Le => &next_inequality_key,
895 AsOfInequalityType::Gt | AsOfInequalityType::Ge => &prev_inequality_key,
896 };
897 let affected_row_r =
898 if let Some(affected_inequality_key_r) = affected_inequality_key_r {
899 side_update
900 .ht
901 .first_by_inequality_with_jk_prefix(
902 &join_key,
903 &affected_inequality_key_r,
904 )
905 .await?
906 } else {
907 None
908 }
909 .map(Either::Left);
910
911 let (row_to_delete_r, row_to_insert_r) =
912 match (&row_to_delete_r, &row_to_insert_r) {
913 (Some(_), Some(_)) => (row_to_delete_r, row_to_insert_r),
914 (None, Some(_)) => (affected_row_r, row_to_insert_r),
915 (Some(_), None) => (row_to_delete_r, affected_row_r),
916 (None, None) => unreachable!(),
917 };
918 let range = match asof_desc.inequality_type {
919 AsOfInequalityType::Lt => (
920 prev_inequality_key
921 .map(Either::Left)
922 .map_or_else(|| Bound::Unbounded, Bound::Included),
923 Bound::Excluded(Either::Right(&inequal_key)),
924 ),
925 AsOfInequalityType::Le => (
926 prev_inequality_key
927 .map(Either::Left)
928 .map_or_else(|| Bound::Unbounded, Bound::Excluded),
929 Bound::Included(Either::Right(&inequal_key)),
930 ),
931 AsOfInequalityType::Gt => (
932 Bound::Excluded(Either::Right(&inequal_key)),
933 next_inequality_key
934 .map(Either::Left)
935 .map_or_else(|| Bound::Unbounded, Bound::Included),
936 ),
937 AsOfInequalityType::Ge => (
938 Bound::Included(Either::Right(&inequal_key)),
939 next_inequality_key
940 .map(Either::Left)
941 .map_or_else(|| Bound::Unbounded, Bound::Excluded),
942 ),
943 };
944
945 let rows_l_stream = side_match
946 .ht
947 .range_by_inequality_with_jk_prefix(&join_key, &range)
948 .await?;
949 #[for_await]
950 for row_l in rows_l_stream {
951 let row_l = row_l?;
952 join_matched_rows_cnt += 1;
953 if let Some(row_to_delete_r) = &row_to_delete_r {
954 if let Some(chunk) =
955 join_chunk_builder.append_row(Op::Delete, row_to_delete_r, &row_l)
956 {
957 yield chunk;
958 }
959 } else if is_as_of_left_outer(T)
960 && let Some(chunk) =
961 join_chunk_builder.append_row_matched(Op::Delete, &row_l)
962 {
963 yield chunk;
964 }
965 if let Some(row_to_insert_r) = &row_to_insert_r {
966 if let Some(chunk) =
967 join_chunk_builder.append_row(Op::Insert, row_to_insert_r, &row_l)
968 {
969 yield chunk;
970 }
971 } else if is_as_of_left_outer(T)
972 && let Some(chunk) =
973 join_chunk_builder.append_row_matched(Op::Insert, &row_l)
974 {
975 yield chunk;
976 }
977 }
978 }
979
980 match op {
981 Op::Insert | Op::UpdateInsert => {
982 side_update.ht.insert(row)?;
983 }
984 Op::Delete | Op::UpdateDelete => {
985 side_update.ht.delete(row)?;
986 }
987 }
988 } else {
989 }
992 join_matched_join_keys.observe(join_matched_rows_cnt as _);
993 if join_matched_rows_cnt > high_join_amplification_threshold {
994 let join_key = row.project(&side_update.join_key_indices);
995 tracing::warn!(target: "high_join_amplification",
996 matched_rows_len = join_matched_rows_cnt,
997 update_table_id = %side_update.ht.table_id(),
998 match_table_id = %side_match.ht.table_id(),
999 join_key = ?join_key,
1000 actor_id = %ctx.id,
1001 fragment_id = %ctx.fragment_id,
1002 "large rows matched for join key when AsOf join updating right side",
1003 );
1004 }
1005 }
1006 if let Some(chunk) = join_chunk_builder.take() {
1007 yield chunk;
1008 }
1009 }
1010}
1011
1012#[cfg(test)]
1013mod tests {
1014 use std::sync::atomic::AtomicU64;
1015
1016 use futures::TryStreamExt;
1017 use prometheus::Registry;
1018 use risingwave_common::array::*;
1019 use risingwave_common::catalog::{ColumnDesc, ColumnId, Field, TableId};
1020 use risingwave_common::config::MetricLevel;
1021 use risingwave_common::metrics::get_label;
1022 use risingwave_common::row;
1023 use risingwave_common::util::epoch::test_epoch;
1024 use risingwave_common::util::sort_util::OrderType;
1025 use risingwave_storage::memory::MemoryStateStore;
1026 use risingwave_storage::store::PrefetchOptions;
1027
1028 use super::*;
1029 use crate::common::table::test_utils::gen_pbtable;
1030 use crate::executor::test_utils::{MessageSender, MockSource, StreamExecutorTestExt};
1031
1032 async fn create_in_memory_state_table(
1033 mem_state: MemoryStateStore,
1034 data_types: &[DataType],
1035 order_types: &[OrderType],
1036 pk_indices: &[usize],
1037 table_id: u32,
1038 ) -> StateTable<MemoryStateStore> {
1039 let column_descs = data_types
1040 .iter()
1041 .enumerate()
1042 .map(|(id, data_type)| ColumnDesc::unnamed(ColumnId::new(id as i32), data_type.clone()))
1043 .collect_vec();
1044 StateTable::from_table_catalog(
1045 &gen_pbtable(
1046 TableId::new(table_id),
1047 column_descs,
1048 order_types.to_vec(),
1049 pk_indices.to_vec(),
1050 0,
1051 ),
1052 mem_state.clone(),
1053 None,
1054 )
1055 .await
1056 }
1057
1058 async fn create_executor<const T: AsOfJoinTypePrimitive>(
1059 asof_desc: AsOfDesc,
1060 use_cache: bool,
1061 ) -> (MessageSender, MessageSender, BoxedMessageStream) {
1062 create_executor_with_metrics::<T>(
1063 asof_desc,
1064 use_cache,
1065 Arc::new(StreamingMetrics::unused()),
1066 )
1067 .await
1068 }
1069
1070 async fn create_executor_with_metrics<const T: AsOfJoinTypePrimitive>(
1071 asof_desc: AsOfDesc,
1072 use_cache: bool,
1073 metrics: Arc<StreamingMetrics>,
1074 ) -> (MessageSender, MessageSender, BoxedMessageStream) {
1075 create_executor_with_store::<T>(asof_desc, use_cache, metrics, MemoryStateStore::new())
1076 .await
1077 }
1078
1079 async fn create_side_state_table(
1081 mem_state: MemoryStateStore,
1082 inequality_idx: usize,
1083 table_id: u32,
1084 ) -> StateTable<MemoryStateStore> {
1085 create_in_memory_state_table(
1086 mem_state,
1087 &[DataType::Int64, DataType::Int64, DataType::Int64],
1088 &[
1089 OrderType::ascending(),
1090 OrderType::ascending(),
1091 OrderType::ascending(),
1092 ],
1093 &[0, inequality_idx, 1],
1094 table_id,
1095 )
1096 .await
1097 }
1098
1099 async fn create_executor_with_store<const T: AsOfJoinTypePrimitive>(
1100 asof_desc: AsOfDesc,
1101 use_cache: bool,
1102 metrics: Arc<StreamingMetrics>,
1103 mem_state: MemoryStateStore,
1104 ) -> (MessageSender, MessageSender, BoxedMessageStream) {
1105 let schema = Schema {
1106 fields: vec![
1107 Field::unnamed(DataType::Int64), Field::unnamed(DataType::Int64),
1109 Field::unnamed(DataType::Int64),
1110 ],
1111 };
1112 let (tx_l, source_l) = MockSource::channel();
1113 let source_l = source_l.into_executor(schema.clone(), vec![1]);
1114 let (tx_r, source_r) = MockSource::channel();
1115 let source_r = source_r.into_executor(schema, vec![1]);
1116 let params_l = JoinParams::new(vec![0], vec![1]);
1117 let params_r = JoinParams::new(vec![0], vec![1]);
1118
1119 let state_l = create_side_state_table(mem_state.clone(), asof_desc.left_idx, 0).await;
1120 let state_r = create_side_state_table(mem_state, asof_desc.right_idx, 1).await;
1121
1122 let schema: Schema = [source_l.schema().fields(), source_r.schema().fields()]
1123 .concat()
1124 .into_iter()
1125 .collect();
1126 let schema_len = schema.len();
1127 let info = ExecutorInfo::for_test(schema, vec![1], "AsOfJoinExecutor".to_owned(), 0);
1128
1129 let executor = AsOfJoinExecutor::<MemoryStateStore, T, AsOfCpuEncoding>::new(
1130 ActorContext::for_test(123),
1131 info,
1132 source_l,
1133 source_r,
1134 params_l,
1135 params_r,
1136 vec![false],
1137 (0..schema_len).collect_vec(),
1138 state_l,
1139 state_r,
1140 Arc::new(AtomicU64::new(0)),
1141 metrics,
1142 1024,
1143 asof_desc,
1144 use_cache,
1145 2048, );
1147 (tx_l, tx_r, executor.boxed().execute())
1148 }
1149
1150 fn join_matched_sample_count(registry: &Registry, table_id: &str) -> u64 {
1151 registry
1152 .gather()
1153 .iter()
1154 .find(|metric_family| metric_family.name() == "stream_join_matched_join_keys")
1155 .and_then(|metric_family| {
1156 metric_family.get_metric().iter().find(|metric| {
1157 get_label::<String>(metric, "table_id").as_deref() == Some(table_id)
1158 })
1159 })
1160 .map(|metric| metric.get_histogram().get_sample_count())
1161 .unwrap_or(0)
1162 }
1163
1164 #[tokio::test]
1165 async fn test_asof_join_records_null_equi_key_probe_metrics() -> StreamExecutorResult<()> {
1166 let asof_desc = AsOfDesc {
1167 left_idx: 1,
1168 right_idx: 1,
1169 inequality_type: AsOfInequalityType::Lt,
1170 };
1171 let registry = Registry::new();
1172 let metrics = Arc::new(StreamingMetrics::new(®istry, MetricLevel::Debug));
1173 let (mut tx_l, mut tx_r, mut hash_join) =
1174 create_executor_with_metrics::<{ AsOfJoinType::Inner }>(asof_desc, false, metrics)
1175 .await;
1176
1177 tx_l.push_barrier(test_epoch(1), false);
1178 tx_r.push_barrier(test_epoch(1), false);
1179 hash_join.next_unwrap_ready_barrier()?;
1180
1181 tx_l.push_chunk(StreamChunk::from_pretty(
1182 " I I I
1183 + . 10 1",
1184 ));
1185 hash_join.next_unwrap_pending();
1186
1187 tx_r.push_chunk(StreamChunk::from_pretty(
1188 " I I I
1189 + . 20 2",
1190 ));
1191 hash_join.next_unwrap_pending();
1192
1193 assert_eq!(join_matched_sample_count(®istry, "0"), 1);
1194 assert_eq!(join_matched_sample_count(®istry, "1"), 1);
1195
1196 Ok(())
1197 }
1198
1199 #[tokio::test]
1200 async fn test_asof_inner_join() -> StreamExecutorResult<()> {
1201 test_asof_inner_join_impl(false).await
1202 }
1203
1204 #[tokio::test]
1205 async fn test_asof_inner_join_with_cache() -> StreamExecutorResult<()> {
1206 test_asof_inner_join_impl(true).await
1207 }
1208
1209 async fn test_asof_inner_join_impl(use_cache: bool) -> StreamExecutorResult<()> {
1210 let asof_desc = AsOfDesc {
1211 left_idx: 0,
1212 right_idx: 2,
1213 inequality_type: AsOfInequalityType::Lt,
1214 };
1215
1216 let chunk_l1 = StreamChunk::from_pretty(
1217 " I I I
1218 + 1 4 7
1219 + 2 5 8
1220 + 3 6 9",
1221 );
1222 let chunk_l2 = StreamChunk::from_pretty(
1223 " I I I
1224 + 3 8 1
1225 - 3 8 1",
1226 );
1227 let chunk_r1 = StreamChunk::from_pretty(
1228 " I I I
1229 + 2 1 7
1230 + 2 2 1
1231 + 2 3 4
1232 + 2 4 2
1233 + 6 1 9
1234 + 6 2 9",
1235 );
1236 let chunk_r2 = StreamChunk::from_pretty(
1237 " I I I
1238 - 2 3 4",
1239 );
1240 let chunk_r3 = StreamChunk::from_pretty(
1241 " I I I
1242 + 2 3 3",
1243 );
1244 let chunk_l3 = StreamChunk::from_pretty(
1245 " I I I
1246 - 2 5 8",
1247 );
1248 let chunk_l4 = StreamChunk::from_pretty(
1249 " I I I
1250 + 6 3 1
1251 + 6 4 1",
1252 );
1253 let chunk_r4 = StreamChunk::from_pretty(
1254 " I I I
1255 - 6 1 9",
1256 );
1257
1258 let (mut tx_l, mut tx_r, mut hash_join) =
1259 create_executor::<{ AsOfJoinType::Inner }>(asof_desc, use_cache).await;
1260
1261 tx_l.push_barrier(test_epoch(1), false);
1263 tx_r.push_barrier(test_epoch(1), false);
1264 hash_join.next_unwrap_ready_barrier()?;
1265
1266 tx_l.push_chunk(chunk_l1);
1268 hash_join.next_unwrap_pending();
1269
1270 tx_l.push_barrier(test_epoch(2), false);
1272 tx_r.push_barrier(test_epoch(2), false);
1273 hash_join.next_unwrap_ready_barrier()?;
1274
1275 tx_l.push_chunk(chunk_l2);
1277 hash_join.next_unwrap_pending();
1278
1279 tx_r.push_chunk(chunk_r1);
1281 let chunk = hash_join.next_unwrap_ready_chunk()?;
1282 assert_eq!(
1283 chunk,
1284 StreamChunk::from_pretty(
1285 " I I I I I I
1286 + 2 5 8 2 1 7
1287 - 2 5 8 2 1 7
1288 + 2 5 8 2 3 4"
1289 )
1290 );
1291
1292 tx_r.push_chunk(chunk_r2);
1294 let chunk = hash_join.next_unwrap_ready_chunk()?;
1295 assert_eq!(
1296 chunk,
1297 StreamChunk::from_pretty(
1298 " I I I I I I
1299 - 2 5 8 2 3 4
1300 + 2 5 8 2 1 7"
1301 )
1302 );
1303
1304 tx_r.push_chunk(chunk_r3);
1306 let chunk = hash_join.next_unwrap_ready_chunk()?;
1307 assert_eq!(
1308 chunk,
1309 StreamChunk::from_pretty(
1310 " I I I I I I
1311 - 2 5 8 2 1 7
1312 + 2 5 8 2 3 3"
1313 )
1314 );
1315
1316 tx_l.push_chunk(chunk_l3);
1318 let chunk = hash_join.next_unwrap_ready_chunk()?;
1319 assert_eq!(
1320 chunk,
1321 StreamChunk::from_pretty(
1322 " I I I I I I
1323 - 2 5 8 2 3 3"
1324 )
1325 );
1326
1327 tx_l.push_chunk(chunk_l4);
1329 let chunk = hash_join.next_unwrap_ready_chunk()?;
1330 assert_eq!(
1331 chunk,
1332 StreamChunk::from_pretty(
1333 " I I I I I I
1334 + 6 3 1 6 1 9
1335 + 6 4 1 6 1 9"
1336 )
1337 );
1338
1339 tx_r.push_chunk(chunk_r4);
1341 let chunk = hash_join.next_unwrap_ready_chunk()?;
1342 assert_eq!(
1343 chunk,
1344 StreamChunk::from_pretty(
1345 " I I I I I I
1346 - 6 3 1 6 1 9
1347 + 6 3 1 6 2 9
1348 - 6 4 1 6 1 9
1349 + 6 4 1 6 2 9"
1350 )
1351 );
1352
1353 Ok(())
1354 }
1355
1356 #[tokio::test]
1357 async fn test_asof_left_outer_join() -> StreamExecutorResult<()> {
1358 test_asof_left_outer_join_impl(false).await
1359 }
1360
1361 #[tokio::test]
1362 async fn test_asof_left_outer_join_with_cache() -> StreamExecutorResult<()> {
1363 test_asof_left_outer_join_impl(true).await
1364 }
1365
1366 async fn test_asof_left_outer_join_impl(use_cache: bool) -> StreamExecutorResult<()> {
1367 let asof_desc = AsOfDesc {
1368 left_idx: 1,
1369 right_idx: 2,
1370 inequality_type: AsOfInequalityType::Ge,
1371 };
1372
1373 let chunk_l1 = StreamChunk::from_pretty(
1374 " I I I
1375 + 1 4 7
1376 + 2 5 8
1377 + 3 6 9",
1378 );
1379 let chunk_l2 = StreamChunk::from_pretty(
1380 " I I I
1381 + 3 8 1
1382 - 3 8 1",
1383 );
1384 let chunk_r1 = StreamChunk::from_pretty(
1385 " I I I
1386 + 2 3 4
1387 + 2 2 5
1388 + 2 1 5
1389 + 6 1 8
1390 + 6 2 9",
1391 );
1392 let chunk_r2 = StreamChunk::from_pretty(
1393 " I I I
1394 - 2 3 4
1395 - 2 1 5
1396 - 2 2 5",
1397 );
1398 let chunk_l3 = StreamChunk::from_pretty(
1399 " I I I
1400 + 6 8 9",
1401 );
1402 let chunk_r3 = StreamChunk::from_pretty(
1403 " I I I
1404 - 6 1 8",
1405 );
1406
1407 let (mut tx_l, mut tx_r, mut hash_join) =
1408 create_executor::<{ AsOfJoinType::LeftOuter }>(asof_desc, use_cache).await;
1409
1410 tx_l.push_barrier(test_epoch(1), false);
1412 tx_r.push_barrier(test_epoch(1), false);
1413 hash_join.next_unwrap_ready_barrier()?;
1414
1415 tx_l.push_chunk(chunk_l1);
1417 let chunk = hash_join.next_unwrap_ready_chunk()?;
1418 assert_eq!(
1419 chunk,
1420 StreamChunk::from_pretty(
1421 " I I I I I I
1422 + 1 4 7 . . .
1423 + 2 5 8 . . .
1424 + 3 6 9 . . ."
1425 )
1426 );
1427
1428 tx_l.push_barrier(test_epoch(2), false);
1430 tx_r.push_barrier(test_epoch(2), false);
1431 hash_join.next_unwrap_ready_barrier()?;
1432
1433 tx_l.push_chunk(chunk_l2);
1435 let chunk = hash_join.next_unwrap_ready_chunk()?;
1436 assert_eq!(
1437 chunk,
1438 StreamChunk::from_pretty(
1439 " I I I I I I
1440 + 3 8 1 . . . D
1441 - 3 8 1 . . . D"
1442 )
1443 );
1444
1445 tx_r.push_chunk(chunk_r1);
1447 let chunk = hash_join.next_unwrap_ready_chunk()?;
1448 assert_eq!(
1449 chunk,
1450 StreamChunk::from_pretty(
1451 " I I I I I I
1452 - 2 5 8 . . .
1453 + 2 5 8 2 3 4
1454 - 2 5 8 2 3 4
1455 + 2 5 8 2 2 5
1456 - 2 5 8 2 2 5
1457 + 2 5 8 2 1 5"
1458 )
1459 );
1460
1461 tx_r.push_chunk(chunk_r2);
1463 let chunk = hash_join.next_unwrap_ready_chunk()?;
1464 assert_eq!(
1465 chunk,
1466 StreamChunk::from_pretty(
1467 " I I I I I I
1468 - 2 5 8 2 1 5
1469 + 2 5 8 2 2 5
1470 - 2 5 8 2 2 5
1471 + 2 5 8 . . ."
1472 )
1473 );
1474
1475 tx_l.push_chunk(chunk_l3);
1477 let chunk = hash_join.next_unwrap_ready_chunk()?;
1478 assert_eq!(
1479 chunk,
1480 StreamChunk::from_pretty(
1481 " I I I I I I
1482 + 6 8 9 6 1 8"
1483 )
1484 );
1485
1486 tx_r.push_chunk(chunk_r3);
1488 let chunk = hash_join.next_unwrap_ready_chunk()?;
1489 assert_eq!(
1490 chunk,
1491 StreamChunk::from_pretty(
1492 " I I I I I I
1493 - 6 8 9 6 1 8
1494 + 6 8 9 . . ."
1495 )
1496 );
1497 Ok(())
1498 }
1499
1500 #[tokio::test]
1501 async fn test_asof_join_state_cleaning() -> StreamExecutorResult<()> {
1502 for use_cache in [false, true] {
1503 let store = MemoryStateStore::new();
1504 let create_executor = || {
1506 create_executor_with_store::<{ AsOfJoinType::Inner }>(
1507 AsOfDesc {
1508 left_idx: 1,
1509 right_idx: 2,
1510 inequality_type: AsOfInequalityType::Ge,
1511 },
1512 use_cache,
1513 Arc::new(StreamingMetrics::unused()),
1514 store.clone(),
1515 )
1516 };
1517 let (mut tx_l, mut tx_r, mut join) = create_executor().await;
1518 tx_l.push_barrier(test_epoch(1), false);
1519 tx_r.push_barrier(test_epoch(1), false);
1520 join.next_unwrap_ready_barrier()?;
1521 tx_r.push_chunk(StreamChunk::from_pretty(" I I I\n + 5 1 50"));
1522 join.next_unwrap_pending();
1523 tx_l.push_chunk(StreamChunk::from_pretty(" I I I\n + 5 100 0"));
1524 assert_eq!(
1525 join.next_unwrap_ready_chunk()?,
1526 StreamChunk::from_pretty(" I I I I I I\n + 5 100 0 5 1 50")
1527 );
1528
1529 tx_l.push_int64_watermark(0, 10);
1530 tx_l.push_barrier(test_epoch(2), false);
1531 tx_r.push_barrier(test_epoch(2), false);
1532 join.next_unwrap_ready_barrier()?;
1533 if use_cache {
1534 (tx_l, tx_r, join) = create_executor().await;
1537 tx_l.push_barrier(test_epoch(2), false);
1538 tx_r.push_barrier(test_epoch(2), false);
1539 join.next_unwrap_ready_barrier()?;
1540 tx_l.push_int64_watermark(0, 10);
1541 }
1542
1543 tx_r.push_chunk(StreamChunk::from_pretty(" I I I\n + 5 2 60"));
1545 assert_eq!(
1546 join.next_unwrap_ready_chunk()?,
1547 StreamChunk::from_pretty(
1548 " I I I I I I
1549 - 5 100 0 5 1 50
1550 + 5 100 0 5 2 60"
1551 ),
1552 "use_cache: {use_cache}"
1553 );
1554
1555 tx_r.push_chunk(StreamChunk::from_pretty(" I I I\n + 15 3 50"));
1557 join.next_unwrap_pending();
1558 tx_l.push_chunk(StreamChunk::from_pretty(" I I I\n + 15 101 0"));
1559 assert_eq!(
1560 join.next_unwrap_ready_chunk()?,
1561 StreamChunk::from_pretty(" I I I I I I\n + 15 101 0 15 3 50")
1562 );
1563 tx_r.push_int64_watermark(0, 20);
1564 for _ in 0..2 {
1565 assert_eq!(
1566 join.next_unwrap_ready_watermark()?.val,
1567 ScalarImpl::Int64(10)
1568 );
1569 }
1570 tx_l.push_barrier(test_epoch(3), false);
1571 tx_r.push_barrier(test_epoch(3), false);
1572 join.next_unwrap_ready_barrier()?;
1573 tx_l.push_chunk(StreamChunk::from_pretty(" I I I\n - 15 101 0"));
1574 assert_eq!(
1575 join.next_unwrap_ready_chunk()?,
1576 StreamChunk::from_pretty(" I I I I I I\n - 15 101 0 15 3 50")
1577 );
1578 tx_l.push_barrier(test_epoch(4), false);
1579 tx_r.push_barrier(test_epoch(4), false);
1580 join.next_unwrap_ready_barrier()?;
1581
1582 tx_l.push_int64_watermark(0, 20);
1584 for _ in 0..2 {
1585 assert_eq!(
1586 join.next_unwrap_ready_watermark()?.val,
1587 ScalarImpl::Int64(20)
1588 );
1589 }
1590 tx_l.push_barrier(test_epoch(5), false);
1591 tx_r.push_barrier(test_epoch(5), false);
1592 join.next_unwrap_ready_barrier()?;
1593 for (inequality_idx, table_id) in [(1, 0), (2, 1)] {
1594 let mut table =
1595 create_side_state_table(store.clone(), inequality_idx, table_id).await;
1596 table
1597 .init_epoch(EpochPair::new_test_epoch(test_epoch(5)))
1598 .await?;
1599 let sub_range: &(Bound<OwnedRow>, Bound<OwnedRow>) =
1600 &(Bound::Unbounded, Bound::Unbounded);
1601 let rows: Vec<OwnedRow> = table
1602 .iter_with_prefix(row::empty(), sub_range, PrefetchOptions::default())
1603 .await?
1604 .try_collect()
1605 .await?;
1606 assert!(rows.is_empty(), "use_cache: {use_cache}, table: {table_id}");
1607 }
1608 }
1609 Ok(())
1610 }
1611}