Skip to main content

risingwave_meta/barrier/
schedule.rs

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