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