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