Skip to main content

risingwave_meta/barrier/
schedule.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::hash_map::Entry;
16use std::collections::{HashMap, HashSet, VecDeque};
17use std::future::Future;
18use std::sync::Arc;
19
20use anyhow::{Context, anyhow};
21use assert_matches::assert_matches;
22use await_tree::InstrumentAwait;
23use itertools::Itertools;
24use parking_lot::Mutex;
25use risingwave_common::catalog::{DatabaseId, TableId};
26use risingwave_common::id::JobId;
27use risingwave_hummock_sdk::HummockVersionId;
28use risingwave_pb::catalog::Database;
29use rw_futures_util::pending_on_none;
30use tokio::select;
31use tokio::sync::watch;
32use tokio::time::{Duration, Instant};
33use tokio_stream::wrappers::IntervalStream;
34use tokio_stream::{StreamExt, StreamMap};
35use tracing::{info, warn};
36
37use super::{Command, Scheduled};
38use crate::barrier::context::GlobalBarrierWorkerContext;
39use crate::hummock::HummockManagerRef;
40use crate::notification::{Notifier, wait_collection};
41use crate::rpc::metrics::GLOBAL_META_METRICS;
42use crate::{MetaError, MetaResult};
43
44pub(super) struct NewBarrier {
45    pub database_id: DatabaseId,
46    pub command: Option<(Command, Notifier)>,
47    pub span: tracing::Span,
48    pub checkpoint: bool,
49}
50
51/// A queue for scheduling barriers.
52struct Inner {
53    queue: Mutex<ScheduledQueue>,
54
55    /// When `queue` is not empty anymore, all subscribers of this watcher will be notified.
56    changed_tx: watch::Sender<()>,
57}
58
59#[derive(Debug)]
60enum QueueStatus {
61    /// The queue is ready to accept new command.
62    Ready,
63    /// The queue is blocked to accept new command with the given reason.
64    Blocked(String),
65}
66
67impl QueueStatus {
68    fn is_blocked(&self) -> bool {
69        matches!(self, Self::Blocked(_))
70    }
71}
72
73struct ScheduledQueueItem {
74    command: Command,
75    notifier: Notifier,
76    span: tracing::Span,
77}
78
79struct StatusQueue<T> {
80    queue: T,
81    status: QueueStatus,
82}
83
84type DatabaseScheduledQueue = StatusQueue<VecDeque<ScheduledQueueItem>>;
85type ScheduledQueue = StatusQueue<HashMap<DatabaseId, DatabaseScheduledQueue>>;
86
87impl DatabaseScheduledQueue {
88    fn new(status: QueueStatus) -> Self {
89        Self {
90            queue: Default::default(),
91            status,
92        }
93    }
94}
95
96impl<T> StatusQueue<T> {
97    fn mark_blocked(&mut self, reason: String) {
98        self.status = QueueStatus::Blocked(reason);
99    }
100
101    fn mark_ready(&mut self) -> bool {
102        let prev_blocked = self.status.is_blocked();
103        self.status = QueueStatus::Ready;
104        prev_blocked
105    }
106
107    fn validate_item(&mut self, command: &Command) -> MetaResult<()> {
108        // We don't allow any command to be scheduled when the queue is blocked, except for dropping streaming jobs.
109        // Because we allow dropping streaming jobs when the cluster is under recovery, so we have to buffer the drop
110        // command and execute it when the cluster is ready to clean up it.
111        // TODO: this is just a workaround to allow dropping streaming jobs when the cluster is under recovery,
112        // we need to refine it when catalog and streaming metadata can be handled in a transactional way.
113        if let QueueStatus::Blocked(reason) = &self.status
114            && !matches!(
115                command,
116                Command::DropStreamingJobs { .. } | Command::DropSubscription { .. }
117            )
118        {
119            return Err(MetaError::unavailable(reason));
120        }
121        Ok(())
122    }
123}
124
125fn tracing_span() -> tracing::Span {
126    if tracing::Span::current().is_none() {
127        tracing::Span::none()
128    } else {
129        tracing::info_span!(
130            "barrier",
131            checkpoint = tracing::field::Empty,
132            epoch = tracing::field::Empty
133        )
134    }
135}
136
137/// The sender side of the barrier scheduling queue.
138/// Can be cloned and held by other managers to schedule and run barriers.
139#[derive(Clone)]
140pub struct BarrierScheduler {
141    inner: Arc<Inner>,
142
143    /// Used for getting the latest snapshot after `FLUSH`.
144    hummock_manager: HummockManagerRef,
145}
146
147impl BarrierScheduler {
148    /// Create a pair of [`BarrierScheduler`] and [`ScheduledBarriers`], for scheduling barriers
149    /// from different managers, and executing them in the barrier manager, respectively.
150    pub fn new_pair(hummock_manager: HummockManagerRef) -> (Self, ScheduledBarriers) {
151        let inner = Arc::new(Inner {
152            queue: Mutex::new(ScheduledQueue {
153                queue: Default::default(),
154                status: QueueStatus::Ready,
155            }),
156            changed_tx: watch::channel(()).0,
157        });
158
159        (
160            Self {
161                inner: inner.clone(),
162                hummock_manager,
163            },
164            ScheduledBarriers { inner },
165        )
166    }
167
168    /// Push a scheduled barrier into the queue.
169    fn push(
170        &self,
171        database_id: DatabaseId,
172        scheduleds: impl IntoIterator<Item = (Command, Notifier)>,
173    ) -> MetaResult<()> {
174        let mut queue = self.inner.queue.lock();
175        let scheduleds = scheduleds.into_iter().collect_vec();
176        scheduleds
177            .iter()
178            .try_for_each(|(command, _)| queue.validate_item(command))?;
179        let queue = queue
180            .queue
181            .entry(database_id)
182            .or_insert_with(|| DatabaseScheduledQueue::new(QueueStatus::Ready));
183        scheduleds
184            .iter()
185            .try_for_each(|(command, _)| queue.validate_item(command))?;
186        for (command, notifier) in scheduleds {
187            queue.queue.push_back(ScheduledQueueItem {
188                command,
189                notifier,
190                span: tracing_span(),
191            });
192            if queue.queue.len() == 1 {
193                self.inner.changed_tx.send(()).ok();
194            }
195        }
196        Ok(())
197    }
198
199    /// Try to cancel scheduled cmd for create streaming job, return true if the command exists previously and get cancelled.
200    pub fn try_cancel_scheduled_create(&self, database_id: DatabaseId, job_id: JobId) -> bool {
201        let queue = &mut self.inner.queue.lock();
202        let Some(queue) = queue.queue.get_mut(&database_id) else {
203            return false;
204        };
205
206        if let Some(idx) = queue.queue.iter().position(|scheduled| {
207            if let Command::CreateStreamingJob { info, .. } = &scheduled.command
208                && info.stream_job_fragments.stream_job_id() == job_id
209            {
210                true
211            } else {
212                false
213            }
214        }) {
215            queue.queue.remove(idx).unwrap();
216            true
217        } else {
218            false
219        }
220    }
221
222    /// Run multiple commands and return when they're all completely finished (i.e., collected). It's ensured that
223    /// multiple commands are executed continuously.
224    ///
225    /// Run a command and return when it's completely finished (i.e., collected).
226    #[await_tree::instrument("run_command({})", command)]
227    pub async fn run_command(&self, database_id: DatabaseId, command: Command) -> MetaResult<()> {
228        self.schedule_command(database_id, command)?.await
229    }
230
231    /// Schedule a command and return a future that resolves once it is collected.
232    pub fn schedule_command(
233        &self,
234        database_id: DatabaseId,
235        command: Command,
236    ) -> MetaResult<impl Future<Output = MetaResult<()>> + use<>> {
237        tracing::trace!("schedule_command: {:?}", command);
238        let (notifier, started_rx) = Notifier::new();
239        self.push(database_id, vec![(command, notifier)])?;
240        Ok(async move {
241            let collect_rxs = started_rx
242                .instrument_await("wait_injected")
243                .await
244                .ok()
245                .context("failed to inject barrier")??;
246            tracing::trace!(
247                collection_count = collect_rxs.len(),
248                "waiting for collect_rx"
249            );
250            // Wait for every part before returning the first collection error.
251            wait_collection(collect_rxs)
252                .instrument_await("wait_collected")
253                .await
254        })
255    }
256
257    /// Schedule a command without waiting for it to be executed.
258    pub fn run_command_no_wait(&self, database_id: DatabaseId, command: Command) -> MetaResult<()> {
259        tracing::trace!("run_command_no_wait: {:?}", command);
260        let (notifier, _started_rx) = Notifier::new();
261        self.push(database_id, vec![(command, notifier)])
262    }
263
264    /// Flush means waiting for the next barrier to collect.
265    pub async fn flush(&self, database_id: DatabaseId) -> MetaResult<HummockVersionId> {
266        let start = Instant::now();
267
268        tracing::debug!("start barrier flush");
269        self.run_command(database_id, Command::Flush).await?;
270
271        let elapsed = Instant::now().duration_since(start);
272        tracing::debug!("barrier flushed in {:?}", elapsed);
273
274        let version_id = self.hummock_manager.get_version_id().await;
275        Ok(version_id)
276    }
277}
278
279/// The receiver side of the barrier scheduling queue.
280pub struct ScheduledBarriers {
281    inner: Arc<Inner>,
282}
283
284/// State specific to each database for barrier generation.
285#[derive(Debug)]
286pub struct DatabaseBarrierState {
287    barrier_interval: Option<Duration>,
288    checkpoint_frequency: Option<u64>,
289    // The numbers of barrier (checkpoint = false) since the last barrier (checkpoint = true)
290    num_uncheckpointed_barrier: u64,
291}
292
293impl DatabaseBarrierState {
294    fn new(barrier_interval_ms: Option<u32>, checkpoint_frequency: Option<u64>) -> Self {
295        Self {
296            barrier_interval: barrier_interval_ms.map(|ms| Duration::from_millis(ms as u64)),
297            checkpoint_frequency,
298            num_uncheckpointed_barrier: 0,
299        }
300    }
301}
302
303/// Held by the [`crate::barrier::worker::GlobalBarrierWorker`] to execute these commands.
304#[derive(Default, Debug)]
305pub struct PeriodicBarriers {
306    /// Default system params for barrier interval and checkpoint frequency.
307    sys_barrier_interval: Duration,
308    sys_checkpoint_frequency: u64,
309    /// Per-database state.
310    databases: HashMap<DatabaseId, DatabaseBarrierState>,
311    /// Holds `IntervalStream` for each database, keyed by `DatabaseId`.
312    /// `StreamMap` will yield `(DatabaseId, Instant)` when a timer ticks.
313    timer_streams: StreamMap<DatabaseId, IntervalStream>,
314    force_checkpoint_databases: HashSet<DatabaseId>,
315}
316
317impl PeriodicBarriers {
318    pub(super) fn new(
319        sys_barrier_interval: Duration,
320        sys_checkpoint_frequency: u64,
321        database_infos: Vec<Database>,
322    ) -> Self {
323        let mut databases = HashMap::with_capacity(database_infos.len());
324        let mut timer_streams = StreamMap::with_capacity(database_infos.len());
325        database_infos.into_iter().for_each(|database| {
326            let database_id: DatabaseId = database.id;
327            let barrier_interval_ms = database.barrier_interval_ms;
328            let checkpoint_frequency = database.checkpoint_frequency;
329            databases.insert(
330                database_id,
331                DatabaseBarrierState::new(barrier_interval_ms, checkpoint_frequency),
332            );
333            let duration = if let Some(ms) = barrier_interval_ms {
334                Duration::from_millis(ms as u64)
335            } else {
336                sys_barrier_interval
337            };
338
339            // Create an `IntervalStream` for the database with the specified interval.
340            let interval_stream = Self::new_interval_stream(duration, &database_id);
341            timer_streams.insert(database_id, interval_stream);
342        });
343        Self {
344            sys_barrier_interval,
345            sys_checkpoint_frequency,
346            databases,
347            timer_streams,
348            force_checkpoint_databases: Default::default(),
349        }
350    }
351
352    // Create a new interval stream with the specified duration.
353    fn new_interval_stream(duration: Duration, database_id: &DatabaseId) -> IntervalStream {
354        GLOBAL_META_METRICS
355            .barrier_interval_by_database
356            .with_label_values(&[&database_id.to_string()])
357            .set(duration.as_millis_f64());
358        let mut interval = tokio::time::interval(duration);
359        interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
360        IntervalStream::new(interval)
361    }
362
363    /// Update the system barrier interval.
364    pub(super) fn set_sys_barrier_interval(&mut self, duration: Duration) {
365        if self.sys_barrier_interval == duration {
366            return;
367        }
368        self.sys_barrier_interval = duration;
369        // Reset the `IntervalStream` for all databases that use default param.
370        for (db_id, db_state) in &mut self.databases {
371            if db_state.barrier_interval.is_none() {
372                let interval_stream = Self::new_interval_stream(duration, db_id);
373                self.timer_streams.insert(*db_id, interval_stream);
374            }
375        }
376    }
377
378    /// Update the system checkpoint frequency.
379    pub fn set_sys_checkpoint_frequency(&mut self, frequency: u64) {
380        if self.sys_checkpoint_frequency == frequency {
381            return;
382        }
383        self.sys_checkpoint_frequency = frequency;
384        // Reset the `num_uncheckpointed_barrier` for all databases that use default param.
385        for db_state in self.databases.values_mut() {
386            if db_state.checkpoint_frequency.is_none() {
387                db_state.num_uncheckpointed_barrier = 0;
388            }
389        }
390    }
391
392    pub(super) fn update_database_barrier(
393        &mut self,
394        database_id: DatabaseId,
395        barrier_interval_ms: Option<u32>,
396        checkpoint_frequency: Option<u64>,
397    ) {
398        match self.databases.entry(database_id) {
399            Entry::Occupied(mut entry) => {
400                let db_state = entry.get_mut();
401                db_state.barrier_interval =
402                    barrier_interval_ms.map(|ms| Duration::from_millis(ms as u64));
403                db_state.checkpoint_frequency = checkpoint_frequency;
404                // Reset the `num_uncheckpointed_barrier` since the barrier interval or checkpoint frequency is changed.
405                db_state.num_uncheckpointed_barrier = 0;
406            }
407            Entry::Vacant(entry) => {
408                entry.insert(DatabaseBarrierState::new(
409                    barrier_interval_ms,
410                    checkpoint_frequency,
411                ));
412            }
413        }
414
415        // If the database already has a timer stream, reset it with the new interval.
416        let duration = if let Some(ms) = barrier_interval_ms {
417            Duration::from_millis(ms as u64)
418        } else {
419            self.sys_barrier_interval
420        };
421
422        let interval_stream = Self::new_interval_stream(duration, &database_id);
423        self.timer_streams.insert(database_id, interval_stream);
424    }
425
426    /// Make the `checkpoint` of the next barrier must be true.
427    pub fn force_checkpoint_in_next_barrier(&mut self, database_id: DatabaseId) {
428        if self.databases.contains_key(&database_id) {
429            self.force_checkpoint_databases.insert(database_id);
430        } else {
431            warn!(
432                ?database_id,
433                "force checkpoint in next barrier for non-existing database"
434            );
435        }
436    }
437
438    fn reset_database_timer(&mut self, database_id: DatabaseId) {
439        // Check if the database exists.
440        assert!(
441            self.databases.contains_key(&database_id),
442            "database {} not found in scheduled barriers",
443            database_id
444        );
445        assert!(
446            self.timer_streams.contains_key(&database_id),
447            "timer stream for database {} not found in scheduled barriers",
448            database_id
449        );
450        // New command will trigger the barriers, so reset the timer for the specific database.
451        for (db_id, timer_stream) in self.timer_streams.iter_mut() {
452            if *db_id == database_id {
453                timer_stream.as_mut().reset();
454            }
455        }
456    }
457
458    #[await_tree::instrument]
459    pub(super) async fn next_barrier(
460        &mut self,
461        context: &impl GlobalBarrierWorkerContext,
462    ) -> NewBarrier {
463        let force_checkpoint_database = self.force_checkpoint_databases.extract_if(|_| true).next();
464        let new_barrier = if let Some(database_id) = force_checkpoint_database {
465            self.reset_database_timer(database_id);
466            NewBarrier {
467                database_id,
468                command: None,
469                span: tracing_span(),
470                checkpoint: true,
471            }
472        } else {
473            select! {
474                biased;
475                scheduled = context.next_scheduled() => {
476                    let database_id = scheduled.database_id;
477                    self.reset_database_timer(database_id);
478                    let checkpoint = scheduled.command.need_checkpoint() || self.try_get_checkpoint(database_id);
479                    NewBarrier {
480                        database_id: scheduled.database_id,
481                        command: Some((scheduled.command, scheduled.notifier)),
482                        span: scheduled.span,
483                        checkpoint,
484                    }
485                },
486                // If there is no database, we won't wait for `Interval`, but only wait for command.
487                // Normally it will not return None, because there is always at least one database.
488                (database_id, _instant) = pending_on_none(self.timer_streams.next()) => {
489                    let checkpoint = self.try_get_checkpoint(database_id);
490                    NewBarrier {
491                        database_id,
492                        command: None,
493                        span: tracing_span(),
494                        checkpoint,
495                    }
496                }
497            }
498        };
499        self.update_num_uncheckpointed_barrier(new_barrier.database_id, new_barrier.checkpoint);
500
501        new_barrier
502    }
503
504    /// Whether the barrier(checkpoint = true) should be injected.
505    fn try_get_checkpoint(&self, database_id: DatabaseId) -> bool {
506        let db_state = self.databases.get(&database_id).unwrap();
507        let checkpoint_frequency = db_state
508            .checkpoint_frequency
509            .unwrap_or(self.sys_checkpoint_frequency);
510        db_state.num_uncheckpointed_barrier + 1 >= checkpoint_frequency
511    }
512
513    /// Update the `num_uncheckpointed_barrier`
514    fn update_num_uncheckpointed_barrier(&mut self, database_id: DatabaseId, checkpoint: bool) {
515        let db_state = self.databases.get_mut(&database_id).unwrap();
516        if checkpoint {
517            db_state.num_uncheckpointed_barrier = 0;
518        } else {
519            db_state.num_uncheckpointed_barrier += 1;
520        }
521    }
522}
523
524impl ScheduledBarriers {
525    pub(super) async fn next_scheduled(&self) -> Scheduled {
526        'outer: loop {
527            let mut rx = self.inner.changed_tx.subscribe();
528            {
529                let mut queue = self.inner.queue.lock();
530                if queue.status.is_blocked() {
531                    continue;
532                }
533                for (database_id, queue) in &mut queue.queue {
534                    if queue.status.is_blocked() {
535                        continue;
536                    }
537                    if let Some(item) = queue.queue.pop_front() {
538                        break 'outer Scheduled {
539                            database_id: *database_id,
540                            command: item.command,
541                            notifier: item.notifier,
542                            span: item.span,
543                        };
544                    }
545                }
546            }
547            rx.changed().await.unwrap();
548        }
549    }
550}
551
552pub(super) enum MarkReadyOptions {
553    Database(DatabaseId),
554    Global {
555        failed_databases: HashMap<DatabaseId, HashSet<JobId>>,
556    },
557}
558
559pub(super) struct PreApplyDropCancel {
560    pub streaming_job_ids: Vec<JobId>,
561    pub dropped_state_table_ids: Vec<TableId>,
562}
563
564impl ScheduledBarriers {
565    /// Pre buffered drop and cancel command, return all dropped state tables if any.
566    pub(super) fn pre_apply_drop_cancel(
567        &self,
568        database_id: Option<DatabaseId>,
569    ) -> PreApplyDropCancel {
570        self.pre_apply_drop_cancel_scheduled(database_id)
571    }
572
573    /// Mark command scheduler as blocked and abort all queued scheduled command and notify with
574    /// specific reason.
575    pub(super) fn abort_and_mark_blocked(
576        &self,
577        database_id: Option<DatabaseId>,
578        reason: impl Into<String>,
579    ) {
580        let mut queue = self.inner.queue.lock();
581        fn database_blocked_reason(database_id: DatabaseId, reason: &String) -> String {
582            format!("database {} unavailable {}", database_id, reason)
583        }
584        fn mark_blocked_and_notify_failed(
585            database_id: DatabaseId,
586            queue: &mut DatabaseScheduledQueue,
587            reason: &String,
588        ) {
589            let reason = database_blocked_reason(database_id, reason);
590            let err: MetaError = anyhow!("{}", reason).into();
591            queue.mark_blocked(reason);
592            while let Some(ScheduledQueueItem { notifier, .. }) = queue.queue.pop_front() {
593                notifier.notify_start_failed(err.clone());
594            }
595        }
596        if let Some(database_id) = database_id {
597            let reason = reason.into();
598            match queue.queue.entry(database_id) {
599                Entry::Occupied(entry) => {
600                    let queue = entry.into_mut();
601                    if queue.status.is_blocked() {
602                        if cfg!(debug_assertions) {
603                            panic!("database {} marked as blocked twice", database_id);
604                        } else {
605                            warn!(?database_id, "database marked as blocked twice");
606                        }
607                    }
608                    info!(?database_id, "database marked as blocked");
609                    mark_blocked_and_notify_failed(database_id, queue, &reason);
610                }
611                Entry::Vacant(entry) => {
612                    entry.insert(DatabaseScheduledQueue::new(QueueStatus::Blocked(
613                        database_blocked_reason(database_id, &reason),
614                    )));
615                }
616            }
617        } else {
618            let reason = reason.into();
619            if queue.status.is_blocked() {
620                if cfg!(debug_assertions) {
621                    panic!("cluster marked as blocked twice");
622                } else {
623                    warn!("cluster marked as blocked twice");
624                }
625            }
626            info!("cluster marked as blocked");
627            queue.mark_blocked(reason.clone());
628            for (database_id, queue) in &mut queue.queue {
629                mark_blocked_and_notify_failed(*database_id, queue, &reason);
630            }
631        }
632    }
633
634    /// Mark command scheduler as ready to accept new command.
635    pub(super) fn mark_ready(&self, options: MarkReadyOptions) {
636        let mut queue = self.inner.queue.lock();
637        let queue = &mut *queue;
638        match options {
639            MarkReadyOptions::Database(database_id) => {
640                info!(?database_id, "database marked as ready");
641                let database_queue = queue
642                    .queue
643                    .entry(database_id)
644                    .or_insert_with(|| DatabaseScheduledQueue::new(QueueStatus::Ready));
645                if !database_queue.status.is_blocked() {
646                    if cfg!(debug_assertions) {
647                        panic!("database {} marked as ready twice", database_id);
648                    } else {
649                        warn!(?database_id, "database marked as ready twice");
650                    }
651                }
652                if database_queue.mark_ready()
653                    && !queue.status.is_blocked()
654                    && !database_queue.queue.is_empty()
655                {
656                    self.inner.changed_tx.send(()).ok();
657                }
658            }
659            MarkReadyOptions::Global { failed_databases } => {
660                if !queue.status.is_blocked() {
661                    if cfg!(debug_assertions) {
662                        panic!("cluster marked as ready twice");
663                    } else {
664                        warn!("cluster marked as ready twice");
665                    }
666                }
667                info!(
668                    failed_database_ids = ?failed_databases.keys().collect_vec(),
669                    "cluster marked as ready"
670                );
671                let prev_blocked = queue.mark_ready();
672                for database_id in failed_databases.keys() {
673                    queue.queue.entry(*database_id).or_insert_with(|| {
674                        DatabaseScheduledQueue::new(QueueStatus::Blocked(format!(
675                            "database {} failed to recover in global recovery",
676                            database_id
677                        )))
678                    });
679                }
680                for (database_id, queue) in &mut queue.queue {
681                    if !failed_databases.contains_key(database_id) {
682                        queue.mark_ready();
683                    }
684                }
685                if prev_blocked
686                    && queue
687                        .queue
688                        .values()
689                        .any(|database_queue| !database_queue.queue.is_empty())
690                {
691                    self.inner.changed_tx.send(()).ok();
692                }
693            }
694        }
695    }
696
697    /// Try to pre apply drop and cancel scheduled command and return all dropped state tables if any.
698    /// It should only be called in recovery.
699    pub(super) fn pre_apply_drop_cancel_scheduled(
700        &self,
701        database_id: Option<DatabaseId>,
702    ) -> PreApplyDropCancel {
703        let mut queue = self.inner.queue.lock();
704        let mut drop_cancel = PreApplyDropCancel {
705            streaming_job_ids: vec![],
706            dropped_state_table_ids: vec![],
707        };
708
709        let mut pre_apply_drop_cancel = |queue: &mut DatabaseScheduledQueue| {
710            while let Some(ScheduledQueueItem {
711                notifier, command, ..
712            }) = queue.queue.pop_front()
713            {
714                match command {
715                    Command::DropStreamingJobs {
716                        streaming_job_ids,
717                        unregistered_state_table_ids,
718                        ..
719                    } => {
720                        drop_cancel.streaming_job_ids.extend(streaming_job_ids);
721                        drop_cancel
722                            .dropped_state_table_ids
723                            .extend(unregistered_state_table_ids);
724                    }
725                    Command::DropSubscription { .. } => {}
726                    _ => {
727                        unreachable!("only drop and cancel streaming jobs should be buffered");
728                    }
729                }
730                // These buffered commands are pre-applied without injecting a real barrier.
731                notifier.start().started();
732            }
733        };
734
735        if let Some(database_id) = database_id {
736            assert_matches!(queue.status, QueueStatus::Ready);
737            if let Some(queue) = queue.queue.get_mut(&database_id) {
738                assert_matches!(queue.status, QueueStatus::Blocked(_));
739                pre_apply_drop_cancel(queue);
740            }
741        } else {
742            assert_matches!(queue.status, QueueStatus::Blocked(_));
743            for queue in queue.queue.values_mut() {
744                pre_apply_drop_cancel(queue);
745            }
746        }
747
748        drop_cancel
749    }
750}
751
752#[cfg(test)]
753mod tests {
754    use futures::FutureExt;
755    use risingwave_meta_model::PartialGraphId;
756
757    use super::*;
758
759    fn create_test_database(
760        id: u32,
761        barrier_interval_ms: Option<u32>,
762        checkpoint_frequency: Option<u64>,
763    ) -> Database {
764        Database {
765            id: id.into(),
766            name: format!("test_db_{}", id),
767            barrier_interval_ms,
768            checkpoint_frequency,
769            ..Default::default()
770        }
771    }
772
773    // Mock context for testing next_barrier
774    struct MockGlobalBarrierWorkerContext {
775        scheduled_rx: tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<Scheduled>>,
776    }
777
778    impl MockGlobalBarrierWorkerContext {
779        fn new() -> (Self, tokio::sync::mpsc::UnboundedSender<Scheduled>) {
780            let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
781            (
782                Self {
783                    scheduled_rx: tokio::sync::Mutex::new(rx),
784                },
785                tx,
786            )
787        }
788    }
789
790    impl GlobalBarrierWorkerContext for MockGlobalBarrierWorkerContext {
791        async fn next_scheduled(&self) -> Scheduled {
792            self.scheduled_rx.lock().await.recv().await.unwrap()
793        }
794
795        async fn commit_epoch(
796            &self,
797            _commit_info: crate::hummock::CommitEpochInfo,
798        ) -> MetaResult<risingwave_pb::hummock::HummockVersionStats> {
799            unimplemented!()
800        }
801
802        async fn abort_and_mark_blocked(
803            &self,
804            _recovery: crate::manager::sink_coordination::RecoveryStart,
805            _recovery_reason: crate::barrier::RecoveryReason,
806        ) -> MetaResult<()> {
807            unimplemented!()
808        }
809
810        async fn mark_ready(&self, _options: MarkReadyOptions) -> MetaResult<()> {
811            unimplemented!()
812        }
813
814        async fn resolve_log_store_epoch<'a>(
815            &'a self,
816            _upstream_table_ids: impl Iterator<Item = risingwave_common::catalog::TableId> + Send + 'a,
817            _since_epoch: u64,
818        ) -> MetaResult<crate::barrier::command::SinceTimestampResolvedEpoch> {
819            Ok(Default::default())
820        }
821
822        async fn post_collect_command(
823            &self,
824            _command: crate::barrier::command::PostCollectCommand,
825        ) -> MetaResult<()> {
826            unimplemented!()
827        }
828
829        async fn notify_creating_job_failed(&self, _database_id: Option<DatabaseId>, _err: String) {
830            unimplemented!()
831        }
832
833        async fn finish_creating_job(
834            &self,
835            _job: crate::barrier::progress::TrackingJob,
836        ) -> MetaResult<()> {
837            unimplemented!()
838        }
839
840        async fn new_control_stream(
841            &self,
842            _node: &risingwave_pb::common::WorkerNode,
843        ) -> MetaResult<risingwave_rpc_client::StreamingControlHandle> {
844            unimplemented!()
845        }
846
847        async fn reload_runtime_info(
848            &self,
849        ) -> MetaResult<crate::barrier::BarrierWorkerRuntimeInfoSnapshot> {
850            unimplemented!()
851        }
852
853        async fn reload_database_runtime_info(
854            &self,
855            _database_id: DatabaseId,
856        ) -> MetaResult<crate::barrier::DatabaseRuntimeInfoSnapshot> {
857            unimplemented!()
858        }
859
860        async fn handle_list_finished_source_ids(
861            &self,
862            _list_finished_source_ids: Vec<
863                risingwave_pb::stream_service::barrier_complete_response::PbListFinishedSource,
864            >,
865        ) -> MetaResult<()> {
866            unimplemented!()
867        }
868
869        async fn handle_load_finished_source_ids(
870            &self,
871            _load_finished_source_ids: Vec<
872                risingwave_pb::stream_service::barrier_complete_response::PbLoadFinishedSource,
873            >,
874        ) -> MetaResult<()> {
875            unimplemented!()
876        }
877
878        async fn finish_cdc_table_backfill(&self, _job_id: JobId) -> MetaResult<()> {
879            unimplemented!()
880        }
881
882        async fn handle_refresh_finished_actors(
883            &self,
884            _refresh_finished_actors: Vec<
885                risingwave_pb::stream_service::barrier_complete_response::PbRefreshFinishedActor,
886            >,
887        ) -> MetaResult<()> {
888            unimplemented!()
889        }
890
891        async fn load_batch_refresh_trigger_context(
892            &self,
893            _job_id: JobId,
894            _database_id: DatabaseId,
895            _last_committed_epoch: u64,
896        ) -> MetaResult<crate::barrier::checkpoint::independent_job::BatchRefreshJobTriggerContext>
897        {
898            unimplemented!()
899        }
900
901        async fn pre_commit_iceberg_pk_index_sink_metadata(
902            &self,
903            _metadata: Vec<crate::manager::iceberg_pk_index_sink::IcebergPkIndexPreCommitMetadata>,
904        ) -> MetaResult<Vec<risingwave_meta_model::SinkId>> {
905            unimplemented!()
906        }
907
908        async fn commit_iceberg_pk_index_sink_metadata(
909            &self,
910            _sink_ids: Vec<risingwave_meta_model::SinkId>,
911        ) -> MetaResult<()> {
912            unimplemented!()
913        }
914
915        fn advance_iceberg_pk_index_sink_committed_epochs(
916            &self,
917            _epochs: impl IntoIterator<Item = (PartialGraphId, u64)>,
918        ) {
919            unimplemented!()
920        }
921    }
922
923    #[tokio::test(start_paused = true)]
924    async fn test_next_barrier_with_different_intervals() {
925        // Create databases with different intervals
926        let databases = vec![
927            create_test_database(1, Some(50), Some(2)), // 50ms interval, checkpoint every 2
928            create_test_database(2, Some(100), Some(3)), // 100ms interval, checkpoint every 3
929            create_test_database(3, None, Some(5)), /* Use system default (200ms), checkpoint every 5 */
930        ];
931
932        let mut periodic = PeriodicBarriers::new(
933            Duration::from_millis(200), // System default
934            10,                         // System checkpoint frequency
935            databases,
936        );
937
938        let (context, _tx) = MockGlobalBarrierWorkerContext::new();
939
940        // Call next_barrier for each database once, because the first tick is returned immediately
941        for _ in 0..3 {
942            let barrier = periodic.next_barrier(&context).await;
943            assert!(barrier.command.is_none()); // Should be a periodic barrier, not a scheduled command
944            assert!(!barrier.checkpoint); // First barrier shouldn't be a checkpoint
945        }
946
947        // Since we have 3 databases with intervals 50ms, 100ms, and 200ms,
948        // the first barrier should come from database 1 (50ms interval)
949        let start_time = Instant::now();
950        let barrier = periodic.next_barrier(&context).await;
951        let mut elapsed = start_time.elapsed();
952
953        // Verify the barrier properties
954        assert_eq!(barrier.database_id, DatabaseId::from(1));
955        assert!(barrier.command.is_none()); // Should be a periodic barrier, not a scheduled command
956        assert!(barrier.checkpoint); // Second barrier should be checkpoint for database 1
957        // Use tokio's time pause mechanism, so it will be exactly 50ms here.
958        assert_eq!(
959            elapsed,
960            Duration::from_millis(50),
961            "Elapsed time exceeded: {:?}",
962            elapsed
963        );
964
965        // Verify that the checkpoint frequency works
966        let db1_id = DatabaseId::from(1);
967        let db1_state = periodic.databases.get_mut(&db1_id).unwrap();
968        assert_eq!(db1_state.num_uncheckpointed_barrier, 0); // Should reset after checkpoint
969
970        // Next barrier should come from database 1 and database 2 at 100ms
971        for _ in 0..2 {
972            let barrier = periodic.next_barrier(&context).await;
973            assert!(barrier.command.is_none()); // Should be a periodic barrier, not a scheduled command
974            assert!(!barrier.checkpoint); // Next two barriers shouldn't be checkpoints
975        }
976
977        elapsed = start_time.elapsed();
978
979        assert_eq!(
980            elapsed,
981            Duration::from_millis(100),
982            "Elapsed time exceeded: {:?}",
983            elapsed
984        );
985    }
986
987    #[tokio::test]
988    async fn test_next_barrier_with_scheduled_command() {
989        let databases = vec![
990            create_test_database(1, Some(1000), Some(2)), // Long interval to avoid interference
991        ];
992
993        let mut periodic = PeriodicBarriers::new(Duration::from_millis(1000), 10, databases);
994
995        let (context, tx) = MockGlobalBarrierWorkerContext::new();
996
997        // Skip the first barrier to let the timers start
998        periodic.next_barrier(&context).await;
999
1000        // Schedule a command
1001        let (notifier, _started_rx) = Notifier::new();
1002        let scheduled_command = Scheduled {
1003            database_id: DatabaseId::from(1),
1004            command: Command::Flush,
1005            notifier,
1006            span: tracing::Span::none(),
1007        };
1008
1009        // Send scheduled command in background
1010        let tx_clone = tx.clone();
1011        tokio::spawn(async move {
1012            tokio::time::sleep(Duration::from_millis(10)).await;
1013            tx_clone.send(scheduled_command).unwrap();
1014        });
1015
1016        let barrier = periodic.next_barrier(&context).await;
1017
1018        // Should return the scheduled command
1019        assert!(barrier.command.is_some());
1020        assert_eq!(barrier.database_id, DatabaseId::from(1));
1021
1022        if let Some((command, _)) = barrier.command {
1023            assert!(matches!(command, Command::Flush));
1024        }
1025    }
1026
1027    #[tokio::test(start_paused = true)]
1028    async fn test_next_barrier_multiple_databases_timing() {
1029        let databases = vec![
1030            create_test_database(1, Some(30), Some(10)), // Fast interval
1031            create_test_database(2, Some(100), Some(10)), // Slower interval
1032        ];
1033
1034        let mut periodic = PeriodicBarriers::new(Duration::from_millis(500), 10, databases);
1035
1036        let (context, _tx) = MockGlobalBarrierWorkerContext::new();
1037
1038        // Skip first 2 barriers to let the timers start
1039        for _ in 0..2 {
1040            periodic.next_barrier(&context).await;
1041        }
1042
1043        let mut barrier_counts = HashMap::new();
1044
1045        // Collect barriers for a short period
1046        let mut barriers = Vec::new();
1047        for _ in 0..5 {
1048            let barrier = periodic.next_barrier(&context).await;
1049            barriers.push(barrier);
1050        }
1051
1052        // Count barriers per database
1053        for barrier in barriers {
1054            *barrier_counts.entry(barrier.database_id).or_insert(0) += 1;
1055        }
1056
1057        // Database 1 (30ms interval) should have more barriers than database 2 (100ms interval)
1058        let db1_count = barrier_counts.get(&DatabaseId::from(1)).unwrap_or(&0);
1059        let db2_count = barrier_counts.get(&DatabaseId::from(2)).unwrap_or(&0);
1060
1061        // Due to timing, db1 should generally have more barriers, but allow for some variance
1062        assert_eq!(*db1_count, 4);
1063        assert_eq!(*db2_count, 1);
1064    }
1065
1066    #[tokio::test]
1067    async fn test_next_barrier_force_checkpoint() {
1068        let databases = vec![create_test_database(1, Some(100), Some(10))];
1069
1070        let mut periodic = PeriodicBarriers::new(Duration::from_millis(100), 10, databases);
1071
1072        let (context, _tx) = MockGlobalBarrierWorkerContext::new();
1073
1074        // Force checkpoint for next barrier
1075        periodic.force_checkpoint_in_next_barrier(DatabaseId::from(1));
1076
1077        let barrier = periodic.next_barrier(&context).now_or_never().unwrap();
1078
1079        // Should be a checkpoint barrier due to force_checkpoint
1080        assert!(barrier.checkpoint);
1081        assert_eq!(barrier.database_id, DatabaseId::from(1));
1082        assert!(barrier.command.is_none());
1083    }
1084
1085    #[tokio::test]
1086    async fn test_next_barrier_multiple_force_checkpoints() {
1087        let databases = vec![
1088            create_test_database(1, Some(100), Some(10)),
1089            create_test_database(2, Some(100), Some(10)),
1090        ];
1091
1092        let mut periodic = PeriodicBarriers::new(Duration::from_millis(100), 10, databases);
1093
1094        let (context, _tx) = MockGlobalBarrierWorkerContext::new();
1095
1096        periodic.force_checkpoint_in_next_barrier(DatabaseId::from(1));
1097        periodic.force_checkpoint_in_next_barrier(DatabaseId::from(2));
1098
1099        let barrier1 = periodic.next_barrier(&context).now_or_never().unwrap();
1100        let barrier2 = periodic.next_barrier(&context).now_or_never().unwrap();
1101
1102        assert!(barrier1.checkpoint);
1103        assert!(barrier1.command.is_none());
1104        assert!(barrier2.checkpoint);
1105        assert!(barrier2.command.is_none());
1106        assert_eq!(
1107            HashSet::from([barrier1.database_id, barrier2.database_id]),
1108            HashSet::from([DatabaseId::from(1), DatabaseId::from(2)])
1109        );
1110        assert!(periodic.force_checkpoint_databases.is_empty());
1111    }
1112
1113    #[tokio::test]
1114    async fn test_next_barrier_checkpoint_frequency() {
1115        let databases = vec![create_test_database(1, Some(50), Some(2))]; // Checkpoint every 2 barriers
1116
1117        let mut periodic = PeriodicBarriers::new(Duration::from_millis(50), 10, databases);
1118
1119        let (context, _tx) = MockGlobalBarrierWorkerContext::new();
1120
1121        // First barrier - should not be checkpoint
1122        let barrier1 = periodic.next_barrier(&context).await;
1123        assert!(!barrier1.checkpoint);
1124
1125        // Second barrier - should be checkpoint (frequency = 2)
1126        let barrier2 = periodic.next_barrier(&context).await;
1127        assert!(barrier2.checkpoint);
1128
1129        // Third barrier - should not be checkpoint (counter reset)
1130        let barrier3 = periodic.next_barrier(&context).await;
1131        assert!(!barrier3.checkpoint);
1132    }
1133
1134    #[tokio::test]
1135    async fn test_update_database_barrier() {
1136        let databases = vec![create_test_database(1, Some(1000), Some(10))];
1137
1138        let mut periodic = PeriodicBarriers::new(Duration::from_millis(500), 20, databases);
1139
1140        let database_id = DatabaseId::new(1);
1141
1142        // Update existing database
1143        periodic.update_database_barrier(database_id, Some(2000), Some(15));
1144
1145        let db_state = periodic.databases.get(&database_id).unwrap();
1146        assert_eq!(db_state.barrier_interval, Some(Duration::from_millis(2000)));
1147        assert_eq!(db_state.checkpoint_frequency, Some(15));
1148        assert_eq!(db_state.num_uncheckpointed_barrier, 0);
1149        assert!(!periodic.force_checkpoint_databases.contains(&database_id));
1150
1151        // Add new database
1152        periodic.update_database_barrier(DatabaseId::from(2), None, None);
1153
1154        assert!(periodic.databases.contains_key(&DatabaseId::from(2)));
1155        let db2_state = periodic.databases.get(&DatabaseId::from(2)).unwrap();
1156        assert_eq!(db2_state.barrier_interval, None);
1157        assert_eq!(db2_state.checkpoint_frequency, None);
1158    }
1159}