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