1use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17use std::time::{Duration, Instant};
18
19use anyhow::anyhow;
20use chrono::{DateTime, Duration as ChronoDuration, NaiveDateTime, Utc};
21use parking_lot::Mutex;
22use risingwave_common::catalog::{DatabaseId, FragmentTypeFlag, TableId};
23use risingwave_common::metrics::{LabelGuardedIntCounter, LabelGuardedUintGauge};
24use risingwave_common::util::epoch::Epoch;
25use risingwave_meta_model::ActorId;
26use risingwave_meta_model::refresh_job::{self, RefreshState};
27use risingwave_pb::catalog::table::OptionalAssociatedSourceId;
28use risingwave_pb::id::SourceId;
29use risingwave_pb::meta::{RefreshRequest, RefreshResponse};
30use thiserror_ext::AsReport;
31use tokio::sync::{Notify, oneshot};
32use tokio::task::JoinHandle;
33
34use super::ScaleControllerRef;
35use crate::barrier::{BarrierScheduler, Command};
36use crate::controller::fragment::InflightFragmentInfo;
37use crate::manager::MetadataManager;
38use crate::rpc::metrics::GLOBAL_META_METRICS;
39use crate::{MetaError, MetaResult};
40
41pub type GlobalRefreshManagerRef = Arc<GlobalRefreshManager>;
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44enum TriggerReason {
45 Manual,
46 Cron,
47 RetryAfterAbort,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum RefreshStage {
52 List,
53 Fetch,
54 Mview,
55}
56
57#[derive(Debug, Default)]
59pub struct RefreshCycleActors {
60 pub list: HashSet<ActorId>,
61 pub fetch: HashSet<ActorId>,
62 pub mview: HashSet<ActorId>,
63}
64
65impl RefreshCycleActors {
66 pub fn from_fragments<'a>(fragments: impl Iterator<Item = &'a InflightFragmentInfo>) -> Self {
67 let mut actors = Self::default();
68 for fragment in fragments {
69 let mask = fragment.fragment_type_mask;
70 let stage = if mask.contains(FragmentTypeFlag::Mview) {
71 &mut actors.mview
72 } else if mask.contains(FragmentTypeFlag::Source)
73 && !mask.contains(FragmentTypeFlag::Dml)
74 {
75 &mut actors.list
76 } else if mask.contains(FragmentTypeFlag::FsFetch) {
77 &mut actors.fetch
78 } else {
79 continue;
80 };
81 stage.extend(fragment.actors.keys().map(|actor_id| *actor_id as ActorId));
82 }
83 actors
84 }
85}
86
87pub struct GlobalRefreshManager {
95 metadata_manager: MetadataManager,
96 barrier_scheduler: BarrierScheduler,
97 scale_controller: ScaleControllerRef,
98 cycles: Mutex<Cycles>,
99 scheduler_wakeup: Notify,
100}
101
102#[derive(Default)]
103struct Cycles {
104 trackers: HashMap<TableId, CycleTracker>,
105 starting: HashSet<TableId>,
107 retry: HashMap<TableId, RetryState>,
108 metrics: HashMap<TableId, RefreshJobMetrics>,
109}
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113enum RetryState {
114 Owed,
115 Used,
116}
117
118impl GlobalRefreshManager {
119 pub async fn start(
120 metadata_manager: MetadataManager,
121 barrier_scheduler: BarrierScheduler,
122 scale_controller: ScaleControllerRef,
123 scheduler_interval: Duration,
124 ) -> MetaResult<(GlobalRefreshManagerRef, JoinHandle<()>, oneshot::Sender<()>)> {
125 for table_id in metadata_manager.list_refreshable_table_ids().await? {
126 metadata_manager.ensure_refresh_job(table_id).await?;
127 }
128 let manager = Arc::new(Self {
129 metadata_manager,
130 barrier_scheduler,
131 scale_controller,
132 cycles: Mutex::new(Cycles::default()),
133 scheduler_wakeup: Notify::new(),
134 });
135 let (shutdown_tx, shutdown_rx) = oneshot::channel();
136 let join_handle = tokio::spawn(
137 manager
138 .clone()
139 .run_scheduler(shutdown_rx, scheduler_interval),
140 );
141 Ok((manager, join_handle, shutdown_tx))
142 }
143
144 pub async fn trigger_manual_refresh(
146 self: &Arc<Self>,
147 request: RefreshRequest,
148 ) -> MetaResult<RefreshResponse> {
149 let table_id = request.table_id;
150 let associated_source_id = request.associated_source_id;
151 tracing::info!(%table_id, %associated_source_id, "trigger manual refresh");
152 self.trigger_cycle(table_id, Some(associated_source_id), TriggerReason::Manual)
153 .await?;
154 Ok(RefreshResponse { status: None })
155 }
156
157 async fn trigger_cycle(
160 self: &Arc<Self>,
161 table_id: TableId,
162 associated_source_id: Option<SourceId>,
163 reason: TriggerReason,
164 ) -> MetaResult<()> {
165 {
166 let mut cycles = self.cycles.lock();
167 if cycles.trackers.contains_key(&table_id) || !cycles.starting.insert(table_id) {
168 return Err(MetaError::invalid_parameter(format!(
169 "Table {} is not in idle state: a refresh cycle is in progress",
170 table_id
171 )));
172 }
173 }
174 let this = self.clone();
175 let result = tokio::spawn(async move {
176 let result = this
177 .start_cycle(table_id, associated_source_id, reason)
178 .await;
179 this.cycles.lock().starting.remove(&table_id);
180 result
181 })
182 .await;
183 result.map_err(|err| anyhow!(err))?
184 }
185
186 async fn start_cycle(
187 &self,
188 table_id: TableId,
189 associated_source_id: Option<SourceId>,
190 reason: TriggerReason,
191 ) -> MetaResult<()> {
192 let _reschedule_guard = self.scale_controller.reschedule_lock.read().await;
196 let catalog = &self.metadata_manager.catalog_controller;
197 let table = catalog.get_table_by_id(table_id).await?;
198 if !table.refreshable {
199 return Err(MetaError::invalid_parameter(format!(
200 "Table '{}' is not refreshable. Only tables created with REFRESHABLE flag support refresh.",
201 table.name
202 )));
203 }
204 let Some(OptionalAssociatedSourceId::AssociatedSourceId(table_source_id)) =
205 table.optional_associated_source_id
206 else {
207 return Err(MetaError::invalid_parameter(format!(
208 "Table '{}' has no associated source",
209 table.name
210 )));
211 };
212 let associated_source_id = associated_source_id.unwrap_or(table_source_id);
213 if associated_source_id != table_source_id {
214 return Err(MetaError::invalid_parameter(format!(
215 "Table '{}' is not associated with source '{}'. table.optional_associated_source_id: {:?}",
216 table.name, associated_source_id, table.optional_associated_source_id
217 )));
218 }
219 if let Some(state) = catalog.get_refresh_job_state(table_id).await?
220 && state != RefreshState::Idle
221 {
222 return Err(MetaError::invalid_parameter(format!(
223 "Table '{}' is not in idle state. Current state: {}",
224 table.name, state
225 )));
226 }
227 let database_id = catalog.get_object_database_id(table_id).await?;
228 let staging_table_id = catalog
229 .get_refresh_staging_table_id(table_id.as_job_id())
230 .await?
231 .ok_or_else(|| anyhow!("staging table not found for refreshable table {}", table_id))?;
232
233 let trigger_time = now_millis();
234 tracing::info!(%table_id, %trigger_time, ?reason, "scheduling refresh cycle");
235 self.barrier_scheduler
236 .schedule_command(
237 database_id,
238 Command::Refresh {
239 table_id,
240 associated_source_id,
241 staging_table_id,
242 trigger_time,
243 },
244 )?
245 .await?;
246
247 let mut cycles = self.cycles.lock();
248 if !cycles
249 .trackers
250 .get(&table_id)
251 .is_some_and(|tracker| tracker.trigger_time == trigger_time)
252 {
253 return Err(anyhow!(
254 "refresh of table {} is not running: the table was dropped or a recovery abandoned the cycle",
255 table_id
256 )
257 .into());
258 }
259 match reason {
260 TriggerReason::Manual => {}
261 TriggerReason::Cron => cycles.job_metrics(table_id).cron_trigger_count.inc(),
262 TriggerReason::RetryAfterAbort => {
263 cycles.retry.insert(table_id, RetryState::Used);
264 }
265 }
266 Ok(())
267 }
268
269 pub async fn cycle_started(
271 &self,
272 table_id: TableId,
273 database_id: DatabaseId,
274 associated_source_id: SourceId,
275 staging_table_id: TableId,
276 trigger_time: NaiveDateTime,
277 actors: RefreshCycleActors,
278 ) -> MetaResult<()> {
279 self.cycles.lock().trackers.insert(
281 table_id,
282 CycleTracker::new(
283 database_id,
284 associated_source_id,
285 staging_table_id,
286 trigger_time,
287 actors,
288 ),
289 );
290 if !self
291 .metadata_manager
292 .begin_refresh_job(table_id, trigger_time)
293 .await?
294 {
295 let mut cycles = self.cycles.lock();
296 if cycles
297 .trackers
298 .get(&table_id)
299 .is_some_and(|tracker| tracker.trigger_time == trigger_time)
300 {
301 cycles.trackers.remove(&table_id);
302 }
303 tracing::warn!(%table_id, %trigger_time, "RefreshStart collected for a table that is not idle, the cycle is not tracked");
304 return Ok(());
305 }
306 tracing::info!(%table_id, %trigger_time, "refresh cycle started");
307 Ok(())
308 }
309
310 pub fn report_stage(
313 &self,
314 table_id: TableId,
315 stage: RefreshStage,
316 actors: HashSet<ActorId>,
317 ) -> MetaResult<()> {
318 let mut cycles = self.cycles.lock();
319 let Some(tracker) = cycles.trackers.get_mut(&table_id) else {
320 tracing::warn!(%table_id, ?stage, ?actors, "ignore refresh stage report without tracker");
322 return Ok(());
323 };
324 if !tracker.stage_mut(stage).report(stage, &actors)? {
325 return Ok(());
326 }
327 let (database_id, associated_source_id, staging_table_id, trigger_time) = (
328 tracker.database_id,
329 tracker.associated_source_id,
330 tracker.staging_table_id,
331 tracker.trigger_time,
332 );
333 let command = match stage {
334 RefreshStage::List => Command::ListFinish {
335 table_id,
336 associated_source_id,
337 },
338 RefreshStage::Fetch => Command::LoadFinish {
339 table_id,
340 associated_source_id,
341 },
342 RefreshStage::Mview => {
343 let command = Command::FinishRefresh {
344 table_id,
345 staging_table_id,
346 trigger_time,
347 };
348 match self
351 .barrier_scheduler
352 .run_command_no_wait(database_id, command)
353 {
354 Ok(()) => {
355 tracing::info!(%table_id, %trigger_time, "FinishRefresh command scheduled")
356 }
357 Err(err) => {
358 tracing::warn!(%table_id, error = %err.as_report(), "failed to schedule FinishRefresh, the recovery in progress abandons the cycle")
359 }
360 }
361 return Ok(());
362 }
363 };
364 self.barrier_scheduler
365 .run_command_no_wait(database_id, command)
366 .map_err(|err| {
367 anyhow!(err).context(format!(
368 "failed to schedule the next refresh stage of table {table_id}"
369 ))
370 })?;
371 tracing::info!(%table_id, ?stage, "refresh stage finished");
372 Ok(())
373 }
374
375 pub async fn complete_refresh(
377 &self,
378 table_id: TableId,
379 trigger_time: NaiveDateTime,
380 ) -> MetaResult<()> {
381 let finished = self
382 .metadata_manager
383 .finish_refresh_job(table_id, trigger_time, true)
384 .await?;
385 let mut cycles = self.cycles.lock();
386 let tracker = cycles
387 .trackers
388 .get(&table_id)
389 .is_some_and(|tracker| tracker.trigger_time == trigger_time)
390 .then(|| cycles.trackers.remove(&table_id))
391 .flatten();
392 if !finished {
393 tracing::warn!(%table_id, %trigger_time, "FinishRefresh collected for a cycle that is no longer current");
394 return Ok(());
395 }
396 tracing::info!(%table_id, %trigger_time, aborted = false, "refresh cycle finished");
397 cycles.record_finished(
398 table_id,
399 "success",
400 cycle_duration_secs(tracker.as_ref(), trigger_time),
401 );
402 cycles.retry.remove(&table_id);
403 Ok(())
404 }
405
406 pub async fn abandon_cycles(&self, database_id: Option<DatabaseId>) -> MetaResult<()> {
409 let jobs = self
410 .metadata_manager
411 .list_refreshing_jobs(database_id)
412 .await?;
413 let mut abandoned = Vec::with_capacity(jobs.len());
414 for job in jobs {
415 let Some(trigger_time) = job.last_trigger_time.map(millis_to_datetime) else {
416 tracing::error!(table_id = %job.table_id, status = ?job.current_status, "refresh job is not idle but has no trigger time");
417 continue;
418 };
419 if self
420 .metadata_manager
421 .finish_refresh_job(job.table_id, trigger_time, false)
422 .await?
423 {
424 abandoned.push((job.table_id, trigger_time));
425 }
426 }
427 let mut cycles = self.cycles.lock();
428 for (table_id, trigger_time) in abandoned {
429 let tracker = cycles.trackers.remove(&table_id);
430 tracing::info!(%table_id, %trigger_time, aborted = true, "refresh cycle finished");
431 cycles.record_finished(
432 table_id,
433 "aborted",
434 cycle_duration_secs(tracker.as_ref(), trigger_time),
435 );
436 if cycles.retry.get(&table_id) == Some(&RetryState::Used) {
437 tracing::warn!(%table_id, "abandoned refresh was already re-run once, waiting for the next trigger");
438 } else {
439 cycles.retry.insert(table_id, RetryState::Owed);
440 }
441 }
442 match database_id {
443 None => cycles.trackers.clear(),
444 Some(database_id) => cycles
445 .trackers
446 .retain(|_, tracker| tracker.database_id != database_id),
447 }
448 Ok(())
449 }
450
451 pub fn remove_progress_tracker(&self, table_id: TableId, status: &str) {
453 let mut cycles = self.cycles.lock();
454 if let Some(tracker) = cycles.trackers.remove(&table_id) {
455 cycles.record_finished(table_id, status, tracker.start_time.elapsed().as_secs());
456 }
457 cycles.retry.remove(&table_id);
458 }
459
460 pub fn notify_scheduler(&self) {
461 self.scheduler_wakeup.notify_one();
462 }
463
464 async fn run_scheduler(
465 self: Arc<Self>,
466 mut shutdown_rx: oneshot::Receiver<()>,
467 scheduler_interval: Duration,
468 ) {
469 let mut ticker = tokio::time::interval(scheduler_interval);
470 loop {
471 tokio::select! {
472 biased;
473 _ = &mut shutdown_rx => {
474 tracing::info!("refresh scheduler shutting down");
475 break;
476 }
477 _ = self.scheduler_wakeup.notified() => {}
478 _ = ticker.tick() => {}
479 }
480 self.tick().await;
481 }
482 }
483
484 async fn tick(self: &Arc<Self>) {
485 let jobs = match self.metadata_manager.list_refresh_jobs().await {
486 Ok(jobs) => jobs,
487 Err(err) => {
488 tracing::warn!(error = %err.as_report(), "failed to list refresh jobs");
489 return;
490 }
491 };
492 let never_triggered: Vec<TableId> = jobs
493 .iter()
494 .filter(|job| job.last_trigger_time.is_none() && cron_interval(job).is_some())
495 .map(|job| job.table_id)
496 .collect();
497 let created_at = match self.created_at_millis(&never_triggered).await {
498 Ok(created_at) => created_at,
499 Err(err) => {
500 tracing::warn!(error = %err.as_report(), "failed to load the creation time of refreshable tables");
501 HashMap::new()
502 }
503 };
504 {
505 let active: HashSet<_> = jobs.iter().map(|job| job.table_id).collect();
506 let mut cycles = self.cycles.lock();
507 cycles
508 .metrics
509 .retain(|table_id, _| active.contains(table_id));
510 cycles.retry.retain(|table_id, _| active.contains(table_id));
511 }
512 for job in &jobs {
513 let table_id = job.table_id;
514 let cron_due = cron_due(job, created_at.get(&table_id).copied());
515 if job.current_status != RefreshState::Idle {
516 if cron_due {
517 let mut cycles = self.cycles.lock();
518 cycles.job_metrics(table_id).cron_miss_count.inc();
519 tracing::warn!(%table_id, status = ?job.current_status, "skip scheduled refresh: the previous cycle is still running");
520 }
521 continue;
522 }
523 let reason = {
524 let cycles = self.cycles.lock();
525 if cycles.starting.contains(&table_id) {
526 continue;
527 }
528 if cycles.retry.get(&table_id) == Some(&RetryState::Owed) {
529 TriggerReason::RetryAfterAbort
530 } else if cron_due {
531 TriggerReason::Cron
532 } else {
533 continue;
534 }
535 };
536 tracing::info!(%table_id, ?reason, "trigger refresh");
537 let this = self.clone();
538 tokio::spawn(async move {
539 if let Err(err) = this.trigger_cycle(table_id, None, reason).await {
540 tracing::warn!(%table_id, ?reason, error = %err.as_report(), "failed to trigger refresh");
541 }
542 });
543 }
544 }
545
546 async fn created_at_millis(&self, table_ids: &[TableId]) -> MetaResult<HashMap<TableId, i64>> {
547 if table_ids.is_empty() {
548 return Ok(HashMap::new());
549 }
550 Ok(self
551 .metadata_manager
552 .get_table_catalog_by_ids(table_ids)
553 .await?
554 .into_iter()
555 .map(|table| {
556 (
557 table.id,
558 Epoch(table.created_at_epoch())
559 .as_timestamptz()
560 .to_datetime_utc()
561 .timestamp_millis(),
562 )
563 })
564 .collect())
565 }
566}
567
568impl Cycles {
569 fn record_finished(&mut self, table_id: TableId, status: &str, duration_secs: u64) {
570 let finished = self
571 .job_metrics(table_id)
572 .finished
573 .entry(status.to_owned())
574 .or_insert_with(|| RefreshFinishedMetrics::new(table_id, status));
575 finished.count.inc();
576 finished.duration.set(duration_secs);
577 }
578
579 fn job_metrics(&mut self, table_id: TableId) -> &mut RefreshJobMetrics {
580 self.metrics
581 .entry(table_id)
582 .or_insert_with(|| RefreshJobMetrics::new(table_id))
583 }
584}
585
586fn cron_interval(job: &refresh_job::Model) -> Option<ChronoDuration> {
587 job.trigger_interval_secs
588 .filter(|secs| *secs > 0)
589 .map(ChronoDuration::seconds)
590}
591
592fn cron_due(job: &refresh_job::Model, created_at_millis: Option<i64>) -> bool {
594 let Some(interval) = cron_interval(job) else {
595 return false;
596 };
597 let Some(last_run) = job.last_trigger_time.or(created_at_millis) else {
598 return false;
599 };
600 Utc::now()
601 .naive_utc()
602 .signed_duration_since(millis_to_datetime(last_run))
603 >= interval
604}
605
606fn now_millis() -> NaiveDateTime {
608 millis_to_datetime(Utc::now().timestamp_millis())
609}
610
611fn cycle_duration_secs(tracker: Option<&CycleTracker>, trigger_time: NaiveDateTime) -> u64 {
614 tracker.map_or_else(
615 || u64::try_from((Utc::now().naive_utc() - trigger_time).num_seconds()).unwrap_or(0),
616 |tracker| tracker.start_time.elapsed().as_secs(),
617 )
618}
619
620fn millis_to_datetime(millis: i64) -> NaiveDateTime {
621 DateTime::from_timestamp_millis(millis)
622 .expect("valid timestamp")
623 .naive_utc()
624}
625
626struct RefreshJobMetrics {
627 cron_trigger_count: LabelGuardedIntCounter,
628 cron_miss_count: LabelGuardedIntCounter,
629 finished: HashMap<String, RefreshFinishedMetrics>,
630}
631
632impl RefreshJobMetrics {
633 fn new(table_id: TableId) -> Self {
634 let table_id = table_id.to_string();
635 Self {
636 cron_trigger_count: GLOBAL_META_METRICS
637 .refresh_cron_job_trigger_cnt
638 .with_guarded_label_values(&[&table_id]),
639 cron_miss_count: GLOBAL_META_METRICS
640 .refresh_cron_job_miss_cnt
641 .with_guarded_label_values(&[&table_id]),
642 finished: HashMap::new(),
643 }
644 }
645}
646
647struct RefreshFinishedMetrics {
648 count: LabelGuardedIntCounter,
649 duration: LabelGuardedUintGauge,
650}
651
652impl RefreshFinishedMetrics {
653 fn new(table_id: TableId, status: &str) -> Self {
654 let table_id = table_id.to_string();
655 Self {
656 count: GLOBAL_META_METRICS
657 .refresh_job_finish_cnt
658 .with_guarded_label_values(&[&table_id, status]),
659 duration: GLOBAL_META_METRICS
660 .refresh_job_duration
661 .with_guarded_label_values(&[&table_id, status]),
662 }
663 }
664}
665
666#[derive(Debug)]
667struct CycleTracker {
668 database_id: DatabaseId,
669 associated_source_id: SourceId,
670 staging_table_id: TableId,
671 trigger_time: NaiveDateTime,
672 list: StageProgress,
673 fetch: StageProgress,
674 mview: StageProgress,
675 start_time: Instant,
676}
677
678impl CycleTracker {
679 fn new(
680 database_id: DatabaseId,
681 associated_source_id: SourceId,
682 staging_table_id: TableId,
683 trigger_time: NaiveDateTime,
684 actors: RefreshCycleActors,
685 ) -> Self {
686 Self {
687 database_id,
688 associated_source_id,
689 staging_table_id,
690 trigger_time,
691 list: StageProgress::new(actors.list),
692 fetch: StageProgress::new(actors.fetch),
693 mview: StageProgress::new(actors.mview),
694 start_time: Instant::now(),
695 }
696 }
697
698 fn stage_mut(&mut self, stage: RefreshStage) -> &mut StageProgress {
699 match stage {
700 RefreshStage::List => &mut self.list,
701 RefreshStage::Fetch => &mut self.fetch,
702 RefreshStage::Mview => &mut self.mview,
703 }
704 }
705}
706
707#[derive(Debug)]
708struct StageProgress {
709 expected: HashSet<ActorId>,
710 finished: HashSet<ActorId>,
711}
712
713impl StageProgress {
714 fn new(expected: HashSet<ActorId>) -> Self {
715 Self {
716 expected,
717 finished: HashSet::new(),
718 }
719 }
720
721 fn report(&mut self, stage: RefreshStage, actors: &HashSet<ActorId>) -> MetaResult<bool> {
723 self.finished.extend(actors);
724 if self.finished.len() < self.expected.len() {
725 return Ok(false);
726 }
727 if self.finished == self.expected {
728 Ok(true)
729 } else {
730 Err(anyhow!(
731 "{stage:?} finished actors mismatch: expected: {:?}, actual: {:?}",
732 self.expected,
733 self.finished
734 )
735 .into())
736 }
737 }
738}