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