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 catalog = &self.metadata_manager.catalog_controller;
193 let table = catalog.get_table_by_id(table_id).await?;
194 if !table.refreshable {
195 return Err(MetaError::invalid_parameter(format!(
196 "Table '{}' is not refreshable. Only tables created with REFRESHABLE flag support refresh.",
197 table.name
198 )));
199 }
200 let Some(OptionalAssociatedSourceId::AssociatedSourceId(table_source_id)) =
201 table.optional_associated_source_id
202 else {
203 return Err(MetaError::invalid_parameter(format!(
204 "Table '{}' has no associated source",
205 table.name
206 )));
207 };
208 let associated_source_id = associated_source_id.unwrap_or(table_source_id);
209 if associated_source_id != table_source_id {
210 return Err(MetaError::invalid_parameter(format!(
211 "Table '{}' is not associated with source '{}'. table.optional_associated_source_id: {:?}",
212 table.name, associated_source_id, table.optional_associated_source_id
213 )));
214 }
215 if let Some(state) = catalog.get_refresh_job_state(table_id).await?
216 && state != RefreshState::Idle
217 {
218 return Err(MetaError::invalid_parameter(format!(
219 "Table '{}' is not in idle state. Current state: {}",
220 table.name, state
221 )));
222 }
223 let database_id = catalog.get_object_database_id(table_id).await?;
224 let staging_table_id = catalog
225 .get_refresh_staging_table_id(table_id.as_job_id())
226 .await?
227 .ok_or_else(|| anyhow!("staging table not found for refreshable table {}", table_id))?;
228
229 let _reschedule_guard = self.scale_controller.reschedule_lock.read().await;
232 let trigger_time = now_millis();
233 tracing::info!(%table_id, %trigger_time, ?reason, "scheduling refresh cycle");
234 self.barrier_scheduler
235 .schedule_command(
236 database_id,
237 Command::Refresh {
238 table_id,
239 associated_source_id,
240 staging_table_id,
241 trigger_time,
242 },
243 )?
244 .await?;
245
246 let mut cycles = self.cycles.lock();
247 if !cycles
248 .trackers
249 .get(&table_id)
250 .is_some_and(|tracker| tracker.trigger_time == trigger_time)
251 {
252 return Err(anyhow!(
253 "refresh of table {} is not running: the table was dropped or a recovery abandoned the cycle",
254 table_id
255 )
256 .into());
257 }
258 match reason {
259 TriggerReason::Manual => {}
260 TriggerReason::Cron => cycles.job_metrics(table_id).cron_trigger_count.inc(),
261 TriggerReason::RetryAfterAbort => {
262 cycles.retry.insert(table_id, RetryState::Used);
263 }
264 }
265 Ok(())
266 }
267
268 pub async fn cycle_started(
270 &self,
271 table_id: TableId,
272 database_id: DatabaseId,
273 associated_source_id: SourceId,
274 staging_table_id: TableId,
275 trigger_time: NaiveDateTime,
276 actors: RefreshCycleActors,
277 ) -> MetaResult<()> {
278 self.cycles.lock().trackers.insert(
280 table_id,
281 CycleTracker::new(
282 database_id,
283 associated_source_id,
284 staging_table_id,
285 trigger_time,
286 actors,
287 ),
288 );
289 if !self
290 .metadata_manager
291 .begin_refresh_job(table_id, trigger_time)
292 .await?
293 {
294 let mut cycles = self.cycles.lock();
295 if cycles
296 .trackers
297 .get(&table_id)
298 .is_some_and(|tracker| tracker.trigger_time == trigger_time)
299 {
300 cycles.trackers.remove(&table_id);
301 }
302 tracing::warn!(%table_id, %trigger_time, "RefreshStart collected for a table that is not idle, the cycle is not tracked");
303 return Ok(());
304 }
305 tracing::info!(%table_id, %trigger_time, "refresh cycle started");
306 Ok(())
307 }
308
309 pub fn report_stage(
312 &self,
313 table_id: TableId,
314 stage: RefreshStage,
315 actors: HashSet<ActorId>,
316 ) -> MetaResult<()> {
317 let mut cycles = self.cycles.lock();
318 let Some(tracker) = cycles.trackers.get_mut(&table_id) else {
319 tracing::warn!(%table_id, ?stage, ?actors, "ignore refresh stage report without tracker");
321 return Ok(());
322 };
323 if !tracker.stage_mut(stage).report(stage, &actors)? {
324 return Ok(());
325 }
326 let (database_id, associated_source_id, staging_table_id, trigger_time) = (
327 tracker.database_id,
328 tracker.associated_source_id,
329 tracker.staging_table_id,
330 tracker.trigger_time,
331 );
332 let command = match stage {
333 RefreshStage::List => Command::ListFinish {
334 table_id,
335 associated_source_id,
336 },
337 RefreshStage::Fetch => Command::LoadFinish {
338 table_id,
339 associated_source_id,
340 },
341 RefreshStage::Mview => {
342 let command = Command::FinishRefresh {
343 table_id,
344 staging_table_id,
345 trigger_time,
346 };
347 match self
350 .barrier_scheduler
351 .run_command_no_wait(database_id, command)
352 {
353 Ok(()) => {
354 tracing::info!(%table_id, %trigger_time, "FinishRefresh command scheduled")
355 }
356 Err(err) => {
357 tracing::warn!(%table_id, error = %err.as_report(), "failed to schedule FinishRefresh, the recovery in progress abandons the cycle")
358 }
359 }
360 return Ok(());
361 }
362 };
363 self.barrier_scheduler
364 .run_command_no_wait(database_id, command)
365 .map_err(|err| {
366 anyhow!(err).context(format!(
367 "failed to schedule the next refresh stage of table {table_id}"
368 ))
369 })?;
370 tracing::info!(%table_id, ?stage, "refresh stage finished");
371 Ok(())
372 }
373
374 pub async fn complete_refresh(
376 &self,
377 table_id: TableId,
378 trigger_time: NaiveDateTime,
379 ) -> MetaResult<()> {
380 let finished = self
381 .metadata_manager
382 .finish_refresh_job(table_id, trigger_time, true)
383 .await?;
384 let mut cycles = self.cycles.lock();
385 let tracker = cycles
386 .trackers
387 .get(&table_id)
388 .is_some_and(|tracker| tracker.trigger_time == trigger_time)
389 .then(|| cycles.trackers.remove(&table_id))
390 .flatten();
391 if !finished {
392 tracing::warn!(%table_id, %trigger_time, "FinishRefresh collected for a cycle that is no longer current");
393 return Ok(());
394 }
395 tracing::info!(%table_id, %trigger_time, aborted = false, "refresh cycle finished");
396 cycles.record_finished(
397 table_id,
398 "success",
399 cycle_duration_secs(tracker.as_ref(), trigger_time),
400 );
401 cycles.retry.remove(&table_id);
402 Ok(())
403 }
404
405 pub async fn abandon_cycles(&self, database_id: Option<DatabaseId>) -> MetaResult<()> {
408 let jobs = self
409 .metadata_manager
410 .list_refreshing_jobs(database_id)
411 .await?;
412 let mut abandoned = Vec::with_capacity(jobs.len());
413 for job in jobs {
414 let Some(trigger_time) = job.last_trigger_time.map(millis_to_datetime) else {
415 tracing::error!(table_id = %job.table_id, status = ?job.current_status, "refresh job is not idle but has no trigger time");
416 continue;
417 };
418 if self
419 .metadata_manager
420 .finish_refresh_job(job.table_id, trigger_time, false)
421 .await?
422 {
423 abandoned.push((job.table_id, trigger_time));
424 }
425 }
426 let mut cycles = self.cycles.lock();
427 for (table_id, trigger_time) in abandoned {
428 let tracker = cycles.trackers.remove(&table_id);
429 tracing::info!(%table_id, %trigger_time, aborted = true, "refresh cycle finished");
430 cycles.record_finished(
431 table_id,
432 "aborted",
433 cycle_duration_secs(tracker.as_ref(), trigger_time),
434 );
435 if cycles.retry.get(&table_id) == Some(&RetryState::Used) {
436 tracing::warn!(%table_id, "abandoned refresh was already re-run once, waiting for the next trigger");
437 } else {
438 cycles.retry.insert(table_id, RetryState::Owed);
439 }
440 }
441 match database_id {
442 None => cycles.trackers.clear(),
443 Some(database_id) => cycles
444 .trackers
445 .retain(|_, tracker| tracker.database_id != database_id),
446 }
447 Ok(())
448 }
449
450 pub fn remove_progress_tracker(&self, table_id: TableId, status: &str) {
452 let mut cycles = self.cycles.lock();
453 if let Some(tracker) = cycles.trackers.remove(&table_id) {
454 cycles.record_finished(table_id, status, tracker.start_time.elapsed().as_secs());
455 }
456 cycles.retry.remove(&table_id);
457 }
458
459 pub fn notify_scheduler(&self) {
460 self.scheduler_wakeup.notify_one();
461 }
462
463 async fn run_scheduler(
464 self: Arc<Self>,
465 mut shutdown_rx: oneshot::Receiver<()>,
466 scheduler_interval: Duration,
467 ) {
468 let mut ticker = tokio::time::interval(scheduler_interval);
469 loop {
470 tokio::select! {
471 biased;
472 _ = &mut shutdown_rx => {
473 tracing::info!("refresh scheduler shutting down");
474 break;
475 }
476 _ = self.scheduler_wakeup.notified() => {}
477 _ = ticker.tick() => {}
478 }
479 self.tick().await;
480 }
481 }
482
483 async fn tick(self: &Arc<Self>) {
484 let jobs = match self.metadata_manager.list_refresh_jobs().await {
485 Ok(jobs) => jobs,
486 Err(err) => {
487 tracing::warn!(error = %err.as_report(), "failed to list refresh jobs");
488 return;
489 }
490 };
491 let never_triggered: Vec<TableId> = jobs
492 .iter()
493 .filter(|job| job.last_trigger_time.is_none() && cron_interval(job).is_some())
494 .map(|job| job.table_id)
495 .collect();
496 let created_at = match self.created_at_millis(&never_triggered).await {
497 Ok(created_at) => created_at,
498 Err(err) => {
499 tracing::warn!(error = %err.as_report(), "failed to load the creation time of refreshable tables");
500 HashMap::new()
501 }
502 };
503 {
504 let active: HashSet<_> = jobs.iter().map(|job| job.table_id).collect();
505 let mut cycles = self.cycles.lock();
506 cycles
507 .metrics
508 .retain(|table_id, _| active.contains(table_id));
509 cycles.retry.retain(|table_id, _| active.contains(table_id));
510 }
511 for job in &jobs {
512 let table_id = job.table_id;
513 let cron_due = cron_due(job, created_at.get(&table_id).copied());
514 if job.current_status != RefreshState::Idle {
515 if cron_due {
516 let mut cycles = self.cycles.lock();
517 cycles.job_metrics(table_id).cron_miss_count.inc();
518 tracing::warn!(%table_id, status = ?job.current_status, "skip scheduled refresh: the previous cycle is still running");
519 }
520 continue;
521 }
522 let reason = {
523 let cycles = self.cycles.lock();
524 if cycles.starting.contains(&table_id) {
525 continue;
526 }
527 if cycles.retry.get(&table_id) == Some(&RetryState::Owed) {
528 TriggerReason::RetryAfterAbort
529 } else if cron_due {
530 TriggerReason::Cron
531 } else {
532 continue;
533 }
534 };
535 tracing::info!(%table_id, ?reason, "trigger refresh");
536 let this = self.clone();
537 tokio::spawn(async move {
538 if let Err(err) = this.trigger_cycle(table_id, None, reason).await {
539 tracing::warn!(%table_id, ?reason, error = %err.as_report(), "failed to trigger refresh");
540 }
541 });
542 }
543 }
544
545 async fn created_at_millis(&self, table_ids: &[TableId]) -> MetaResult<HashMap<TableId, i64>> {
546 if table_ids.is_empty() {
547 return Ok(HashMap::new());
548 }
549 Ok(self
550 .metadata_manager
551 .get_table_catalog_by_ids(table_ids)
552 .await?
553 .into_iter()
554 .map(|table| {
555 (
556 table.id,
557 Epoch(table.created_at_epoch())
558 .as_timestamptz()
559 .to_datetime_utc()
560 .timestamp_millis(),
561 )
562 })
563 .collect())
564 }
565}
566
567impl Cycles {
568 fn record_finished(&mut self, table_id: TableId, status: &str, duration_secs: u64) {
569 let finished = self
570 .job_metrics(table_id)
571 .finished
572 .entry(status.to_owned())
573 .or_insert_with(|| RefreshFinishedMetrics::new(table_id, status));
574 finished.count.inc();
575 finished.duration.set(duration_secs);
576 }
577
578 fn job_metrics(&mut self, table_id: TableId) -> &mut RefreshJobMetrics {
579 self.metrics
580 .entry(table_id)
581 .or_insert_with(|| RefreshJobMetrics::new(table_id))
582 }
583}
584
585fn cron_interval(job: &refresh_job::Model) -> Option<ChronoDuration> {
586 job.trigger_interval_secs
587 .filter(|secs| *secs > 0)
588 .map(ChronoDuration::seconds)
589}
590
591fn cron_due(job: &refresh_job::Model, created_at_millis: Option<i64>) -> bool {
593 let Some(interval) = cron_interval(job) else {
594 return false;
595 };
596 let Some(last_run) = job.last_trigger_time.or(created_at_millis) else {
597 return false;
598 };
599 Utc::now()
600 .naive_utc()
601 .signed_duration_since(millis_to_datetime(last_run))
602 >= interval
603}
604
605fn now_millis() -> NaiveDateTime {
607 millis_to_datetime(Utc::now().timestamp_millis())
608}
609
610fn cycle_duration_secs(tracker: Option<&CycleTracker>, trigger_time: NaiveDateTime) -> u64 {
613 tracker.map_or_else(
614 || u64::try_from((Utc::now().naive_utc() - trigger_time).num_seconds()).unwrap_or(0),
615 |tracker| tracker.start_time.elapsed().as_secs(),
616 )
617}
618
619fn millis_to_datetime(millis: i64) -> NaiveDateTime {
620 DateTime::from_timestamp_millis(millis)
621 .expect("valid timestamp")
622 .naive_utc()
623}
624
625struct RefreshJobMetrics {
626 cron_trigger_count: LabelGuardedIntCounter,
627 cron_miss_count: LabelGuardedIntCounter,
628 finished: HashMap<String, RefreshFinishedMetrics>,
629}
630
631impl RefreshJobMetrics {
632 fn new(table_id: TableId) -> Self {
633 let table_id = table_id.to_string();
634 Self {
635 cron_trigger_count: GLOBAL_META_METRICS
636 .refresh_cron_job_trigger_cnt
637 .with_guarded_label_values(&[&table_id]),
638 cron_miss_count: GLOBAL_META_METRICS
639 .refresh_cron_job_miss_cnt
640 .with_guarded_label_values(&[&table_id]),
641 finished: HashMap::new(),
642 }
643 }
644}
645
646struct RefreshFinishedMetrics {
647 count: LabelGuardedIntCounter,
648 duration: LabelGuardedUintGauge,
649}
650
651impl RefreshFinishedMetrics {
652 fn new(table_id: TableId, status: &str) -> Self {
653 let table_id = table_id.to_string();
654 Self {
655 count: GLOBAL_META_METRICS
656 .refresh_job_finish_cnt
657 .with_guarded_label_values(&[&table_id, status]),
658 duration: GLOBAL_META_METRICS
659 .refresh_job_duration
660 .with_guarded_label_values(&[&table_id, status]),
661 }
662 }
663}
664
665#[derive(Debug)]
666struct CycleTracker {
667 database_id: DatabaseId,
668 associated_source_id: SourceId,
669 staging_table_id: TableId,
670 trigger_time: NaiveDateTime,
671 list: StageProgress,
672 fetch: StageProgress,
673 mview: StageProgress,
674 start_time: Instant,
675}
676
677impl CycleTracker {
678 fn new(
679 database_id: DatabaseId,
680 associated_source_id: SourceId,
681 staging_table_id: TableId,
682 trigger_time: NaiveDateTime,
683 actors: RefreshCycleActors,
684 ) -> Self {
685 Self {
686 database_id,
687 associated_source_id,
688 staging_table_id,
689 trigger_time,
690 list: StageProgress::new(actors.list),
691 fetch: StageProgress::new(actors.fetch),
692 mview: StageProgress::new(actors.mview),
693 start_time: Instant::now(),
694 }
695 }
696
697 fn stage_mut(&mut self, stage: RefreshStage) -> &mut StageProgress {
698 match stage {
699 RefreshStage::List => &mut self.list,
700 RefreshStage::Fetch => &mut self.fetch,
701 RefreshStage::Mview => &mut self.mview,
702 }
703 }
704}
705
706#[derive(Debug)]
707struct StageProgress {
708 expected: HashSet<ActorId>,
709 finished: HashSet<ActorId>,
710}
711
712impl StageProgress {
713 fn new(expected: HashSet<ActorId>) -> Self {
714 Self {
715 expected,
716 finished: HashSet::new(),
717 }
718 }
719
720 fn report(&mut self, stage: RefreshStage, actors: &HashSet<ActorId>) -> MetaResult<bool> {
722 self.finished.extend(actors);
723 if self.finished.len() < self.expected.len() {
724 return Ok(false);
725 }
726 if self.finished == self.expected {
727 Ok(true)
728 } else {
729 Err(anyhow!(
730 "{stage:?} finished actors mismatch: expected: {:?}, actual: {:?}",
731 self.expected,
732 self.finished
733 )
734 .into())
735 }
736 }
737}