1use 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
43enum 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 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}