Skip to main content

risingwave_meta/barrier/
complete_task.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;
16use std::future::{Future, pending};
17use std::mem::replace;
18use std::sync::Arc;
19
20use anyhow::Context;
21use futures::future::try_join_all;
22use risingwave_common::id::JobId;
23use risingwave_common::must_match;
24use risingwave_common::util::deployment::Deployment;
25use risingwave_pb::hummock::HummockVersionStats;
26use risingwave_pb::id::{DatabaseId, PartialGraphId};
27use risingwave_pb::stream_service::barrier_complete_response::{
28    PbIcebergPkIndexSinkMetadata, PbListFinishedSource, PbLoadFinishedSource,
29};
30use tokio::task::JoinHandle;
31
32use crate::barrier::checkpoint::CheckpointControl;
33use crate::barrier::context::GlobalBarrierWorkerContext;
34use crate::barrier::info::BarrierInfo;
35use crate::barrier::partial_graph::{PartialGraphBarrierInfo, PartialGraphManager};
36use crate::barrier::progress::TrackingJob;
37use crate::barrier::rpc::from_partial_graph_id;
38use crate::barrier::schedule::PeriodicBarriers;
39use crate::hummock::CommitEpochInfo;
40use crate::manager::MetaSrvEnv;
41use crate::rpc::metrics::GLOBAL_META_METRICS;
42use crate::{MetaError, MetaResult};
43
44pub(super) enum CompletingTask {
45    None,
46    Completing {
47        #[expect(clippy::type_complexity)]
48        /// `database_id` -> (`Some(database_graph_committed_epoch)`, [(`creating_job_id`, `creating_job_committed_epoch`)])
49        epochs_to_ack: HashMap<DatabaseId, (Option<u64>, Vec<(JobId, u64)>)>,
50
51        // The join handle of a spawned task that completes the barrier.
52        // The return value indicate whether there is some create streaming job command
53        // that has finished but not checkpointed. If there is any, we will force checkpoint on the next barrier
54        join_handle: JoinHandle<MetaResult<HummockVersionStats>>,
55    },
56    #[expect(dead_code)]
57    Err(MetaError),
58}
59
60/// Only for checkpoint barrier. For normal barrier, there won't be a task.
61#[derive(Default)]
62pub(super) struct CompleteBarrierTask {
63    pub(super) commit_info: CommitEpochInfo,
64    pub(super) finished_jobs: Vec<TrackingJob>,
65    pub(super) finished_cdc_table_backfill: Vec<JobId>,
66    /// `partial_graph_id` -> barrier info for post-collect processing
67    pub(super) epoch_infos: HashMap<PartialGraphId, PartialGraphBarrierInfo>,
68    /// Source listing completion events that need `ListFinish` commands
69    pub(super) list_finished_source_ids: Vec<PbListFinishedSource>,
70    /// Source load completion events that need `LoadFinish` commands
71    pub(super) load_finished_source_ids: Vec<PbLoadFinishedSource>,
72    /// Table IDs that have finished materialize refresh and need completion signaling
73    pub(super) refresh_finished_table_job_ids: Vec<JobId>,
74    /// Iceberg pk-index sink reports collected during this barrier
75    pub(super) iceberg_pk_index_sink_metadata: Vec<PbIcebergPkIndexSinkMetadata>,
76}
77
78impl CompleteBarrierTask {
79    #[expect(clippy::type_complexity)]
80    pub(super) fn epochs_to_ack(&self) -> HashMap<DatabaseId, (Option<u64>, Vec<(JobId, u64)>)> {
81        let mut epochs_to_ack: HashMap<DatabaseId, (Option<u64>, Vec<(JobId, u64)>)> =
82            HashMap::new();
83        for (partial_graph_id, info) in &self.epoch_infos {
84            let (database_id, creating_job_id) = from_partial_graph_id(*partial_graph_id);
85            let epoch = info.barrier_info.prev_epoch();
86            let (database, jobs) = epochs_to_ack.entry(database_id).or_default();
87            if let Some(job_id) = creating_job_id {
88                jobs.push((job_id, epoch));
89            } else {
90                *database = Some(epoch);
91            }
92        }
93        epochs_to_ack
94    }
95}
96
97impl CompleteBarrierTask {
98    pub(super) async fn complete_barrier(
99        self,
100        context: &impl GlobalBarrierWorkerContext,
101        env: MetaSrvEnv,
102    ) -> MetaResult<HummockVersionStats> {
103        let mut notifiers = Vec::new();
104        let result: MetaResult<HummockVersionStats> = try {
105            let wait_commit_timer = GLOBAL_META_METRICS
106                .barrier_wait_commit_latency
107                .start_timer();
108
109            // Iceberg pk-index sink metadata reports are handled in three steps around hummock `commit_epoch`:
110            //   1. pre_commit: persist pending rows under `pending_sink_state` (no iceberg I/O).
111            //   2. commit_epoch: advance hummock.
112            //   3. commit: drive iceberg overwrite_files for queued epochs.
113            let mut iceberg_pk_index_commit_sink_ids = Vec::new();
114            if !self.iceberg_pk_index_sink_metadata.is_empty() {
115                let res = context
116                    .pre_commit_iceberg_pk_index_sink_metadata(self.iceberg_pk_index_sink_metadata)
117                    .await?;
118                iceberg_pk_index_commit_sink_ids = res;
119            }
120
121            let version_stats = context.commit_epoch(self.commit_info).await?;
122
123            if !iceberg_pk_index_commit_sink_ids.is_empty() {
124                context
125                    .commit_iceberg_pk_index_sink_metadata(iceberg_pk_index_commit_sink_ids)
126                    .await?;
127            }
128
129            // Handle list finished source IDs for refreshable batch sources
130            // Spawn this asynchronously to avoid deadlock during barrier collection
131            //
132            // This step is for fs-like refreshable-batch sources, which need to list the data first finishing loading. It guarantees finishing listing before loading.
133            // The other sources can skip this step.
134
135            if !self.list_finished_source_ids.is_empty() {
136                context
137                    .handle_list_finished_source_ids(self.list_finished_source_ids.clone())
138                    .await?;
139            }
140
141            // Handle load finished source IDs for refreshable batch sources
142            // Spawn this asynchronously to avoid deadlock during barrier collection
143            if !self.load_finished_source_ids.is_empty() {
144                context
145                    .handle_load_finished_source_ids(self.load_finished_source_ids.clone())
146                    .await?;
147            }
148
149            // Handle refresh finished table IDs for materialized view refresh completion
150            if !self.refresh_finished_table_job_ids.is_empty() {
151                context
152                    .handle_refresh_finished_table_ids(self.refresh_finished_table_job_ids.clone())
153                    .await?;
154            }
155
156            for (partial_graph_id, info) in self.epoch_infos {
157                let (database_id, job_id) = from_partial_graph_id(partial_graph_id);
158                let command_name = info.post_collect_command.command_name().to_owned();
159                let elapsed_secs = info.elapsed_secs();
160                notifiers.extend(info.notifiers);
161                context
162                    .post_collect_command(info.post_collect_command)
163                    .await?;
164                if job_id.is_none() {
165                    Self::report_complete_event(
166                        &env,
167                        database_id,
168                        elapsed_secs,
169                        &info.barrier_info,
170                        command_name,
171                    );
172                }
173            }
174
175            wait_commit_timer.observe_duration();
176            version_stats
177        };
178
179        let version_stats = {
180            let version_stats = match result {
181                Ok(version_stats) => version_stats,
182                Err(e) => {
183                    for notifier in notifiers {
184                        notifier.notify_collection_failed(e.clone());
185                    }
186                    return Err(e);
187                }
188            };
189            notifiers.into_iter().for_each(|notifier| {
190                notifier.notify_collected();
191            });
192            try_join_all(
193                self.finished_jobs
194                    .into_iter()
195                    .map(|finished_job| context.finish_creating_job(finished_job)),
196            )
197            .await?;
198            try_join_all(
199                self.finished_cdc_table_backfill
200                    .into_iter()
201                    .map(|job_id| context.finish_cdc_table_backfill(job_id)),
202            )
203            .await?;
204            version_stats
205        };
206
207        Ok(version_stats)
208    }
209}
210
211impl CompleteBarrierTask {
212    fn report_complete_event(
213        env: &MetaSrvEnv,
214        database_id: DatabaseId,
215        duration_sec: f64,
216        barrier_info: &BarrierInfo,
217        command: String,
218    ) {
219        // Record barrier latency in event log.
220        use risingwave_pb::meta::event_log;
221        let event = event_log::EventBarrierComplete {
222            prev_epoch: barrier_info.prev_epoch(),
223            cur_epoch: barrier_info.curr_epoch(),
224            duration_sec,
225            command,
226            barrier_kind: barrier_info.kind.as_str_name().to_owned(),
227            database_id: database_id.as_raw_id(),
228        };
229        if cfg!(debug_assertions) || Deployment::current().is_ci() {
230            // Add a warning log so that debug mode / CI can observe it
231            if duration_sec > 5.0 {
232                tracing::warn!(event = ?event,"high barrier latency observed!")
233            }
234        }
235        env.event_log_manager_ref()
236            .add_event_logs(vec![event_log::Event::BarrierComplete(event)]);
237    }
238}
239
240pub(super) struct BarrierCompleteOutput {
241    #[expect(clippy::type_complexity)]
242    /// `database_id` -> (`Some(database_graph_committed_epoch)`, [(`creating_job_id`, `creating_job_committed_epoch`)])
243    pub epochs_to_ack: HashMap<DatabaseId, (Option<u64>, Vec<(JobId, u64)>)>,
244    pub hummock_version_stats: HummockVersionStats,
245}
246
247impl CompletingTask {
248    pub(super) fn next_completed_barrier<'a>(
249        &'a mut self,
250        periodic_barriers: &mut PeriodicBarriers,
251        checkpoint_control: &mut CheckpointControl,
252        partial_graph_manager: &mut PartialGraphManager,
253        context: &Arc<impl GlobalBarrierWorkerContext>,
254        env: &MetaSrvEnv,
255    ) -> impl Future<Output = MetaResult<BarrierCompleteOutput>> + 'a {
256        // If there is no completing barrier, try to start completing the earliest barrier if
257        // it has been collected.
258        if let CompletingTask::None = self
259            && let Some(task) = checkpoint_control
260                .next_complete_barrier_task(periodic_barriers, partial_graph_manager)
261        {
262            {
263                let epochs_to_ack = task.epochs_to_ack();
264                let context = context.clone();
265                let await_tree_reg = env.await_tree_reg().clone();
266                let env = env.clone();
267
268                let fut = async move { task.complete_barrier(&*context, env).await };
269                let fut = await_tree_reg
270                    .register_derived_root("Barrier Completion Task")
271                    .instrument(fut);
272                let join_handle = tokio::spawn(fut);
273
274                *self = CompletingTask::Completing {
275                    epochs_to_ack,
276                    join_handle,
277                };
278            }
279        }
280
281        async move {
282            if !matches!(self, CompletingTask::Completing { .. }) {
283                return pending().await;
284            };
285            self.next_completed_barrier_inner().await
286        }
287    }
288
289    #[await_tree::instrument]
290    pub(super) async fn wait_completing_task(
291        &mut self,
292    ) -> MetaResult<Option<BarrierCompleteOutput>> {
293        match self {
294            CompletingTask::None => Ok(None),
295            CompletingTask::Completing { .. } => {
296                self.next_completed_barrier_inner().await.map(Some)
297            }
298            CompletingTask::Err(_) => {
299                unreachable!("should not be called on previous err")
300            }
301        }
302    }
303
304    async fn next_completed_barrier_inner(&mut self) -> MetaResult<BarrierCompleteOutput> {
305        let CompletingTask::Completing { join_handle, .. } = self else {
306            unreachable!()
307        };
308
309        {
310            {
311                let join_result: MetaResult<_> = try {
312                    join_handle
313                        .await
314                        .context("failed to join completing command")
315                        .map_err(MetaError::from)??
316                };
317                // It's important to reset the completing_command after await no matter the result is err
318                // or not, and otherwise the join handle will be polled again after ready.
319                let next_completing_command_status = if let Err(e) = &join_result {
320                    CompletingTask::Err(e.clone())
321                } else {
322                    CompletingTask::None
323                };
324                let completed_command = replace(self, next_completing_command_status);
325                let hummock_version_stats = join_result?;
326
327                must_match!(completed_command, CompletingTask::Completing {
328                    epochs_to_ack,
329                    ..
330                } => {
331                    Ok(BarrierCompleteOutput {
332                        epochs_to_ack,
333                        hummock_version_stats,
334                    })
335                })
336            }
337        }
338    }
339}