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