Skip to main content

risingwave_meta/barrier/
worker.rs

1// Copyright 2024 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};
17use std::mem::{replace, take};
18use std::pin::pin;
19use std::sync::Arc;
20use std::time::Duration;
21
22use anyhow::anyhow;
23use arc_swap::ArcSwap;
24use futures::{TryFutureExt, pin_mut};
25use itertools::Itertools;
26use risingwave_common::catalog::DatabaseId;
27use risingwave_common::id::JobId;
28use risingwave_common::system_param::PAUSE_ON_NEXT_BOOTSTRAP_KEY;
29use risingwave_common::system_param::reader::SystemParamsRead;
30use risingwave_meta_model::WorkerId;
31use risingwave_pb::common::WorkerNode;
32use risingwave_pb::meta::Recovery;
33use risingwave_pb::meta::subscribe_response::{Info, Operation};
34use thiserror_ext::AsReport;
35use tokio::select;
36use tokio::sync::mpsc;
37use tokio::sync::oneshot::{Receiver, Sender};
38use tokio::task::JoinHandle;
39use tonic::Status;
40use tracing::{Instrument, debug, error, info, warn};
41
42use crate::barrier::checkpoint::{CheckpointControl, CheckpointControlEvent};
43use crate::barrier::complete_task::{BarrierCompleteOutput, CompletingTask};
44use crate::barrier::context::recovery::{RenderedDatabaseRuntimeInfo, render_runtime_info};
45use crate::barrier::context::{GlobalBarrierWorkerContext, GlobalBarrierWorkerContextImpl};
46use crate::barrier::info::InflightDatabaseInfo;
47use crate::barrier::rpc::{
48    DatabaseInitialBarrierCollector, database_partial_graphs, from_partial_graph_id,
49    merge_node_rpc_errors,
50};
51use crate::barrier::schedule::{MarkReadyOptions, PeriodicBarriers};
52use crate::barrier::{
53    BarrierManagerRequest, BarrierManagerStatus, BarrierWorkerRuntimeInfoSnapshot, Command,
54    CreateStreamingJobType, RecoveryReason, RescheduleContext, UpdateDatabaseBarrierRequest,
55    schedule,
56};
57use crate::controller::scale::{materialize_actor_assignments, preview_actor_assignments};
58use crate::error::MetaErrorInner;
59use crate::hummock::HummockManagerRef;
60use crate::manager::iceberg_compaction::IcebergCompactionManagerRef;
61use crate::manager::iceberg_pk_index_sink::IcebergPkIndexSinkManager;
62use crate::manager::sink_coordination::SinkCoordinatorManager;
63use crate::manager::{
64    ActiveStreamingWorkerChange, ActiveStreamingWorkerNodes, LocalNotification, MetaSrvEnv,
65    MetadataManager,
66};
67use crate::rpc::metrics::GLOBAL_META_METRICS;
68use crate::serving::ServingVnodeMappingRef;
69use crate::stream::{
70    GlobalRefreshManagerRef, ScaleControllerRef, SourceManagerRef, build_reschedule_commands,
71    rendered_layout_matches_current,
72};
73use crate::{MetaError, MetaResult};
74
75/// [`crate::barrier::worker::GlobalBarrierWorker`] sends barriers to all registered compute nodes and
76/// collect them, with monotonic increasing epoch numbers. On compute nodes, `LocalBarrierManager`
77/// in `risingwave_stream` crate will serve these requests and dispatch them to source actors.
78///
79/// Configuration change in our system is achieved by the mutation in the barrier. Thus,
80/// [`crate::barrier::worker::GlobalBarrierWorker`] provides a set of interfaces like a state machine,
81/// accepting [`crate::barrier::command::Command`] that carries info to build `Mutation`. To keep the consistency between
82/// barrier manager and meta store, some actions like "drop materialized view" or "create mv on mv"
83/// must be done in barrier manager transactional using [`crate::barrier::command::Command`].
84pub(super) struct GlobalBarrierWorker<C> {
85    /// Enable recovery or not when failover.
86    enable_recovery: bool,
87
88    /// The queue of scheduled barriers.
89    periodic_barriers: PeriodicBarriers,
90
91    /// Whether per database failure isolation is enabled in system parameters.
92    system_enable_per_database_isolation: bool,
93
94    pub(super) context: Arc<C>,
95
96    env: MetaSrvEnv,
97
98    checkpoint_control: CheckpointControl,
99
100    /// Command that has been collected but is still completing.
101    /// The join handle of the completing future is stored.
102    completing_task: CompletingTask,
103
104    request_rx: mpsc::UnboundedReceiver<BarrierManagerRequest>,
105
106    active_streaming_nodes: ActiveStreamingWorkerNodes,
107
108    partial_graph_manager: PartialGraphManager,
109}
110
111#[cfg(test)]
112mod tests {
113    use std::collections::HashMap;
114
115    use tokio::sync::oneshot;
116
117    use super::*;
118    use crate::barrier::RescheduleContext;
119    use crate::barrier::notifier::Notifier;
120
121    #[tokio::test]
122    async fn test_reschedule_intent_without_workers_notifies_start_failed() {
123        let env = MetaSrvEnv::for_test().await;
124        let database_id = DatabaseId::new(1);
125        let database_info =
126            InflightDatabaseInfo::empty(database_id, env.shared_actor_infos().clone());
127        let (started_tx, started_rx) = oneshot::channel();
128        let (_collected_tx, _collected_rx) = oneshot::channel();
129
130        let notifier = Notifier {
131            started: Some(started_tx),
132            collected: Some(_collected_tx),
133        };
134
135        let new_barrier = schedule::NewBarrier {
136            database_id,
137            command: Some((
138                Command::RescheduleIntent {
139                    context: RescheduleContext::empty(),
140                    reschedule_plan: None,
141                },
142                vec![notifier],
143            )),
144            span: tracing::Span::none(),
145            checkpoint: false,
146        };
147
148        let result =
149            resolve_reschedule_intent(env, HashMap::new(), Some(&database_info), new_barrier);
150
151        assert!(matches!(result, Ok(None)));
152        let started = started_rx.await.expect("started notifier dropped");
153        assert!(started.is_err());
154    }
155}
156
157impl<C: GlobalBarrierWorkerContext> GlobalBarrierWorker<C> {
158    pub(super) async fn new_inner(
159        env: MetaSrvEnv,
160        request_rx: mpsc::UnboundedReceiver<BarrierManagerRequest>,
161        context: Arc<C>,
162    ) -> Self {
163        let enable_recovery = env.opts.enable_recovery;
164
165        let active_streaming_nodes = ActiveStreamingWorkerNodes::uninitialized();
166
167        let partial_graph_manager = PartialGraphManager::uninitialized(env.clone());
168
169        let reader = env.system_params_reader().await;
170        let system_enable_per_database_isolation = reader.per_database_isolation();
171        // Load config will be performed in bootstrap phase.
172        let periodic_barriers = PeriodicBarriers::default();
173
174        let checkpoint_control = CheckpointControl::new(env.clone());
175        Self {
176            enable_recovery,
177            periodic_barriers,
178            system_enable_per_database_isolation,
179            context,
180            env,
181            checkpoint_control,
182            completing_task: CompletingTask::None,
183            request_rx,
184            active_streaming_nodes,
185            partial_graph_manager,
186        }
187    }
188}
189
190fn resolve_reschedule_intent(
191    env: MetaSrvEnv,
192    worker_nodes: HashMap<WorkerId, WorkerNode>,
193    database_info: Option<&InflightDatabaseInfo>,
194    mut new_barrier: schedule::NewBarrier,
195) -> MetaResult<Option<schedule::NewBarrier>> {
196    let Some((command, notifiers)) = new_barrier.command.take() else {
197        return Ok(Some(new_barrier));
198    };
199
200    match command {
201        Command::RescheduleIntent {
202            context,
203            reschedule_plan,
204        } => {
205            if let Some(reschedule_plan) = reschedule_plan {
206                new_barrier.command = Some((
207                    Command::RescheduleIntent {
208                        context,
209                        reschedule_plan: Some(reschedule_plan),
210                    },
211                    notifiers,
212                ));
213                return Ok(Some(new_barrier));
214            }
215            let span = tracing::info_span!(
216                "resolve_reschedule_intent",
217                database_id = %new_barrier.database_id
218            );
219            let reschedule_plan = {
220                let _guard = span.enter();
221                build_reschedule_from_context(
222                    &env,
223                    worker_nodes,
224                    new_barrier.database_id,
225                    context,
226                    database_info.ok_or_else(|| {
227                        anyhow!(
228                            "database {} not found when resolving reschedule intent",
229                            new_barrier.database_id
230                        )
231                    })?,
232                )
233            };
234            match reschedule_plan {
235                Ok(Some(reschedule_plan)) => {
236                    new_barrier.command = Some((
237                        Command::RescheduleIntent {
238                            context: RescheduleContext::empty(),
239                            reschedule_plan: Some(reschedule_plan),
240                        },
241                        notifiers,
242                    ));
243                    Ok(Some(new_barrier))
244                }
245                Ok(None) => {
246                    // No-op intent: notify to unblock callers even though no barrier is injected.
247                    for mut notifier in notifiers {
248                        notifier.notify_started();
249                        notifier.notify_collected();
250                    }
251                    Ok(None)
252                }
253                Err(err) => {
254                    for notifier in notifiers {
255                        notifier.notify_start_failed(err.clone());
256                    }
257                    Ok(None)
258                }
259            }
260        }
261        _ => {
262            new_barrier.command = Some((command, notifiers));
263            Ok(Some(new_barrier))
264        }
265    }
266}
267
268fn build_reschedule_from_context(
269    env: &MetaSrvEnv,
270    worker_nodes: HashMap<WorkerId, WorkerNode>,
271    database_id: DatabaseId,
272    context: RescheduleContext,
273    database_info: &InflightDatabaseInfo,
274) -> MetaResult<Option<crate::barrier::ReschedulePlan>> {
275    if worker_nodes.is_empty() {
276        return Err(anyhow!("no active streaming workers for reschedule").into());
277    }
278
279    if context.is_empty() {
280        return Ok(None);
281    }
282
283    // Barrier worker resolves this intent against a stable in-flight snapshot.
284    // Reuse the same fragment view for preview comparison and command building.
285    let all_prev_fragments = database_info
286        .fragment_infos()
287        .map(|fragment| (fragment.fragment_id, fragment))
288        .collect();
289
290    let previewed = preview_actor_assignments(&worker_nodes, &context.loaded)?;
291
292    if rendered_layout_matches_current(&previewed.fragments, &all_prev_fragments)? {
293        return Ok(None);
294    }
295
296    let actor_id_counter = env.actor_id_generator();
297    // Materialization only replaces preview actor ids with real ids. Worker
298    // placement, vnode ownership, and split assignment remain unchanged.
299    let rendered = materialize_actor_assignments(actor_id_counter, previewed);
300    let mut commands = build_reschedule_commands(rendered.fragments, context, all_prev_fragments)?;
301    Ok(commands.remove(&database_id))
302}
303
304impl GlobalBarrierWorker<GlobalBarrierWorkerContextImpl> {
305    /// Create a new [`crate::barrier::worker::GlobalBarrierWorker`].
306    #[expect(clippy::too_many_arguments)]
307    pub async fn new(
308        scheduled_barriers: schedule::ScheduledBarriers,
309        env: MetaSrvEnv,
310        metadata_manager: MetadataManager,
311        hummock_manager: HummockManagerRef,
312        serving_vnode_mapping: ServingVnodeMappingRef,
313        source_manager: SourceManagerRef,
314        sink_manager: SinkCoordinatorManager,
315        iceberg_pk_index_sink_manager: IcebergPkIndexSinkManager,
316        iceberg_compaction_manager: IcebergCompactionManagerRef,
317        scale_controller: ScaleControllerRef,
318        request_rx: mpsc::UnboundedReceiver<BarrierManagerRequest>,
319        barrier_scheduler: schedule::BarrierScheduler,
320        refresh_manager: GlobalRefreshManagerRef,
321    ) -> Self {
322        let status = Arc::new(ArcSwap::new(Arc::new(BarrierManagerStatus::Starting)));
323
324        let context = Arc::new(GlobalBarrierWorkerContextImpl::new(
325            scheduled_barriers,
326            status,
327            metadata_manager,
328            hummock_manager,
329            serving_vnode_mapping,
330            source_manager,
331            scale_controller,
332            env.clone(),
333            barrier_scheduler,
334            refresh_manager,
335            sink_manager,
336            iceberg_pk_index_sink_manager,
337            iceberg_compaction_manager,
338        ));
339
340        Self::new_inner(env, request_rx, context).await
341    }
342
343    pub fn start(self) -> (JoinHandle<()>, Sender<()>) {
344        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
345        let fut = (self.env.await_tree_reg())
346            .register_derived_root("Global Barrier Worker")
347            .instrument(self.run(shutdown_rx));
348        let join_handle = tokio::spawn(fut);
349
350        (join_handle, shutdown_tx)
351    }
352
353    /// Check whether we should pause on bootstrap from the system parameter and reset it.
354    async fn take_pause_on_bootstrap(&mut self) -> MetaResult<bool> {
355        let paused = self
356            .env
357            .system_params_reader()
358            .await
359            .pause_on_next_bootstrap()
360            || self.env.opts.pause_on_next_bootstrap_offline;
361
362        if paused {
363            warn!(
364                "The cluster will bootstrap with all data sources paused as specified by the system parameter `{}`. \
365                 It will now be reset to `false`. \
366                 To resume the data sources, either restart the cluster again or use `risectl meta resume`.",
367                PAUSE_ON_NEXT_BOOTSTRAP_KEY
368            );
369            self.env
370                .system_params_manager_impl_ref()
371                .set_param(PAUSE_ON_NEXT_BOOTSTRAP_KEY, Some("false".to_owned()))
372                .await?;
373        }
374        Ok(paused)
375    }
376
377    /// Start an infinite loop to take scheduled barriers and send them.
378    async fn run(mut self, shutdown_rx: Receiver<()>) {
379        tracing::info!(
380            "Starting barrier manager with: enable_recovery={}, in_flight_barrier_nums={}",
381            self.enable_recovery,
382            self.checkpoint_control.in_flight_barrier_nums,
383        );
384
385        if !self.enable_recovery {
386            let job_exist = self
387                .context
388                .metadata_manager
389                .catalog_controller
390                .has_any_streaming_jobs()
391                .await
392                .unwrap();
393            if job_exist {
394                panic!(
395                    "Some streaming jobs already exist in meta, please start with recovery enabled \
396                or clean up the metadata using `./risedev clean-data`"
397                );
398            }
399        }
400
401        {
402            // Bootstrap recovery. Here we simply trigger a recovery process to achieve the
403            // consistency.
404            // Even if there's no actor to recover, we still go through the recovery process to
405            // inject the first `Initial` barrier.
406            let span = tracing::info_span!("bootstrap_recovery");
407            crate::telemetry::report_event(
408                risingwave_pb::telemetry::TelemetryEventStage::Recovery,
409                "normal_recovery",
410                0,
411                None,
412                None,
413                None,
414            );
415
416            let paused = self.take_pause_on_bootstrap().await.unwrap_or(false);
417
418            // Keep the bootstrap recovery future boxed so the outer barrier worker future
419            // stays below clippy's large-futures threshold.
420            Box::pin(
421                self.recovery(paused, RecoveryReason::Bootstrap)
422                    .instrument(span),
423            )
424            .await;
425        }
426
427        Box::pin(self.run_inner(shutdown_rx)).await
428    }
429}
430
431impl<C: GlobalBarrierWorkerContext> GlobalBarrierWorker<C> {
432    fn enable_per_database_isolation(&self) -> bool {
433        self.system_enable_per_database_isolation && {
434            if let Err(e) =
435                risingwave_common::license::Feature::DatabaseFailureIsolation.check_available()
436            {
437                warn!(error = %e.as_report(), "DatabaseFailureIsolation disabled by license");
438                false
439            } else {
440                true
441            }
442        }
443    }
444
445    async fn resolve_since_timestamp_snapshot_backfill(
446        &mut self,
447        new_barrier: &mut schedule::NewBarrier,
448    ) -> MetaResult<bool> {
449        let Some((
450            Command::CreateStreamingJob {
451                job_type:
452                    CreateStreamingJobType::SnapshotBackfill {
453                        snapshot_backfill_info,
454                        since_epoch: Some(since_epoch),
455                    },
456                ..
457            },
458            notifiers,
459        )) = &mut new_barrier.command
460        else {
461            return Ok(true);
462        };
463        let since_timestamp_epoch = since_epoch.provided_since_epoch;
464
465        // Complete any inflight command first so the committed upstream epoch and
466        // table changelog view used for since_timestamp resolution cannot be stale.
467        match self.completing_task.wait_completing_task().await {
468            Ok(Some(output)) => self
469                .checkpoint_control
470                .ack_completed(&mut self.partial_graph_manager, output),
471            Ok(None) => {}
472            Err(err) => {
473                error!(
474                    err = %err.as_report(),
475                    "failed to wait completing task before resolving since_timestamp"
476                );
477                return Err(err);
478            }
479        }
480
481        match self
482            .context
483            .resolve_log_store_epoch(
484                snapshot_backfill_info
485                    .upstream_mv_table_id_to_backfill_epoch
486                    .keys()
487                    .copied(),
488                since_timestamp_epoch,
489            )
490            .await
491        {
492            Ok(upstream_log_epochs) => {
493                since_epoch.resolved = Some(upstream_log_epochs);
494                Ok(true)
495            }
496            Err(err) => {
497                error!(
498                    err = %err.as_report(),
499                    "failed to resolve log store epoch for since_timestamp"
500                );
501                for notifier in take(notifiers) {
502                    notifier.notify_start_failed(err.clone());
503                }
504                Ok(false)
505            }
506        }
507    }
508
509    pub(super) async fn run_inner(mut self, mut shutdown_rx: Receiver<()>) {
510        let (local_notification_tx, mut local_notification_rx) =
511            tokio::sync::mpsc::unbounded_channel();
512        self.env
513            .notification_manager()
514            .insert_local_sender(local_notification_tx);
515
516        // Start the event loop.
517        loop {
518            tokio::select! {
519                biased;
520
521                // Shutdown
522                _ = &mut shutdown_rx => {
523                    tracing::info!("Barrier manager is stopped");
524                    break;
525                }
526
527                request = self.request_rx.recv() => {
528                    if let Some(request) = request {
529                        match request {
530                            BarrierManagerRequest::GetBackfillProgress(result_tx) => {
531                                let progress = self.checkpoint_control.gen_backfill_progress();
532                                if result_tx.send(Ok(progress)).is_err() {
533                                    error!("failed to send get ddl progress");
534                                }
535                            }
536                            BarrierManagerRequest::GetFragmentBackfillProgress(result_tx) => {
537                                let progress =
538                                    self.checkpoint_control.gen_fragment_backfill_progress();
539                                if result_tx.send(Ok(progress)).is_err() {
540                                    error!("failed to send get fragment backfill progress");
541                                }
542                            }
543                            BarrierManagerRequest::GetCdcProgress(result_tx) => {
544                                let progress = self.checkpoint_control.gen_cdc_progress();
545                                if result_tx.send(Ok(progress)).is_err() {
546                                    error!("failed to send get ddl progress");
547                                }
548                            }
549                            // Handle adhoc recovery triggered by user.
550                            BarrierManagerRequest::AdhocRecovery(sender) => {
551                                self.adhoc_recovery().await;
552                                if sender.send(()).is_err() {
553                                    warn!("failed to notify finish of adhoc recovery");
554                                }
555                            }
556                            BarrierManagerRequest::UpdateDatabaseBarrier( UpdateDatabaseBarrierRequest {
557                                database_id,
558                                barrier_interval_ms,
559                                checkpoint_frequency,
560                                sender,
561                            }) => {
562                                self.periodic_barriers
563                                    .update_database_barrier(
564                                        database_id,
565                                        barrier_interval_ms,
566                                        checkpoint_frequency,
567                                    );
568                                if sender.send(()).is_err() {
569                                    warn!("failed to notify finish of update database barrier");
570                                }
571                            }
572                        }
573                    } else {
574                        tracing::info!("end of request stream. meta node may be shutting down. Stop global barrier manager");
575                        return;
576                    }
577                }
578
579                changed_worker = self.active_streaming_nodes.changed() => {
580                    #[cfg(debug_assertions)]
581                    {
582                        self.active_streaming_nodes.validate_change().await;
583                    }
584
585                    info!(?changed_worker, "worker changed");
586
587                    match changed_worker {
588                        ActiveStreamingWorkerChange::Add(node)
589                        | ActiveStreamingWorkerChange::Update(node) => {
590                            self.partial_graph_manager
591                                .add_worker(node, self.context.clone())
592                                .await;
593                        }
594                        ActiveStreamingWorkerChange::Remove(node) => {
595                            self.partial_graph_manager.remove_worker(node);
596                        }
597                    }
598                }
599
600                notification = local_notification_rx.recv() => {
601                    let notification = notification.unwrap();
602                    if let LocalNotification::SystemParamsChange(p) = notification {
603                        {
604                            self.periodic_barriers.set_sys_barrier_interval(Duration::from_millis(p.barrier_interval_ms() as u64));
605                            self.periodic_barriers
606                                .set_sys_checkpoint_frequency(p.checkpoint_frequency());
607                            self.system_enable_per_database_isolation = p.per_database_isolation();
608                        }
609                    }
610                }
611                complete_result = self
612                    .completing_task
613                    .next_completed_barrier(
614                        &mut self.periodic_barriers,
615                        &mut self.checkpoint_control,
616                        &mut self.partial_graph_manager,
617                        &self.context,
618                        &self.env,
619                ) => {
620                    match complete_result {
621                        Ok(output) => {
622                            self.checkpoint_control.ack_completed(&mut self.partial_graph_manager, output);
623                        }
624                        Err(e) => {
625                            self.failure_recovery(e).await;
626                        }
627                    }
628                },
629                event = self.checkpoint_control.next_event() => {
630                    let result: MetaResult<()> = try {
631                        match event {
632                            CheckpointControlEvent::EnteringInitializing(entering_initializing) => {
633                                let database_id = entering_initializing.database_id();
634                                let error = merge_node_rpc_errors(&format!("database {} reset", database_id), entering_initializing.action.0.iter().filter_map(|(worker_id, resp)| {
635                                    resp.root_err.as_ref().map(|root_err| {
636                                        (*worker_id, ScoredError {
637                                            error: Status::internal(&root_err.err_msg),
638                                            score: Score(root_err.score)
639                                        })
640                                    })
641                                }));
642                                Self::report_collect_failure(&self.env, &error);
643                                self.context.notify_creating_job_failed(Some(database_id), format!("{}", error.as_report())).await;
644                                let result: MetaResult<_> = try {
645                                    let runtime_info = self.context.reload_database_runtime_info(database_id).await.inspect_err(|err| {
646                                        warn!(%database_id, err = %err.as_report(), "reload runtime info failed");
647                                    })?;
648                                    let rendered_info = render_runtime_info(
649                                        self.env.actor_id_generator(),
650                                        &self.active_streaming_nodes,
651                                        &runtime_info.recovery_context,
652                                        database_id,
653                                    )
654                                    .inspect_err(|err: &MetaError| {
655                                        warn!(%database_id, err = %err.as_report(), "render runtime info failed");
656                                    })?;
657                                    if let Some(rendered_info) = rendered_info {
658                                        BarrierWorkerRuntimeInfoSnapshot::validate_database_info(
659                                            database_id,
660                                            &rendered_info.job_infos,
661                                            &self.active_streaming_nodes,
662                                            &rendered_info.stream_actors,
663                                            &runtime_info.state_table_committed_epochs,
664                                        )
665                                        .inspect_err(|err| {
666                                            warn!(%database_id, err = ?err.as_report(), "database runtime info failed validation");
667                                        })?;
668                                        Some((runtime_info, rendered_info))
669                                    } else {
670                                        None
671                                    }
672                                };
673                                match result {
674                                    Ok(Some((runtime_info, rendered_info))) => {
675                                        entering_initializing.enter(
676                                            runtime_info,
677                                            rendered_info,
678                                            &mut self.partial_graph_manager,
679                                        );
680                                    }
681                                    Ok(None) => {
682                                        info!(%database_id, "database removed after reloading empty runtime info");
683                                        entering_initializing.remove();
684                                        self.context
685                                            .refresh_table_refill_runtime_state_after_recovery()
686                                            .await?;
687                                        // Mark ready only after the refill runtime state is refreshed.
688                                        self.context.mark_ready(MarkReadyOptions::Database(database_id));
689                                    }
690                                    Err(e) => {
691                                        entering_initializing.fail_reload_runtime_info(e);
692                                    }
693                                }
694                            }
695                            CheckpointControlEvent::EnteringRunning(entering_running) => {
696                                let database_id = entering_running.database_id();
697                                entering_running.enter();
698                                self.context
699                                    .refresh_table_refill_runtime_state_after_recovery()
700                                    .await?;
701                                self.context
702                                    .mark_ready(MarkReadyOptions::Database(database_id));
703                            }
704                            CheckpointControlEvent::BatchRefreshTrigger { database_id, job_id } => {
705                                self.handle_batch_refresh_trigger(database_id, job_id).await?;
706                            }
707                        }
708                    };
709                    if let Err(e) = result {
710                        self.failure_recovery(e).await;
711                    }
712                }
713                event = self.partial_graph_manager.next_event(&self.context) => {
714                    let result: MetaResult<()> = try {
715                        match event {
716                            PartialGraphManagerEvent::Worker(_worker_id, WorkerEvent::WorkerConnected) => {
717                                // no handling on new worker connected event yet
718                            }
719                            PartialGraphManagerEvent::Worker(worker_id, WorkerEvent::WorkerError { err, affected_partial_graphs }) => {
720                                let failed_databases = self
721                                    .checkpoint_control
722                                    .databases_failed_at_worker_err(worker_id)
723                                    .chain(
724                                        affected_partial_graphs
725                                        .into_iter()
726                                        .map(|partial_graph_id| {
727                                            let (database_id, _) = from_partial_graph_id(partial_graph_id);
728                                            database_id
729                                        })
730                                    )
731                                    .collect::<HashSet<_>>();
732                                if !failed_databases.is_empty() {
733                                    if !self.enable_recovery {
734                                        panic!("control stream to worker {} failed but recovery not enabled: {}", worker_id, err.as_report());
735                                    }
736                                    if !self.enable_per_database_isolation() {
737                                        Err(err.clone())?;
738                                    }
739                                    Self::report_collect_failure(&self.env, &err);
740                                    for database_id in failed_databases {
741                                        if let Some(entering_recovery) = self.checkpoint_control.on_report_failure(database_id, &mut self.partial_graph_manager) {
742                                            warn!(%worker_id, %database_id, "database entering recovery on node failure");
743                                            self.context.abort_and_mark_blocked(Some(database_id), RecoveryReason::Failover(anyhow!("reset database: {}", database_id).into()));
744                                            self.context.notify_creating_job_failed(Some(database_id), format!("database {} reset due to node {} failure: {}", database_id, worker_id, err.as_report())).await;
745                                            // TODO: add log on blocking time
746                                            let output = self.completing_task.wait_completing_task().await?;
747                                            entering_recovery.enter(output, &mut self.partial_graph_manager);
748                                        }
749                                    }
750                                }  else {
751                                    warn!(%worker_id, "no barrier to collect from worker, ignore err");
752                                }
753                                continue;
754                            }
755                            PartialGraphManagerEvent::PartialGraph(partial_graph_id, event) => {
756                                let (database_id, _creating_job_id) = from_partial_graph_id(partial_graph_id);
757                                match event {
758                                    PartialGraphEvent::BarrierCollected(collected_barrier) => {
759                                        self.checkpoint_control.barrier_collected(partial_graph_id, collected_barrier, &mut self.periodic_barriers)?;
760                                    }
761                                    PartialGraphEvent::Error(worker_id) => {
762                                        if !self.enable_recovery {
763                                            panic!("database {database_id} failure reported from {worker_id} but recovery not enabled")
764                                        }
765                                        if !self.enable_per_database_isolation() {
766                                                Err(MetaError::from(anyhow!("database {database_id} report failure from {worker_id}")))?;
767                                            }
768                                        if let Some(entering_recovery) = self.checkpoint_control.on_report_failure(database_id, &mut self.partial_graph_manager) {
769                                            warn!(%database_id, "database entering recovery");
770                                            self.context.abort_and_mark_blocked(Some(database_id), RecoveryReason::Failover(anyhow!("reset database: {}", database_id).into()));
771                                            // TODO: add log on blocking time
772                                            let output = self.completing_task.wait_completing_task().await?;
773                                            entering_recovery.enter(output, &mut self.partial_graph_manager);
774                                        }
775                                    }
776                                    PartialGraphEvent::Reset(reset_resps) => {
777                                        self.checkpoint_control.on_partial_graph_reset(partial_graph_id, reset_resps);
778                                    }
779                                    PartialGraphEvent::Initialized => {
780                                        self.checkpoint_control.on_partial_graph_initialized(
781                                            partial_graph_id,
782                                            &mut self.partial_graph_manager,
783                                        )?;
784                                    }
785                                }
786                            }
787                        };
788                    };
789                    if let Err(e) = result {
790                        self.failure_recovery(e).await;
791                    }
792                }
793                new_barrier = self.periodic_barriers.next_barrier(&*self.context) => {
794                    let database_id = new_barrier.database_id;
795                    let mut new_barrier = if matches!(
796                        new_barrier.command,
797                        Some((Command::RescheduleIntent { .. }, _))
798                    ) {
799                        let env = self.env.clone();
800                        let worker_nodes = self
801                            .active_streaming_nodes
802                            .current()
803                            .iter()
804                            .map(|(worker_id, worker)| (*worker_id, worker.clone()))
805                            .collect();
806                        let database_info = self.checkpoint_control.database_info(database_id);
807                        match resolve_reschedule_intent(
808                            env,
809                            worker_nodes,
810                            database_info,
811                            new_barrier,
812                        ) {
813                            Ok(Some(new_barrier)) => new_barrier,
814                            Ok(None) => continue,
815                            Err(err) => {
816                                self.failure_recovery(err).await;
817                                continue;
818                            }
819                        }
820                    } else {
821                        new_barrier
822                    };
823                    match self
824                        .resolve_since_timestamp_snapshot_backfill(&mut new_barrier)
825                        .await
826                    {
827                        Ok(true) => {}
828                        Ok(false) => continue,
829                        Err(err) => {
830                            self.failure_recovery(err).await;
831                            continue;
832                        }
833                    }
834                    if let Err(e) = self.checkpoint_control.handle_new_barrier(
835                        new_barrier,
836                        &mut self.partial_graph_manager,
837                        self.active_streaming_nodes.current()
838                    ) {
839                        if !self.enable_recovery {
840                            panic!(
841                                "failed to inject barrier to some databases but recovery not enabled: {:?}", (
842                                    database_id,
843                                    e.as_report()
844                                )
845                            );
846                        }
847                        let result: MetaResult<_> = try {
848                            if !self.enable_per_database_isolation() {
849                                let err = anyhow!("failed to inject barrier to databases: {:?}", (database_id, e.as_report()));
850                                Err(MetaError::from(err))?;
851                            } else if let Some(entering_recovery) = self.checkpoint_control.on_report_failure(database_id, &mut self.partial_graph_manager) {
852                                warn!(%database_id, e = %e.as_report(),"database entering recovery on inject failure");
853                                self.context.abort_and_mark_blocked(Some(database_id), RecoveryReason::Failover(anyhow!(e).context("inject barrier failure").into()));
854                                // TODO: add log on blocking time
855                                let output = self.completing_task.wait_completing_task().await?;
856                                entering_recovery.enter(output, &mut self.partial_graph_manager);
857                            }
858                        };
859                        if let Err(e) = result {
860                            self.failure_recovery(e).await;
861                        }
862                    }
863                }
864            }
865        }
866    }
867}
868
869impl<C: GlobalBarrierWorkerContext> GlobalBarrierWorker<C> {
870    /// We need to make sure there are no changes when doing recovery
871    pub async fn clear_on_err(&mut self, err: &MetaError) {
872        // join spawned completing command to finish no matter it succeeds or not.
873        match replace(&mut self.completing_task, CompletingTask::None) {
874            CompletingTask::None | CompletingTask::Err(_) => {}
875            CompletingTask::Completing {
876                epochs_to_ack,
877                join_handle,
878                ..
879            } => {
880                info!("waiting for completing command to finish in recovery");
881                match join_handle.await {
882                    Err(e) => {
883                        warn!(err = %e.as_report(), "failed to join completing task");
884                    }
885                    Ok(Err(e)) => {
886                        warn!(
887                            err = %e.as_report(),
888                            "failed to complete barrier during clear"
889                        );
890                    }
891                    Ok(Ok(hummock_version_stats)) => {
892                        self.checkpoint_control.ack_completed(
893                            &mut self.partial_graph_manager,
894                            BarrierCompleteOutput {
895                                epochs_to_ack,
896                                hummock_version_stats,
897                            },
898                        );
899                    }
900                }
901            }
902        };
903        self.partial_graph_manager.notify_all_err(err);
904    }
905}
906
907impl<C: GlobalBarrierWorkerContext> GlobalBarrierWorker<C> {
908    /// Handle a batch refresh trigger: load metadata + log epochs, then start a logstore
909    /// consumption run for the given batch refresh job.
910    async fn handle_batch_refresh_trigger(
911        &mut self,
912        database_id: DatabaseId,
913        job_id: JobId,
914    ) -> MetaResult<()> {
915        // 1. Get the last committed epoch for this job (read-only).
916        let last_committed_epoch = self
917            .checkpoint_control
918            .get_batch_refresh_trigger_info(database_id, job_id);
919
920        // 2. Load context metadata + resolve log epochs asynchronously.
921        let context = self
922            .context
923            .load_batch_refresh_trigger_context(job_id, database_id, last_committed_epoch)
924            .await?;
925
926        // 3. Start the refresh run.
927        let started = self.checkpoint_control.start_batch_refresh_run(
928            database_id,
929            job_id,
930            &context,
931            self.active_streaming_nodes.current(),
932            self.env.actor_id_generator(),
933            &mut self.partial_graph_manager,
934        )?;
935
936        // 5. Update shared_actor_infos with the new fragment infos.
937        if started {
938            self.checkpoint_control
939                .apply_batch_refresh_fragment_infos(database_id, job_id);
940        }
941
942        Ok(())
943    }
944}
945
946impl<C: GlobalBarrierWorkerContext> GlobalBarrierWorker<C> {
947    /// Set barrier manager status.
948    async fn failure_recovery(&mut self, err: MetaError) {
949        self.clear_on_err(&err).await;
950
951        if self.enable_recovery {
952            let span = tracing::info_span!(
953                "failure_recovery",
954                error = %err.as_report(),
955            );
956
957            crate::telemetry::report_event(
958                risingwave_pb::telemetry::TelemetryEventStage::Recovery,
959                "failure_recovery",
960                0,
961                None,
962                None,
963                None,
964            );
965
966            let reason = RecoveryReason::Failover(err);
967
968            // No need to clean dirty tables for barrier recovery,
969            // The foreground stream job should cleanup their own tables.
970            self.recovery(false, reason).instrument(span).await;
971        } else {
972            panic!(
973                "a streaming error occurred while recovery is disabled, aborting: {:?}",
974                err.as_report()
975            );
976        }
977    }
978
979    async fn adhoc_recovery(&mut self) {
980        let err = MetaErrorInner::AdhocRecovery.into();
981        self.clear_on_err(&err).await;
982
983        let span = tracing::info_span!(
984            "adhoc_recovery",
985            error = %err.as_report(),
986        );
987
988        crate::telemetry::report_event(
989            risingwave_pb::telemetry::TelemetryEventStage::Recovery,
990            "adhoc_recovery",
991            0,
992            None,
993            None,
994            None,
995        );
996
997        // No need to clean dirty tables for barrier recovery,
998        // The foreground stream job should cleanup their own tables.
999        self.recovery(false, RecoveryReason::Adhoc)
1000            .instrument(span)
1001            .await;
1002    }
1003}
1004
1005impl<C> GlobalBarrierWorker<C> {
1006    /// Send barrier-complete-rpc and wait for responses from all CNs
1007    pub(super) fn report_collect_failure(env: &MetaSrvEnv, error: &MetaError) {
1008        // Record failure in event log.
1009        use risingwave_pb::meta::event_log;
1010        let event = event_log::EventCollectBarrierFail {
1011            error: error.to_report_string(),
1012        };
1013        env.event_log_manager_ref()
1014            .add_event_logs(vec![event_log::Event::CollectBarrierFail(event)]);
1015    }
1016}
1017
1018mod retry_strategy {
1019    use std::time::Duration;
1020
1021    use tokio_retry::strategy::{ExponentialBackoff, jitter};
1022
1023    // Retry base interval in milliseconds.
1024    const RECOVERY_RETRY_BASE_INTERVAL: u64 = 20;
1025    // Retry max interval.
1026    const RECOVERY_RETRY_MAX_INTERVAL: Duration = Duration::from_secs(5);
1027
1028    // MrCroxx: Use concrete type here to prevent unsolved compiler issue.
1029    // Feel free to replace the concrete type with TAIT after fixed.
1030
1031    // mod retry_backoff_future {
1032    //     use std::future::Future;
1033    //     use std::time::Duration;
1034    //
1035    //     use tokio::time::sleep;
1036    //
1037    //     pub(crate) type RetryBackoffFuture = impl Future<Output = ()> + Unpin + Send + 'static;
1038    //
1039    //     #[define_opaque(RetryBackoffFuture)]
1040    //     pub(super) fn get_retry_backoff_future(duration: Duration) -> RetryBackoffFuture {
1041    //         Box::pin(sleep(duration))
1042    //     }
1043    // }
1044    // pub(crate) use retry_backoff_future::*;
1045
1046    pub(crate) type RetryBackoffFuture = std::pin::Pin<Box<tokio::time::Sleep>>;
1047
1048    pub(crate) fn get_retry_backoff_future(duration: Duration) -> RetryBackoffFuture {
1049        Box::pin(tokio::time::sleep(duration))
1050    }
1051
1052    pub(crate) type RetryBackoffStrategy =
1053        impl Iterator<Item = RetryBackoffFuture> + Send + 'static;
1054
1055    /// Initialize a retry strategy for operation in recovery.
1056    #[inline(always)]
1057    pub(crate) fn get_retry_strategy() -> impl Iterator<Item = Duration> + Send + 'static {
1058        ExponentialBackoff::from_millis(RECOVERY_RETRY_BASE_INTERVAL)
1059            .max_delay(RECOVERY_RETRY_MAX_INTERVAL)
1060            .map(jitter)
1061    }
1062
1063    #[define_opaque(RetryBackoffStrategy)]
1064    pub(crate) fn get_retry_backoff_strategy() -> RetryBackoffStrategy {
1065        get_retry_strategy().map(get_retry_backoff_future)
1066    }
1067}
1068
1069pub(crate) use retry_strategy::*;
1070use risingwave_common::error::tonic::extra::{Score, ScoredError};
1071use risingwave_pb::meta::event_log::{Event, EventRecovery};
1072
1073use crate::barrier::partial_graph::{
1074    PartialGraphEvent, PartialGraphManager, PartialGraphManagerEvent, WorkerEvent,
1075};
1076
1077impl<C: GlobalBarrierWorkerContext> GlobalBarrierWorker<C> {
1078    /// Recovery the whole cluster from the latest epoch.
1079    ///
1080    /// If `paused_reason` is `Some`, all data sources (including connectors and DMLs) will be
1081    /// immediately paused after recovery, until the user manually resume them either by restarting
1082    /// the cluster or `risectl` command. Used for debugging purpose.
1083    ///
1084    /// Returns the new state of the barrier manager after recovery.
1085    pub async fn recovery(&mut self, is_paused: bool, recovery_reason: RecoveryReason) {
1086        // Clear all control streams to release resources (connections to compute nodes) first.
1087        self.partial_graph_manager.clear_worker();
1088
1089        let reason_str = match &recovery_reason {
1090            RecoveryReason::Bootstrap => "bootstrap".to_owned(),
1091            RecoveryReason::Failover(err) => {
1092                format!("failed over: {}", err.as_report())
1093            }
1094            RecoveryReason::Adhoc => "adhoc recovery".to_owned(),
1095        };
1096        self.context.abort_and_mark_blocked(None, recovery_reason);
1097
1098        self.recovery_inner(is_paused, reason_str).await;
1099        self.context.mark_ready(MarkReadyOptions::Global {
1100            blocked_databases: self.checkpoint_control.recovering_databases().collect(),
1101        });
1102    }
1103
1104    #[await_tree::instrument("recovery({recovery_reason})")]
1105    async fn recovery_inner(&mut self, is_paused: bool, recovery_reason: String) {
1106        let event_log_manager_ref = self.env.event_log_manager_ref();
1107
1108        tracing::info!("recovery start!");
1109        event_log_manager_ref.add_event_logs(vec![Event::Recovery(
1110            EventRecovery::global_recovery_start(recovery_reason.clone()),
1111        )]);
1112
1113        let retry_strategy = get_retry_strategy();
1114
1115        // We take retry into consideration because this is the latency user sees for a cluster to
1116        // get recovered.
1117        let recovery_timer = GLOBAL_META_METRICS
1118            .recovery_latency
1119            .with_label_values(&["global"])
1120            .start_timer();
1121
1122        let enable_per_database_isolation = self.enable_per_database_isolation();
1123
1124        let recovery_future = tokio_retry::Retry::spawn(retry_strategy, || async {
1125            self.env.stream_client_pool().invalidate_all();
1126            // We need to notify_creating_job_failed in every recovery retry, because in outer create_streaming_job handler,
1127            // it holds the reschedule_read_lock and wait for creating job to finish, and caused the following scale_actor fail
1128            // to acquire the reschedule_write_lock, and then keep recovering, and then deadlock.
1129            // TODO: refactor and fix this hacky implementation.
1130            self.context
1131                .notify_creating_job_failed(None, recovery_reason.clone())
1132                .await;
1133
1134            let runtime_info_snapshot = self
1135                .context
1136                .reload_runtime_info()
1137                .await?;
1138            let BarrierWorkerRuntimeInfoSnapshot {
1139                active_streaming_nodes,
1140                recovery_context,
1141                mut state_table_committed_epochs,
1142                mut state_table_log_epochs,
1143                mut mv_depended_subscriptions,
1144                mut background_jobs,
1145                hummock_version_stats,
1146                database_infos,
1147                mut cdc_table_snapshot_splits,
1148            } = runtime_info_snapshot;
1149
1150            let mut partial_graph_manager = PartialGraphManager::recover(
1151                    self.env.clone(),
1152                    active_streaming_nodes.current(),
1153                    self.context.clone(),
1154                )
1155                .await;
1156            {
1157                let mut empty_databases = HashSet::new();
1158                let mut collected_databases = HashMap::new();
1159                let mut collecting_databases = HashMap::new();
1160                let mut failed_databases = HashMap::new();
1161                for &database_id in recovery_context.fragment_context.database_map.keys() {
1162                    let mut recoverer = partial_graph_manager.start_recover();
1163                    let result: MetaResult<_> = try {
1164                        let Some(rendered_info) = render_runtime_info(
1165                            self.env.actor_id_generator(),
1166                            &active_streaming_nodes,
1167                            &recovery_context,
1168                            database_id,
1169                        )
1170                            .inspect_err(|err: &MetaError| {
1171                                warn!(%database_id, err = %err.as_report(), "render runtime info failed");
1172                            })? else {
1173                            empty_databases.insert(database_id);
1174                            continue;
1175                        };
1176                        BarrierWorkerRuntimeInfoSnapshot::validate_database_info(
1177                            database_id,
1178                            &rendered_info.job_infos,
1179                            &active_streaming_nodes,
1180                            &rendered_info.stream_actors,
1181                            &state_table_committed_epochs,
1182                        )
1183                        .inspect_err(|err| {
1184                            warn!(%database_id, err = %err.as_report(), "rendered runtime info failed validation");
1185                        })?;
1186                        let RenderedDatabaseRuntimeInfo {
1187                            job_infos,
1188                            stream_actors,
1189                            mut source_splits,
1190                            batch_refresh,
1191                        } = rendered_info;
1192                        recoverer.inject_database_initial_barrier(
1193                            database_id,
1194                            job_infos,
1195                            &recovery_context.job_extra_info,
1196                            &mut state_table_committed_epochs,
1197                            &mut state_table_log_epochs,
1198                            &recovery_context.fragment_relations,
1199                            &stream_actors,
1200                            &mut source_splits,
1201                            &mut background_jobs,
1202                            &mut mv_depended_subscriptions,
1203                            is_paused,
1204                            &hummock_version_stats,
1205                            &mut cdc_table_snapshot_splits,
1206                            batch_refresh,
1207                        )?
1208                    };
1209                    let collector = match result {
1210                        Ok(database) => {
1211                            DatabaseInitialBarrierCollector {
1212                                database_id,
1213                                initializing_partial_graphs: recoverer.all_initializing(),
1214                                database,
1215                            }
1216                        }
1217                        Err(e) => {
1218                            warn!(%database_id, e = %e.as_report(), "failed to inject database initial barrier");
1219                            assert!(failed_databases.insert(database_id, recoverer.failed()).is_none(), "non-duplicate");
1220                            continue;
1221                        }
1222                    };
1223                    if !collector.is_collected() {
1224                        assert!(collecting_databases.insert(database_id, collector).is_none());
1225                    } else {
1226                        warn!(%database_id, "database has no node to inject initial barrier");
1227                        assert!(collected_databases.insert(database_id, collector.finish()).is_none());
1228                    }
1229                }
1230                if !empty_databases.is_empty() {
1231                    info!(?empty_databases, "empty database in global recovery");
1232                }
1233                while !collecting_databases.is_empty() {
1234                    match partial_graph_manager.next_event(&self.context).await {
1235                        PartialGraphManagerEvent::Worker(_, WorkerEvent::WorkerConnected) => {
1236                            // not handle WorkerConnected yet
1237                        }
1238                        PartialGraphManagerEvent::Worker(worker_id, WorkerEvent::WorkerError { err, affected_partial_graphs }) => {
1239                            let affected_databases: HashSet<_> = affected_partial_graphs.into_iter().map(|partial_graph_id| {
1240                                let (database_id, _) = from_partial_graph_id(partial_graph_id);
1241                                database_id
1242                            }).collect();
1243                            warn!(%worker_id, err = %err.as_report(), "worker node failure during recovery");
1244                            for (failed_database_id, collector) in collecting_databases.extract_if(|database_id, collector| {
1245                                !collector.is_valid_after_worker_err(worker_id) || affected_databases.contains(database_id)
1246                            }) {
1247                                warn!(%failed_database_id, %worker_id, "database failed to recovery in global recovery due to worker node err");
1248                                let resetting_partial_graphs: HashSet<_> = collector.all_partial_graphs().collect();
1249                                partial_graph_manager.reset_partial_graphs(resetting_partial_graphs.iter().copied());
1250                                assert!(failed_databases.insert(failed_database_id, resetting_partial_graphs).is_none());
1251                            }
1252                        }
1253                        PartialGraphManagerEvent::PartialGraph(partial_graph_id, event) => {
1254                            match event {
1255                                PartialGraphEvent::BarrierCollected(_) => {
1256                                    unreachable!("no barrier collected event on initializing")
1257                                }
1258                                PartialGraphEvent::Reset(_) => {
1259                                    // Reset responses only carry diagnostic root errors. Recovery
1260                                    // itself only needs to track the partial graphs still resetting.
1261                                    let (database_id, _) =
1262                                        from_partial_graph_id(partial_graph_id);
1263                                    let resetting_partial_graphs = failed_databases
1264                                        .get_mut(&database_id)
1265                                        .expect("reset partial graph should belong to a failed database");
1266                                    assert!(
1267                                        resetting_partial_graphs.remove(&partial_graph_id),
1268                                        "partial graph {partial_graph_id} should be resetting"
1269                                    );
1270                                }
1271                                PartialGraphEvent::Error(worker_id) => {
1272                                    let (database_id, _) = from_partial_graph_id(partial_graph_id);
1273                                    if let Some(collector) = collecting_databases.remove(&database_id) {
1274                                        warn!(%database_id, %worker_id, "database reset during global recovery");
1275                                        let resetting_partial_graphs: HashSet<_> = collector.all_partial_graphs().collect();
1276                                        partial_graph_manager.reset_partial_graphs(resetting_partial_graphs.iter().copied());
1277                                        assert!(failed_databases.insert(database_id, resetting_partial_graphs).is_none());
1278                                    } else if let Some(database) = collected_databases.remove(&database_id) {
1279                                        warn!(%database_id, %worker_id, "database initialized but later reset during global recovery");
1280                                        let resetting_partial_graphs: HashSet<_> = database_partial_graphs(database_id, database.independent_checkpoint_job_controls.keys().copied()).collect();
1281                                        partial_graph_manager.reset_partial_graphs(resetting_partial_graphs.iter().copied());
1282                                        assert!(failed_databases.insert(database_id, resetting_partial_graphs).is_none());
1283                                    } else {
1284                                        assert!(failed_databases.contains_key(&database_id));
1285                                    }
1286                                }
1287                                PartialGraphEvent::Initialized => {
1288                                    let (database_id, _) = from_partial_graph_id(partial_graph_id);
1289                                    if failed_databases.contains_key(&database_id) {
1290                                        assert!(!collecting_databases.contains_key(&database_id));
1291                                        // ignore the lately initialized partial graph of failed database
1292                                        continue;
1293                                    }
1294                                    let Entry::Occupied(mut entry) = collecting_databases.entry(database_id) else {
1295                                        unreachable!("should exist")
1296                                    };
1297                                    let collector = entry.get_mut();
1298                                    collector.partial_graph_initialized(partial_graph_id);
1299                                    if collector.is_collected() {
1300                                        let collector = entry.remove();
1301                                        assert!(collected_databases.insert(database_id, collector.finish()).is_none());
1302                                    }
1303                                }
1304                            }
1305                        }
1306                    }
1307                }
1308                debug!("collected initial barrier");
1309                if !background_jobs.is_empty() {
1310                    warn!(job_ids = ?background_jobs.iter().collect_vec(), "unused recovered background mview in recovery");
1311                }
1312                if !mv_depended_subscriptions.is_empty() {
1313                    warn!(?mv_depended_subscriptions, "unused subscription infos in recovery");
1314                }
1315                if !state_table_committed_epochs.is_empty() {
1316                    warn!(?state_table_committed_epochs, "unused state table committed epoch in recovery");
1317                }
1318                if !enable_per_database_isolation && !failed_databases.is_empty() {
1319                    return Err(anyhow!(
1320                        "global recovery failed due to failure of databases {:?}",
1321                        failed_databases.keys().collect_vec()).into()
1322                    );
1323                }
1324                let checkpoint_control = CheckpointControl::recover(
1325                    collected_databases,
1326                    failed_databases,
1327                    hummock_version_stats,
1328                    self.env.clone(),
1329                );
1330
1331                let reader = self.env.system_params_reader().await;
1332                let checkpoint_frequency = reader.checkpoint_frequency();
1333                let barrier_interval = Duration::from_millis(reader.barrier_interval_ms() as u64);
1334                let periodic_barriers = PeriodicBarriers::new(
1335                    barrier_interval,
1336                    checkpoint_frequency,
1337                    database_infos,
1338                );
1339
1340                self.context
1341                    .refresh_table_refill_runtime_state_after_recovery()
1342                    .await?;
1343
1344                Ok((
1345                    active_streaming_nodes,
1346                    partial_graph_manager,
1347                    checkpoint_control,
1348                    periodic_barriers,
1349                ))
1350            }
1351        }.inspect_err(|err: &MetaError| {
1352            tracing::error!(error = %err.as_report(), "recovery failed");
1353            event_log_manager_ref.add_event_logs(vec![Event::Recovery(
1354                EventRecovery::global_recovery_failure(recovery_reason.clone(), err.to_report_string()),
1355            )]);
1356            GLOBAL_META_METRICS.recovery_failure_cnt.with_label_values(&["global"]).inc();
1357        }))
1358        .instrument(tracing::info_span!("recovery_attempt"));
1359
1360        let mut recover_txs = vec![];
1361        let mut update_barrier_requests = vec![];
1362        pin_mut!(recovery_future);
1363        let mut request_rx_closed = false;
1364        let new_state = loop {
1365            select! {
1366                biased;
1367                new_state = &mut recovery_future => {
1368                    break new_state.expect("Retry until recovery success.");
1369                }
1370                request = pin!(self.request_rx.recv()), if !request_rx_closed => {
1371                    let Some(request) = request else {
1372                        warn!("request rx channel closed during recovery");
1373                        request_rx_closed = true;
1374                        continue;
1375                    };
1376                    match request {
1377                        BarrierManagerRequest::GetBackfillProgress(tx) => {
1378                            let _ = tx.send(Err(anyhow!("cluster under recovery[{}]", recovery_reason).into()));
1379                        }
1380                        BarrierManagerRequest::GetFragmentBackfillProgress(tx) => {
1381                            let _ = tx.send(Err(anyhow!("cluster under recovery[{}]", recovery_reason).into()));
1382                        }
1383                        BarrierManagerRequest::GetCdcProgress(tx) => {
1384                            let _ = tx.send(Err(anyhow!("cluster under recovery[{}]", recovery_reason).into()));
1385                        }
1386                        BarrierManagerRequest::AdhocRecovery(tx) => {
1387                            recover_txs.push(tx);
1388                        }
1389                        BarrierManagerRequest::UpdateDatabaseBarrier(request) => {
1390                            update_barrier_requests.push(request);
1391                        }
1392                    }
1393                }
1394            }
1395        };
1396
1397        let duration = recovery_timer.stop_and_record();
1398
1399        (
1400            self.active_streaming_nodes,
1401            self.partial_graph_manager,
1402            self.checkpoint_control,
1403            self.periodic_barriers,
1404        ) = new_state;
1405
1406        tracing::info!("recovery success");
1407
1408        for UpdateDatabaseBarrierRequest {
1409            database_id,
1410            barrier_interval_ms,
1411            checkpoint_frequency,
1412            sender,
1413        } in update_barrier_requests
1414        {
1415            self.periodic_barriers.update_database_barrier(
1416                database_id,
1417                barrier_interval_ms,
1418                checkpoint_frequency,
1419            );
1420            let _ = sender.send(());
1421        }
1422
1423        for tx in recover_txs {
1424            let _ = tx.send(());
1425        }
1426
1427        let recovering_databases = self
1428            .checkpoint_control
1429            .recovering_databases()
1430            .map(|database| database.as_raw_id())
1431            .collect_vec();
1432        let running_databases = self
1433            .checkpoint_control
1434            .running_databases()
1435            .map(|database| database.as_raw_id())
1436            .collect_vec();
1437
1438        event_log_manager_ref.add_event_logs(vec![Event::Recovery(
1439            EventRecovery::global_recovery_success(
1440                recovery_reason.clone(),
1441                duration as f32,
1442                running_databases,
1443                recovering_databases,
1444            ),
1445        )]);
1446
1447        self.env
1448            .notification_manager()
1449            .notify_frontend_without_version(Operation::Update, Info::Recovery(Recovery {}));
1450        self.env
1451            .notification_manager()
1452            .notify_compute_without_version(Operation::Update, Info::Recovery(Recovery {}));
1453    }
1454}