1use std::cmp::min;
16use std::path::Path;
17use std::sync::Arc;
18use std::time::Duration;
19
20use anyhow::Result;
21use rand::{RngExt as _, rng as thread_rng};
22use sqllogictest::substitution::well_known;
23use sqllogictest::{
24 Condition, ParallelTestError, Partitioner, QueryExpect, Record, StatementExpect,
25};
26
27use crate::client::RisingWave;
28use crate::cluster::Cluster;
29use crate::parse::extract_sql_command;
30use crate::slt::background_ddl_mode::*;
31use crate::slt::runner::{random_vnode_count, run_kill_not_allowed, run_no_kill};
32use crate::slt::slt_env::{Env, Opts};
33use crate::slt::vnode_mode::*;
34use crate::utils::TimedExt;
35use crate::{evaluate_skip, wait_or_retry_background_ddl};
36
37const MAX_RETRY: usize = 10;
39
40#[derive(Debug, PartialEq, Eq)]
41pub enum SqlCmd {
42 Create {
44 is_create_table_as: bool,
45 },
46 CreateMaterializedView {
48 name: String,
49 },
50 SetBackgroundDdl {
52 enable: bool,
53 },
54 Drop,
55 Dml,
56 Flush,
57 Others,
58}
59
60impl SqlCmd {
61 fn allow_kill(&self) -> bool {
62 matches!(
63 self,
64 SqlCmd::Create {
65 is_create_table_as: false,
67 ..
68 } | SqlCmd::CreateMaterializedView { .. }
69 | SqlCmd::Drop
70 )
71 }
76
77 fn is_create(&self) -> bool {
78 matches!(
79 self,
80 SqlCmd::Create { .. } | SqlCmd::CreateMaterializedView { .. }
81 )
82 }
83}
84
85const KILL_IGNORE_FILES: &[&str] = &[
86 "tpch_snapshot.slt",
88 "tpch_upstream.slt",
89 "search_path.slt",
91 "alter_mv/alter_mv_concurrency_check.slt",
93 "transaction/now.slt",
95 "transaction/read_only_multi_conn.slt",
96 "transaction/read_only.slt",
97 "transaction/tolerance.slt",
98 "transaction/cursor.slt",
99 "transaction/cursor_multi_conn.slt",
100];
101
102mod background_ddl_mode {
104 use anyhow::bail;
105 use rand::RngExt as _;
106 use rand::rngs::ChaCha20Rng;
107 use sqllogictest::{Condition, Record, StatementExpect};
108
109 use crate::client::RisingWave;
110 use crate::slt::SqlCmd;
111 use crate::slt::slt_env::Env;
112
113 pub(super) async fn wait_background_mv_finished(mview_name: &str) -> anyhow::Result<()> {
115 let Ok(rw) = RisingWave::connect("frontend".into(), "dev".into()).await else {
116 bail!("failed to connect to frontend for {mview_name}");
117 };
118 let client = rw.pg_client();
119 if client.simple_query("WAIT;").await.is_err() {
120 bail!("failed to wait for background mv to finish creating for {mview_name}");
121 }
122
123 let Ok(result) = client
124 .query(
125 "select count(*) from pg_matviews where matviewname=$1;",
126 &[&mview_name],
127 )
128 .await
129 else {
130 bail!("failed to query pg_matviews for {mview_name}");
131 };
132
133 match result[0].try_get::<_, i64>(0) {
134 Ok(1) => Ok(()),
135 r => bail!("expected 1 row in pg_matviews, got {r:#?} instead for {mview_name}"),
136 }
137 }
138
139 pub(super) async fn set_background_ddl<D, M, T>(
141 tester: &mut sqllogictest::Runner<D, M>,
142 env: &Env,
143 record: &Record<T>,
144 cmd: &SqlCmd,
145 manual_background_ddl_enabled: bool,
146 rng: &mut ChaCha20Rng,
147 background_ddl_enabled: &mut bool,
148 ) where
149 D: sqllogictest::AsyncDB<ColumnType = T>,
150 M: sqllogictest::MakeConnection<Conn = D>,
151 T: sqllogictest::ColumnType,
152 {
153 if let Record::Statement {
154 loc,
155 conditions,
156 connection,
157 ..
158 } = &record
159 && matches!(cmd, SqlCmd::CreateMaterializedView { .. })
160 && !manual_background_ddl_enabled
161 && conditions.iter().all(|c| {
162 *c != Condition::SkipIf {
163 label: "madsim".to_owned(),
164 }
165 })
166 && env.background_ddl_rate() > 0.0
167 {
168 let background_ddl_setting = rng.random_bool(env.background_ddl_rate());
169 let set_background_ddl = Record::Statement {
170 loc: loc.clone(),
171 conditions: conditions.clone(),
172 connection: connection.clone(),
173 expected: StatementExpect::Ok,
174 sql: format!("SET BACKGROUND_DDL={background_ddl_setting};"),
175 retry: None,
176 };
177 tester.run_async(set_background_ddl).await.unwrap();
178 *background_ddl_enabled = background_ddl_setting;
179 };
180 }
181
182 #[macro_export]
184 macro_rules! wait_or_retry_background_ddl {
185 ($record:expr, $name:expr, $iteration:expr) => {
186 let record = $record;
187 let i = $iteration;
188 tracing::debug!(iteration = i, "Retry for background ddl");
189 match wait_background_mv_finished($name).await {
190 Ok(_) => {
191 tracing::debug!(
192 iteration = i,
193 "Record with background_ddl {:?} finished",
194 record
195 );
196 break;
197 }
198 Err(err) => {
199 tracing::error!(
200 iteration = i,
201 ?err,
202 "failed to wait for background mv to finish creating"
203 );
204 if i >= MAX_RETRY {
205 panic!("failed to run test after retry {i} times, error={err:#?}");
206 }
207 continue;
208 }
209 }
210 };
211 }
212}
213
214mod vnode_mode {
215 use std::env;
216 use std::hash::{DefaultHasher, Hash, Hasher};
217 use std::sync::LazyLock;
218
219 use anyhow::bail;
220 use sqllogictest::Partitioner;
221
222 #[derive(Clone)]
224 pub(super) struct HashPartitioner {
225 count: u64,
226 id: u64,
227 }
228
229 impl HashPartitioner {
230 pub(super) fn new(count: u64, id: u64) -> anyhow::Result<Self> {
231 if count == 0 {
232 bail!("partition count must be greater than zero");
233 }
234 if id >= count {
235 bail!("partition id (zero-based) must be less than count");
236 }
237 Ok(Self { count, id })
238 }
239 }
240
241 impl Partitioner for HashPartitioner {
242 fn matches(&self, file_name: &str) -> bool {
243 let mut hasher = DefaultHasher::new();
244 file_name.hash(&mut hasher);
245 hasher.finish() % self.count == self.id
246 }
247 }
248
249 pub(super) static PARTITIONER: LazyLock<Option<HashPartitioner>> = LazyLock::new(|| {
250 let count = env::var("BUILDKITE_PARALLEL_JOB_COUNT")
251 .ok()?
252 .parse::<u64>()
253 .unwrap();
254 let id = env::var("BUILDKITE_PARALLEL_JOB")
255 .ok()?
256 .parse::<u64>()
257 .unwrap();
258 Some(HashPartitioner::new(count, id).unwrap())
259 });
260}
261
262pub mod slt_env {
263 use rand::SeedableRng;
264 use rand::rngs::ChaCha20Rng;
265
266 use crate::cluster::KillOpts;
267
268 pub struct Opts {
269 pub kill_opts: KillOpts,
270 pub background_ddl_rate: f64,
272 pub random_vnode_count: bool,
274 }
275
276 pub(super) struct Env {
277 opts: Opts,
278 }
279
280 impl Env {
281 pub fn new(opts: Opts) -> Self {
282 Self { opts }
283 }
284
285 pub fn get_rng() -> ChaCha20Rng {
286 let seed = std::env::var("MADSIM_TEST_SEED")
287 .unwrap_or("0".to_owned())
288 .parse::<u64>()
289 .unwrap();
290 ChaCha20Rng::seed_from_u64(seed)
291 }
292
293 pub fn background_ddl_rate(&self) -> f64 {
294 self.opts.background_ddl_rate
295 }
296
297 pub fn kill(&self) -> bool {
298 self.opts.kill_opts.kill_compute
299 || self.opts.kill_opts.kill_meta
300 || self.opts.kill_opts.kill_frontend
301 || self.opts.kill_opts.kill_compactor
302 }
303
304 pub fn random_vnode_count(&self) -> bool {
305 self.opts.random_vnode_count
306 }
307
308 pub fn kill_opts(&self) -> KillOpts {
309 self.opts.kill_opts
310 }
311
312 pub fn kill_rate(&self) -> f64 {
313 self.opts.kill_opts.kill_rate as f64
314 }
315 }
316}
317
318mod runner {
319 use std::time::Duration;
320
321 use rand::prelude::IteratorRandom;
322 use rand::rng as thread_rng;
323 use sqllogictest::{Record, StatementExpect, TestError};
324
325 use crate::slt::slt_env::Env;
326 use crate::slt::{MAX_RETRY, SqlCmd};
327 use crate::utils::TimedExt;
328 #[macro_export]
329 macro_rules! evaluate_skip {
330 ($env:expr, $path:expr) => {
331 if let Some(partitioner) = PARTITIONER.as_ref()
332 && !partitioner.matches($path.to_str().unwrap())
333 {
334 println!("[skip partition] {}", $path.display());
335 continue;
336 } else if $env.kill() && KILL_IGNORE_FILES.iter().any(|s| $path.ends_with(s)) {
337 println!("[skip kill] {}", $path.display());
338 continue;
339 } else {
340 println!("[run] {}", $path.display());
341 }
342 };
343 }
344
345 pub(super) fn random_vnode_count<T: sqllogictest::ColumnType>(
346 env: &Env,
347 records: &[Record<T>],
348 ) -> bool {
349 env.random_vnode_count()
350 && records.iter().all(|record| {
351 if let Record::Statement { sql, .. } | Record::Query { sql, .. } = record
352 && sql.to_lowercase().contains("parallelism")
353 {
354 println!("[RANDOM VNODE COUNT] skip: {}", sql);
355 false
356 } else {
357 true
358 }
359 })
360 }
361
362 pub(super) async fn run_no_kill<D, M, T>(
364 tester: &mut sqllogictest::Runner<D, M>,
365 random_vnode_count: bool,
366 cmd: &SqlCmd,
367 record: Record<T>,
368 ) -> Result<(), TestError>
369 where
370 D: sqllogictest::AsyncDB<ColumnType = T>,
371 M: sqllogictest::MakeConnection<Conn = D>,
372 T: sqllogictest::ColumnType,
373 {
374 if random_vnode_count
376 && cmd.is_create()
377 && let Record::Statement {
378 loc,
379 conditions,
380 connection,
381 ..
382 } = &record
383 {
384 let vnode_count = (2..=64) .chain(224..=288) .chain(992..=1056) .choose(&mut thread_rng())
388 .unwrap();
389 let sql = format!("SET STREAMING_MAX_PARALLELISM = {vnode_count};");
390 println!("[RANDOM VNODE COUNT] set: {vnode_count}");
391 let set_random_vnode_count = Record::Statement {
392 loc: loc.clone(),
393 conditions: conditions.clone(),
394 connection: connection.clone(),
395 sql,
396 expected: StatementExpect::Ok,
397 retry: None,
398 };
399 tester.run_async(set_random_vnode_count).await.unwrap();
400 println!("[RANDOM VNODE COUNT] run: {record}");
401 }
402
403 tester.run_async(record).await.map(|_| ())
404 }
405
406 pub(super) async fn run_kill_not_allowed<D, M, T>(
408 tester: &mut sqllogictest::Runner<D, M>,
409 record: Record<T>,
410 ) where
411 D: sqllogictest::AsyncDB<ColumnType = T>,
412 M: sqllogictest::MakeConnection<Conn = D>,
413 T: sqllogictest::ColumnType,
414 {
415 for i in 0usize.. {
416 let delay = Duration::from_secs(1 << i);
417 if let Err(err) = tester
418 .run_async(record.clone())
419 .timed(|_res, elapsed| {
420 tracing::debug!("Record {:?} finished in {:?}", record, elapsed)
421 })
422 .await
423 {
424 let err_string = err.to_string().to_ascii_lowercase();
425 let allowed_errs = [
428 "no reader for dml in table",
429 "error reading a body from connection: broken pipe",
430 "failed to inject barrier",
431 "get error from control stream",
432 "cluster is under recovering",
433 "streaming vnode mapping has not been initialized",
434 "streaming vnode mapping not found",
435 ];
436 let should_retry = i < MAX_RETRY
437 && allowed_errs
438 .iter()
439 .any(|allowed_err| err_string.contains(&allowed_err.to_ascii_lowercase()));
440 if !should_retry {
441 panic!("{}", err);
442 }
443 tracing::error!("failed to run test: {err}\nretry after {delay:?}");
444 } else {
445 break;
446 }
447 tokio::time::sleep(delay).await;
448 }
449 }
450}
451
452pub async fn run_slt_task(cluster: Arc<Cluster>, glob: &str, opts: Opts) {
454 let env = slt_env::Env::new(opts);
455 tracing::info!("background_ddl_rate: {}", env.background_ddl_rate());
456 let mut rng = Env::get_rng();
457 let files = glob::glob(glob).expect("failed to read glob pattern");
458 for file in files {
459 let mut tester =
461 sqllogictest::Runner::new(|| RisingWave::connect("frontend".into(), "dev".into()));
462 tester.add_label("madsim");
463 tester.set_var(well_known::DATABASE.to_owned(), "dev".to_owned());
464
465 let file = file.unwrap();
466 let path = file.as_path();
467
468 evaluate_skip!(env, path);
469
470 let tempfile = (path.ends_with("kafka.slt") || path.ends_with("kafka_batch.slt"))
472 .then(|| hack_kafka_test(path));
473 let path = tempfile.as_ref().map(|p| p.path()).unwrap_or(path);
474
475 let mut background_ddl_enabled = false;
477
478 let mut manual_background_ddl_enabled = false;
481
482 let records = sqllogictest::parse_file(path).expect("failed to parse file");
483 let random_vnode_count = random_vnode_count(&env, &records);
484
485 for record in records {
486 if let sqllogictest::Record::Halt { .. } = record {
491 break;
492 }
493
494 let cmd = match &record {
495 sqllogictest::Record::Statement {
496 sql, conditions, ..
497 }
498 | sqllogictest::Record::Query {
499 sql, conditions, ..
500 } if conditions
501 .iter()
502 .all(|c| !matches!(c, Condition::SkipIf{ label } if label == "madsim"))
503 && !conditions
504 .iter()
505 .any(|c| matches!(c, Condition::OnlyIf{ label} if label != "madsim" )) =>
506 {
507 extract_sql_command(sql).unwrap_or(SqlCmd::Others)
508 }
509 _ => SqlCmd::Others,
510 };
511
512 if !env.kill() {
514 match run_no_kill(&mut tester, random_vnode_count, &cmd, record).await {
515 Ok(_) => continue,
516 Err(e) => panic!("{}", e),
517 }
518 }
519
520 tracing::debug!(?cmd, "Running");
522
523 if let SqlCmd::SetBackgroundDdl { enable } = cmd {
524 manual_background_ddl_enabled = enable;
525 background_ddl_enabled = enable;
526 }
527
528 set_background_ddl(
530 &mut tester,
531 &env,
532 &record,
533 &cmd,
534 manual_background_ddl_enabled,
535 &mut rng,
536 &mut background_ddl_enabled,
537 )
538 .await;
539
540 if !cmd.allow_kill() {
541 run_kill_not_allowed(&mut tester, record.clone()).await;
542 continue;
543 }
544
545 let should_kill = thread_rng().random_bool(env.kill_rate());
546 let handle = if should_kill {
548 let cluster = cluster.clone();
549 let opts = env.kill_opts();
550 Some(tokio::spawn(async move {
551 let t = thread_rng().random_range(Duration::default()..Duration::from_secs(1));
552 tokio::time::sleep(t).await;
553 cluster.kill_node(&opts).await;
554 tokio::time::sleep(Duration::from_secs(15)).await;
555 }))
556 } else {
557 None
558 };
559
560 for i in 0usize.. {
561 tracing::debug!(iteration = i, "retry count");
562 let delay = Duration::from_secs(min(1 << i, 10));
563 if i > 0 {
564 tokio::time::sleep(delay).await;
565 }
566 match tester
567 .run_async(record.clone())
568 .timed(|_res, elapsed| {
569 tracing::debug!("Record {:?} finished in {:?}", record, elapsed)
570 })
571 .await
572 {
573 Ok(_) => {
574 if let SqlCmd::CreateMaterializedView { ref name } = cmd
576 && background_ddl_enabled
577 && !matches!(
578 record,
579 Record::Statement {
580 expected: StatementExpect::Error(_),
581 ..
582 } | Record::Query {
583 expected: QueryExpect::Error(_),
584 ..
585 }
586 )
587 {
588 wait_or_retry_background_ddl!(&record, name, i);
589 }
590 break;
591 }
592 Err(e) => {
593 match cmd {
594 SqlCmd::Create {
596 is_create_table_as: false,
597 }
598 | SqlCmd::CreateMaterializedView { .. }
599 if i != 0
600 && let e = e.to_string()
601 && !e.contains("gRPC request to meta service failed")
604 && e.contains("exists")
605 && !e.contains("still being created")
606 && e.contains("Catalog error") =>
607 {
608 break;
609 }
610 SqlCmd::Drop
612 if i != 0
613 && e.to_string().contains("not found")
614 && e.to_string().contains("Catalog error") =>
615 {
616 break;
617 }
618
619 _ if i >= MAX_RETRY => {
621 panic!("failed to run test after retry {i} times: {e}")
622 }
623 SqlCmd::CreateMaterializedView { ref name }
624 if i != 0
625 && e.to_string().contains("table is in creating procedure")
626 && background_ddl_enabled =>
627 {
628 wait_or_retry_background_ddl!(&record, name, i);
629 }
630 _ => tracing::error!(
631 iteration = i,
632 "failed to run test: {e}\nretry after {delay:?}"
633 ),
634 }
635 }
636 }
637 }
638 if let SqlCmd::SetBackgroundDdl { enable } = cmd {
639 background_ddl_enabled = enable;
640 };
641 if let Some(handle) = handle {
642 handle.await.unwrap();
643 }
644 }
645 }
646}
647
648pub async fn run_parallel_slt_task(glob: &str, jobs: usize) -> Result<(), ParallelTestError> {
649 let mut tester =
650 sqllogictest::Runner::new(|| RisingWave::connect("frontend".into(), "dev".into()));
651 tester.add_label("madsim");
652
653 if let Some(partitioner) = PARTITIONER.as_ref() {
654 tester.with_partitioner(partitioner.clone());
655 }
656
657 tester
658 .run_parallel_async(
659 glob,
660 vec!["frontend".into()],
661 |host, dbname| async move { RisingWave::connect(host, dbname).await.unwrap() },
662 jobs,
663 )
664 .await
665 .map_err(|e| panic!("{e}"))
666}
667
668fn hack_kafka_test(path: &Path) -> tempfile::NamedTempFile {
670 let content = std::fs::read_to_string(path).expect("failed to read file");
671 let simple_avsc_full_path =
672 std::fs::canonicalize("src/connector/src/test_data/simple-schema.avsc")
673 .expect("failed to get schema path");
674 let complex_avsc_full_path =
675 std::fs::canonicalize("src/connector/src/test_data/complex-schema.avsc")
676 .expect("failed to get schema path");
677 let json_schema_full_path =
678 std::fs::canonicalize("src/connector/src/test_data/complex-schema.json")
679 .expect("failed to get schema path");
680 let content = content
681 .replace("127.0.0.1:29092", "192.168.11.1:29092")
682 .replace("localhost:29092", "192.168.11.1:29092")
683 .replace(
684 "/risingwave/avro-simple-schema.avsc",
685 simple_avsc_full_path.to_str().unwrap(),
686 )
687 .replace(
688 "/risingwave/avro-complex-schema.avsc",
689 complex_avsc_full_path.to_str().unwrap(),
690 )
691 .replace(
692 "/risingwave/json-complex-schema",
693 json_schema_full_path.to_str().unwrap(),
694 );
695 let file = tempfile::NamedTempFile::new().expect("failed to create temp file");
696 std::fs::write(file.path(), content).expect("failed to write file");
697 println!("created a temp file for kafka test: {:?}", file.path());
698 file
699}
700
701#[cfg(test)]
702mod tests {
703 use std::fmt::Debug;
704
705 use expect_test::{Expect, expect};
706
707 use super::*;
708
709 fn check(actual: impl Debug, expect: Expect) {
710 let actual = format!("{:#?}", actual);
711 expect.assert_eq(&actual);
712 }
713
714 #[test]
715 fn test_extract_sql_command() {
716 check(
717 extract_sql_command("create table t as select 1;"),
718 expect![[r#"
719 Ok(
720 Create {
721 is_create_table_as: true,
722 },
723 )"#]],
724 );
725 check(
726 extract_sql_command(" create table t (a int);"),
727 expect![[r#"
728 Ok(
729 Create {
730 is_create_table_as: false,
731 },
732 )"#]],
733 );
734 check(
735 extract_sql_command(" create materialized view m_1 as select 1;"),
736 expect![[r#"
737 Ok(
738 CreateMaterializedView {
739 name: "m_1",
740 },
741 )"#]],
742 );
743 check(
744 extract_sql_command("set background_ddl= true;"),
745 expect![[r#"
746 Ok(
747 SetBackgroundDdl {
748 enable: true,
749 },
750 )"#]],
751 );
752 check(
753 extract_sql_command("SET BACKGROUND_DDL=true;"),
754 expect![[r#"
755 Ok(
756 SetBackgroundDdl {
757 enable: true,
758 },
759 )"#]],
760 );
761 check(
762 extract_sql_command("CREATE MATERIALIZED VIEW if not exists m_1 as select 1;"),
763 expect![[r#"
764 Ok(
765 CreateMaterializedView {
766 name: "m_1",
767 },
768 )"#]],
769 )
770 }
771}