Skip to main content

risingwave_meta/barrier/checkpoint/
recovery.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::{HashMap, HashSet};
16use std::mem::{replace, take};
17use std::task::{Context, Poll};
18
19use futures::FutureExt;
20use prometheus::{HistogramTimer, IntCounter};
21use risingwave_common::catalog::DatabaseId;
22use risingwave_common::id::JobId;
23use risingwave_meta_model::WorkerId;
24use risingwave_pb::id::PartialGraphId;
25use risingwave_pb::meta::event_log::{Event, EventRecovery};
26use risingwave_pb::stream_service::streaming_control_stream_response::ResetPartialGraphResponse;
27use thiserror_ext::AsReport;
28use tracing::{info, warn};
29
30use crate::barrier::DatabaseRuntimeInfoSnapshot;
31use crate::barrier::checkpoint::CheckpointControl;
32use crate::barrier::checkpoint::control::DatabaseCheckpointControlStatus;
33use crate::barrier::complete_task::BarrierCompleteOutput;
34use crate::barrier::context::recovery::RenderedDatabaseRuntimeInfo;
35use crate::barrier::partial_graph::PartialGraphManager;
36use crate::barrier::rpc::{DatabaseInitialBarrierCollector, to_partial_graph_id};
37use crate::barrier::worker::{
38    RetryBackoffFuture, RetryBackoffStrategy, get_retry_backoff_strategy,
39};
40use crate::rpc::metrics::GLOBAL_META_METRICS;
41use crate::{MetaError, MetaResult};
42
43/// We can treat each database as a state machine of 3 states: `Running`, `Resetting` and `Initializing`.
44/// The state transition can be triggered when receiving 3 variants of response: `ReportDatabaseFailure`, `BarrierComplete`, `DatabaseReset`.
45/// The logic of state transition can be summarized as followed:
46///
47/// `Running`
48///     - on `ReportDatabaseFailure`
49///         - wait for the inflight B`arrierCompletingTask` to finish if there is any, mark the database as blocked in command queue
50///         - send `ResetDatabaseRequest` to all CNs.
51///         - enter `Resetting` state.
52///     - on `BarrierComplete`: update the `DatabaseCheckpointControl`.
53///     - on `DatabaseReset`: unreachable
54/// `Resetting`
55///     - on `ReportDatabaseFailure` or `BarrierComplete`: ignore
56///     - on `DatabaseReset`:
57///         - mark the CN as collected.
58///         - when all CNs have collected the response:
59///             - load the database runtime info from catalog manager and fragment manager
60///             - inject the initial barrier to CNs, save the set of nodes that need to collect response
61///             - if any failure happened, re-enter `Resetting` state.
62///             - enter `Initializing` state
63/// `Initializing`
64///     - on `BarrierComplete`:
65///         - mark the CN as collected
66///         - when all CNs have collected the response: enter Running
67///     - on `ReportDatabaseFailure`
68///         - send `ResetDatabaseRequest` to all CNs
69///         - enter `Resetting`
70///     - on `DatabaseReset`: unreachable
71enum DatabaseRecoveringStage {
72    Resetting {
73        resetting_partial_graphs: HashSet<PartialGraphId>,
74        reset_resps: Vec<(WorkerId, ResetPartialGraphResponse)>,
75        backoff_future: Option<RetryBackoffFuture>,
76    },
77    Initializing {
78        initial_barrier_collector: Box<DatabaseInitialBarrierCollector>,
79    },
80}
81
82pub(crate) struct DatabaseRecoveringState {
83    stage: DatabaseRecoveringStage,
84    retry_backoff_strategy: RetryBackoffStrategy,
85    metrics: DatabaseRecoveryMetrics,
86}
87
88pub(super) enum RecoveringStateAction {
89    EnterInitializing(Vec<(WorkerId, ResetPartialGraphResponse)>),
90    EnterRunning,
91}
92
93struct DatabaseRecoveryMetrics {
94    recovery_failure_cnt: IntCounter,
95    recovery_timer: Option<HistogramTimer>,
96}
97
98impl DatabaseRecoveryMetrics {
99    fn new(database_id: DatabaseId) -> Self {
100        let database_id_str = format!("database {}", database_id);
101        Self {
102            recovery_failure_cnt: GLOBAL_META_METRICS
103                .recovery_failure_cnt
104                .with_label_values(&[database_id_str.as_str()]),
105            recovery_timer: Some(
106                GLOBAL_META_METRICS
107                    .recovery_latency
108                    .with_label_values(&[database_id_str.as_str()])
109                    .start_timer(),
110            ),
111        }
112    }
113}
114
115impl DatabaseRecoveringState {
116    pub(super) fn new_resetting(
117        database_id: DatabaseId,
118        resetting_partial_graphs: HashSet<PartialGraphId>,
119    ) -> Self {
120        let mut retry_backoff_strategy = get_retry_backoff_strategy();
121        let backoff_future = retry_backoff_strategy.next().unwrap();
122        let metrics = DatabaseRecoveryMetrics::new(database_id);
123        metrics.recovery_failure_cnt.inc();
124        Self {
125            stage: DatabaseRecoveringStage::Resetting {
126                resetting_partial_graphs,
127                reset_resps: vec![],
128                backoff_future: Some(backoff_future),
129            },
130            retry_backoff_strategy,
131            metrics,
132        }
133    }
134
135    fn next_retry(&mut self) -> RetryBackoffFuture {
136        self.retry_backoff_strategy
137            .next()
138            .expect("should not be empty")
139    }
140
141    pub(super) fn partial_graph_initialized(&mut self, partial_graph_id: PartialGraphId) {
142        match &mut self.stage {
143            DatabaseRecoveringStage::Resetting { .. } => {
144                unreachable!("should not have partial graph initialized when resetting")
145            }
146            DatabaseRecoveringStage::Initializing {
147                initial_barrier_collector,
148            } => {
149                initial_barrier_collector.partial_graph_initialized(partial_graph_id);
150            }
151        }
152    }
153
154    pub(super) fn is_valid_after_worker_err(&mut self, worker_id: WorkerId) -> bool {
155        match &mut self.stage {
156            DatabaseRecoveringStage::Resetting { .. } => true,
157            DatabaseRecoveringStage::Initializing {
158                initial_barrier_collector,
159                ..
160            } => initial_barrier_collector.is_valid_after_worker_err(worker_id),
161        }
162    }
163
164    pub(super) fn on_partial_graph_reset(
165        &mut self,
166        partial_graph_id: PartialGraphId,
167        new_reset_resps: HashMap<WorkerId, ResetPartialGraphResponse>,
168    ) {
169        match &mut self.stage {
170            DatabaseRecoveringStage::Resetting {
171                resetting_partial_graphs,
172                reset_resps,
173                ..
174            } => {
175                assert!(resetting_partial_graphs.remove(&partial_graph_id));
176                reset_resps.extend(new_reset_resps);
177            }
178            DatabaseRecoveringStage::Initializing { .. } => {
179                unreachable!("all reset resp should have been received in Resetting")
180            }
181        }
182    }
183
184    pub(super) fn poll_next_event(&mut self, cx: &mut Context<'_>) -> Poll<RecoveringStateAction> {
185        match &mut self.stage {
186            DatabaseRecoveringStage::Resetting {
187                resetting_partial_graphs,
188                reset_resps,
189                backoff_future: backoff_future_option,
190            } => {
191                let pass_backoff = if let Some(backoff_future) = backoff_future_option {
192                    if backoff_future.poll_unpin(cx).is_ready() {
193                        *backoff_future_option = None;
194                        true
195                    } else {
196                        false
197                    }
198                } else {
199                    true
200                };
201                if pass_backoff && resetting_partial_graphs.is_empty() {
202                    return Poll::Ready(RecoveringStateAction::EnterInitializing(take(
203                        reset_resps,
204                    )));
205                }
206            }
207            DatabaseRecoveringStage::Initializing {
208                initial_barrier_collector,
209                ..
210            } => {
211                if initial_barrier_collector.is_collected() {
212                    return Poll::Ready(RecoveringStateAction::EnterRunning);
213                }
214            }
215        }
216        Poll::Pending
217    }
218}
219
220pub(crate) struct DatabaseStatusAction<'a, A> {
221    control: &'a mut CheckpointControl,
222    database_id: DatabaseId,
223    pub(crate) action: A,
224}
225
226impl<A> DatabaseStatusAction<'_, A> {
227    pub(crate) fn database_id(&self) -> DatabaseId {
228        self.database_id
229    }
230}
231
232impl CheckpointControl {
233    pub(super) fn new_database_status_action<A>(
234        &mut self,
235        database_id: DatabaseId,
236        action: A,
237    ) -> DatabaseStatusAction<'_, A> {
238        DatabaseStatusAction {
239            control: self,
240            database_id,
241            action,
242        }
243    }
244}
245
246pub(crate) struct EnterReset;
247
248impl DatabaseStatusAction<'_, EnterReset> {
249    pub(crate) fn job_ids(&self) -> HashSet<JobId> {
250        let database_status = self
251            .control
252            .databases
253            .get(&self.database_id)
254            .expect("should exist");
255        match database_status {
256            DatabaseCheckpointControlStatus::Running(database) => database
257                .database_info
258                .job_ids()
259                .chain(database.independent_checkpoint_job_controls.keys().copied())
260                .collect(),
261            DatabaseCheckpointControlStatus::Recovering(_) => {
262                unreachable!("should only enter reset from a running database")
263            }
264        }
265    }
266
267    pub(crate) fn enter(
268        self,
269        barrier_complete_output: Option<BarrierCompleteOutput>,
270        partial_graph_manager: &mut PartialGraphManager,
271    ) -> MetaResult<()> {
272        let event_log_manager_ref = self.control.env.event_log_manager_ref();
273        if let Some(output) = barrier_complete_output {
274            self.control.ack_completed(partial_graph_manager, output)?;
275        }
276        let database_status = self
277            .control
278            .databases
279            .get_mut(&self.database_id)
280            .expect("should exist");
281        match database_status {
282            DatabaseCheckpointControlStatus::Running(database) => {
283                let mut resetting_partial_graphs = HashSet::new();
284                let new_reset_partial_graphs: HashSet<_> = database
285                    .independent_checkpoint_job_controls
286                    .drain()
287                    .filter_map(|(job_id, job)| {
288                        let partial_graph_id = to_partial_graph_id(self.database_id, Some(job_id));
289                        if job.reset() {
290                            resetting_partial_graphs.insert(partial_graph_id);
291                            None
292                        } else {
293                            Some(partial_graph_id)
294                        }
295                    })
296                    .chain([to_partial_graph_id(self.database_id, None)])
297                    .collect();
298                resetting_partial_graphs
299                    .iter()
300                    .for_each(|partial_graph_id| {
301                        partial_graph_manager.assert_resetting(*partial_graph_id)
302                    });
303                partial_graph_manager
304                    .reset_partial_graphs(new_reset_partial_graphs.iter().copied());
305                resetting_partial_graphs.extend(new_reset_partial_graphs);
306
307                let metrics = DatabaseRecoveryMetrics::new(self.database_id);
308                event_log_manager_ref.add_event_logs(vec![Event::Recovery(
309                    EventRecovery::database_recovery_start(self.database_id.as_raw_id()),
310                )]);
311                *database_status =
312                    DatabaseCheckpointControlStatus::Recovering(DatabaseRecoveringState {
313                        stage: DatabaseRecoveringStage::Resetting {
314                            resetting_partial_graphs,
315                            reset_resps: vec![],
316                            backoff_future: None,
317                        },
318                        retry_backoff_strategy: get_retry_backoff_strategy(),
319                        metrics,
320                    });
321            }
322            DatabaseCheckpointControlStatus::Recovering(state) => match &mut state.stage {
323                DatabaseRecoveringStage::Resetting { .. } => {
324                    unreachable!("should not enter resetting again")
325                }
326                DatabaseRecoveringStage::Initializing {
327                    initial_barrier_collector,
328                } => {
329                    let partial_graphs: HashSet<_> =
330                        initial_barrier_collector.all_partial_graphs().collect();
331                    event_log_manager_ref.add_event_logs(vec![Event::Recovery(
332                        EventRecovery::database_recovery_failure(self.database_id.as_raw_id()),
333                    )]);
334                    let backoff_future = state.next_retry();
335                    partial_graph_manager.reset_partial_graphs(partial_graphs.iter().copied());
336                    state.metrics.recovery_failure_cnt.inc();
337                    state.stage = DatabaseRecoveringStage::Resetting {
338                        resetting_partial_graphs: partial_graphs,
339                        reset_resps: vec![],
340                        backoff_future: Some(backoff_future),
341                    };
342                }
343            },
344        }
345        Ok(())
346    }
347}
348
349impl CheckpointControl {
350    pub(crate) fn on_report_failure(
351        &mut self,
352        database_id: DatabaseId,
353        partial_graph_manager: &mut PartialGraphManager,
354    ) -> Option<DatabaseStatusAction<'_, EnterReset>> {
355        let database_status = self.databases.get_mut(&database_id).expect("should exist");
356        match database_status {
357            DatabaseCheckpointControlStatus::Running(_) => {
358                Some(self.new_database_status_action(database_id, EnterReset))
359            }
360            DatabaseCheckpointControlStatus::Recovering(state) => match &mut state.stage {
361                DatabaseRecoveringStage::Resetting { .. } => {
362                    // ignore reported failure during resetting or backoff.
363                    None
364                }
365                DatabaseRecoveringStage::Initializing {
366                    initial_barrier_collector,
367                } => {
368                    warn!(database_id = %database_id, "failed to initialize database");
369                    let partial_graphs: HashSet<_> =
370                        initial_barrier_collector.all_partial_graphs().collect();
371                    let backoff_future = state.next_retry();
372                    partial_graph_manager.reset_partial_graphs(partial_graphs.iter().copied());
373                    state.metrics.recovery_failure_cnt.inc();
374                    state.stage = DatabaseRecoveringStage::Resetting {
375                        resetting_partial_graphs: partial_graphs,
376                        reset_resps: vec![],
377                        backoff_future: Some(backoff_future),
378                    };
379                    None
380                }
381            },
382        }
383    }
384}
385
386pub(crate) struct EnterInitializing(pub(crate) Vec<(WorkerId, ResetPartialGraphResponse)>);
387
388impl DatabaseStatusAction<'_, EnterInitializing> {
389    pub(crate) fn enter(
390        self,
391        runtime_info: DatabaseRuntimeInfoSnapshot,
392        rendered_info: RenderedDatabaseRuntimeInfo,
393        partial_graph_manager: &mut PartialGraphManager,
394    ) {
395        let database_status = self
396            .control
397            .databases
398            .get_mut(&self.database_id)
399            .expect("should exist");
400        let status = match database_status {
401            DatabaseCheckpointControlStatus::Running(_) => {
402                unreachable!("should not enter initializing when running")
403            }
404            DatabaseCheckpointControlStatus::Recovering(state) => match state.stage {
405                DatabaseRecoveringStage::Initializing { .. } => {
406                    unreachable!("can only enter initializing when resetting")
407                }
408                DatabaseRecoveringStage::Resetting { .. } => state,
409            },
410        };
411        let DatabaseRuntimeInfoSnapshot {
412            recovery_context,
413            mut state_table_committed_epochs,
414            mut state_table_log_epochs,
415            mut mv_depended_subscriptions,
416            mut creating_jobs,
417            mut cdc_table_snapshot_splits,
418        } = runtime_info;
419        let fragment_relations = &recovery_context.fragment_relations;
420        let RenderedDatabaseRuntimeInfo {
421            job_infos,
422            stream_actors,
423            mut source_splits,
424            batch_refresh,
425            iceberg_v3,
426        } = rendered_info;
427        let mut recoverer = partial_graph_manager.start_recover();
428        let result: MetaResult<_> = try {
429            recoverer.inject_database_initial_barrier(
430                self.database_id,
431                job_infos,
432                &recovery_context.job_extra_info,
433                &mut state_table_committed_epochs,
434                &mut state_table_log_epochs,
435                fragment_relations,
436                &stream_actors,
437                &mut source_splits,
438                &mut creating_jobs,
439                &mut mv_depended_subscriptions,
440                false,
441                &self.control.hummock_version_stats,
442                &mut cdc_table_snapshot_splits,
443                batch_refresh,
444                iceberg_v3,
445            )?
446        };
447        match result {
448            Ok(database) => {
449                let initializing_partial_graphs = recoverer.all_initializing();
450                info!(?initializing_partial_graphs, database_id = ?self.database_id, "database enter initializing");
451                status.stage = DatabaseRecoveringStage::Initializing {
452                    initial_barrier_collector: DatabaseInitialBarrierCollector {
453                        database_id: self.database_id,
454                        initializing_partial_graphs,
455                        database,
456                    }
457                    .into(),
458                };
459            }
460            Err(e) => {
461                warn!(
462                    database_id = %self.database_id,
463                    e = %e.as_report(),
464                    "failed to inject initial barrier"
465                );
466                let backoff_future = status.next_retry();
467                let resetting_partial_graphs = recoverer.failed();
468                status.metrics.recovery_failure_cnt.inc();
469                status.stage = DatabaseRecoveringStage::Resetting {
470                    resetting_partial_graphs,
471                    reset_resps: vec![],
472                    backoff_future: Some(backoff_future),
473                };
474            }
475        }
476    }
477
478    pub(crate) fn fail_reload_runtime_info(self, e: MetaError) {
479        let database_status = self
480            .control
481            .databases
482            .get_mut(&self.database_id)
483            .expect("should exist");
484        let status = match database_status {
485            DatabaseCheckpointControlStatus::Running(_) => {
486                unreachable!("should not enter initializing when running")
487            }
488            DatabaseCheckpointControlStatus::Recovering(state) => match state.stage {
489                DatabaseRecoveringStage::Initializing { .. } => {
490                    unreachable!("can only enter initializing when resetting")
491                }
492                DatabaseRecoveringStage::Resetting { .. } => state,
493            },
494        };
495        warn!(
496            database_id = %self.database_id,
497            e = %e.as_report(),
498            "failed to reload runtime info"
499        );
500        let backoff_future = status.next_retry();
501        status.metrics.recovery_failure_cnt.inc();
502        status.stage = DatabaseRecoveringStage::Resetting {
503            resetting_partial_graphs: HashSet::new(),
504            reset_resps: vec![],
505            backoff_future: Some(backoff_future),
506        };
507    }
508
509    pub(crate) fn remove(self) {
510        self.control
511            .databases
512            .remove(&self.database_id)
513            .expect("should exist");
514        self.control
515            .env
516            .shared_actor_infos()
517            .remove_database(self.database_id);
518    }
519}
520
521pub(crate) struct EnterRunning;
522
523impl DatabaseStatusAction<'_, EnterRunning> {
524    pub(crate) fn enter(self) {
525        info!(database_id = ?self.database_id, "database enter running");
526        let event_log_manager_ref = self.control.env.event_log_manager_ref();
527        event_log_manager_ref.add_event_logs(vec![Event::Recovery(
528            EventRecovery::database_recovery_success(self.database_id.as_raw_id()),
529        )]);
530        let database_status = self
531            .control
532            .databases
533            .get_mut(&self.database_id)
534            .expect("should exist");
535        match database_status {
536            DatabaseCheckpointControlStatus::Running(_) => {
537                unreachable!("should not enter running again")
538            }
539            DatabaseCheckpointControlStatus::Recovering(state) => {
540                let temp_place_holder = DatabaseRecoveringStage::Resetting {
541                    resetting_partial_graphs: Default::default(),
542                    reset_resps: vec![],
543                    backoff_future: None,
544                };
545                match state.metrics.recovery_timer.take() {
546                    Some(recovery_timer) => {
547                        recovery_timer.observe_duration();
548                    }
549                    _ => {
550                        if cfg!(debug_assertions) {
551                            panic!(
552                                "take database {} recovery latency for twice",
553                                self.database_id
554                            )
555                        } else {
556                            warn!(database_id = %self.database_id,"failed to take recovery latency")
557                        }
558                    }
559                }
560                match replace(&mut state.stage, temp_place_holder) {
561                    DatabaseRecoveringStage::Resetting { .. } => {
562                        unreachable!("can only enter running during initializing")
563                    }
564                    DatabaseRecoveringStage::Initializing {
565                        initial_barrier_collector,
566                    } => {
567                        *database_status = DatabaseCheckpointControlStatus::Running(
568                            initial_barrier_collector.finish(),
569                        );
570                    }
571                }
572            }
573        }
574    }
575}