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