1use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17use std::time::Instant;
18
19use anyhow::{Context, anyhow};
20use risingwave_common::catalog::TableId;
21use risingwave_common::util::epoch::Epoch;
22use risingwave_hummock_sdk::change_log::TableChangeLog;
23use risingwave_hummock_sdk::version::HummockVersion;
24use sea_orm::{
25 ColumnTrait, Condition, ConnectionTrait, DbBackend, EntityTrait, FromQueryResult, QueryFilter,
26 QueryTrait, TransactionTrait,
27};
28use tracing::warn;
29
30use crate::controller::streaming_job::{
31 ProgressTableChangeLogSubscriber, TableChangeLogTruncateInfo,
32};
33use crate::hummock::HummockManager;
34use crate::hummock::error::{Error, Result};
35use crate::hummock::manager::gc::change_log_retention::RetainedChangeLogBatch;
36use crate::hummock::model::ext::to_table_change_log;
37
38#[derive(Clone, Debug)]
39pub struct CommittedTableChangeLogProgress {
40 pub upstream_table_id: TableId,
41 pub progress_table_id: TableId,
42 pub truncate_epoch: u64,
43}
44
45fn update_truncate_epoch(
46 truncate_epochs: &mut HashMap<TableId, u64>,
47 table_id: TableId,
48 truncate_epoch: u64,
49) {
50 truncate_epochs
51 .entry(table_id)
52 .and_modify(|epoch| *epoch = (*epoch).min(truncate_epoch))
53 .or_insert(truncate_epoch);
54}
55
56fn resolve_progress_table_truncate_epochs(
57 subscribers: &[ProgressTableChangeLogSubscriber],
58 progress: &HashMap<TableId, (TableId, u64)>,
59) -> (HashMap<TableId, u64>, HashSet<TableId>) {
60 let mut truncate_epochs = HashMap::new();
61 let mut untruncatable_table_ids = HashSet::new();
62 for subscriber in subscribers {
63 match progress.get(&subscriber.progress_table_id) {
64 Some((upstream_table_id, truncate_epoch))
65 if *upstream_table_id == subscriber.upstream_table_id =>
66 {
67 update_truncate_epoch(&mut truncate_epochs, *upstream_table_id, *truncate_epoch);
68 }
69 Some((upstream_table_id, _)) => {
70 debug_assert_eq!(
71 *upstream_table_id, subscriber.upstream_table_id,
72 "progress table {} changed upstream table",
73 subscriber.progress_table_id
74 );
75 warn!(
76 progress_table_id = %subscriber.progress_table_id,
77 expected_upstream_table_id = %subscriber.upstream_table_id,
78 actual_upstream_table_id = %upstream_table_id,
79 "progress table changed upstream table; veto table change log truncation"
80 );
81 untruncatable_table_ids.insert(subscriber.upstream_table_id);
82 }
83 None => {
84 untruncatable_table_ids.insert(subscriber.upstream_table_id);
85 }
86 }
87 }
88 (truncate_epochs, untruncatable_table_ids)
89}
90
91fn resolve_table_change_log_truncate_epochs(
92 info: &TableChangeLogTruncateInfo,
93 version: &HummockVersion,
94 current_time_epoch: Epoch,
95) -> anyhow::Result<HashMap<TableId, u64>> {
96 let mut truncate_epochs = HashMap::new();
97 let mut untruncatable_table_ids = HashSet::new();
98 for (table_id, retention_seconds) in &info.subscription_retention_seconds {
99 if version.table_committed_epoch(*table_id).is_none() {
100 tracing::warn!(
102 %table_id,
103 "cannot get committed epoch for subscribed table, skip table change log truncation"
104 );
105 continue;
106 }
107 let truncate_epoch = current_time_epoch
108 .subtract_ms(retention_seconds.saturating_mul(1000))
109 .0;
110 update_truncate_epoch(&mut truncate_epochs, *table_id, truncate_epoch);
111 }
112
113 for job in &info.committed_epoch_truncate_jobs {
114 let mut all_snapshot_epochs_none = true;
115 for (upstream_table_id, snapshot_epoch) in &job.upstream_table_snapshot_epochs {
116 match snapshot_epoch {
117 Some(_) => all_snapshot_epochs_none = false,
118 None => {
119 untruncatable_table_ids.insert(*upstream_table_id);
122 }
123 }
124 }
125 if all_snapshot_epochs_none {
126 continue;
127 }
128
129 let mut state_table_ids = job.state_table_ids.iter();
130 let first_table_id = state_table_ids.next().ok_or_else(|| {
131 anyhow!(
132 "committed epoch truncate job {} has no state table",
133 job.job_id
134 )
135 })?;
136 let committed_epoch = version
137 .table_committed_epoch(*first_table_id)
138 .ok_or_else(|| {
139 anyhow!(
140 "cannot get committed epoch of state table {} in committed epoch truncate job {}",
141 first_table_id,
142 job.job_id
143 )
144 })?;
145 for table_id in state_table_ids {
146 let table_committed_epoch =
147 version.table_committed_epoch(*table_id).ok_or_else(|| {
148 anyhow!(
149 "cannot get committed epoch of state table {} in committed epoch truncate job {}",
150 table_id,
151 job.job_id
152 )
153 })?;
154 if table_committed_epoch != committed_epoch {
155 return Err(anyhow!(
156 "state tables {} and {} in committed epoch truncate job {} have different committed epochs {} and {}",
157 first_table_id,
158 table_id,
159 job.job_id,
160 committed_epoch,
161 table_committed_epoch
162 ));
163 }
164 }
165
166 for (upstream_table_id, snapshot_epoch) in &job.upstream_table_snapshot_epochs {
167 if let Some(snapshot_epoch) = snapshot_epoch {
168 let pinned_epoch = committed_epoch.max(*snapshot_epoch);
169 update_truncate_epoch(&mut truncate_epochs, *upstream_table_id, pinned_epoch);
170 }
171 }
172 }
173 truncate_epochs.retain(|table_id, _| !untruncatable_table_ids.contains(table_id));
174 Ok(truncate_epochs)
175}
176
177impl HummockManager {
178 pub(super) fn update_table_change_log_progress(
179 &self,
180 updates: impl IntoIterator<Item = CommittedTableChangeLogProgress>,
181 ) {
182 let mut progress = self.table_change_log_progress.write();
183 for update in updates {
184 let key = update.progress_table_id;
185 progress
186 .entry(key)
187 .and_modify(|(upstream_table_id, truncate_epoch)| {
188 assert_eq!(*upstream_table_id, update.upstream_table_id);
189 if update.truncate_epoch < *truncate_epoch {
190 debug_assert!(
191 update.truncate_epoch >= *truncate_epoch,
192 "table change log progress regressed from {} to {} for progress table {}",
193 *truncate_epoch,
194 update.truncate_epoch,
195 update.progress_table_id,
196 );
197 warn!(
198 progress_table_id = %update.progress_table_id,
199 previous_epoch = *truncate_epoch,
200 reported_epoch = update.truncate_epoch,
201 "table change log progress regressed"
202 );
203 } else {
204 *truncate_epoch = update.truncate_epoch;
205 }
206 })
207 .or_insert((update.upstream_table_id, update.truncate_epoch));
208 }
209 }
210
211 pub async fn truncate_table_change_log(&self, info: TableChangeLogTruncateInfo) -> Result<()> {
212 let _timer = self.metrics.table_change_log_truncate_latency.start_timer();
213 let resolution_started_at = Instant::now();
214 let (progress_table_truncate_epochs, progress_table_untruncatable_table_ids) = {
215 let progress = self.table_change_log_progress.read();
216 resolve_progress_table_truncate_epochs(&info.progress_table_subscribers, &progress)
217 };
218 let mut versioning = self
219 .versioning
220 .write_with_process_name("truncate_table_change_log")
221 .await;
222 let current_time_epoch = Epoch::now();
223 let truncate_epochs = resolve_table_change_log_truncate_epochs(
224 &info,
225 versioning.current_version.as_ref(),
226 current_time_epoch,
227 )
228 .map_err(Error::Internal)?;
229 let mut truncate_epochs = truncate_epochs;
230 for (table_id, truncate_epoch) in progress_table_truncate_epochs {
231 update_truncate_epoch(&mut truncate_epochs, table_id, truncate_epoch);
232 }
233 truncate_epochs
234 .retain(|table_id, _| !progress_table_untruncatable_table_ids.contains(table_id));
235 let truncate_epochs: Vec<_> = truncate_epochs
236 .into_iter()
237 .filter(|(table_id, _)| versioning.table_change_log.contains_key(table_id))
238 .collect();
239 let resolution_time = resolution_started_at.elapsed();
240 let table_count = truncate_epochs.len();
241 if truncate_epochs.is_empty() {
242 tracing::info!(
243 resolution_time = ?resolution_time,
244 truncation_time = ?std::time::Duration::ZERO,
245 table_count,
246 rows_affected = 0,
247 may_delete_object_count = 0,
248 "table change log truncation finished"
249 );
250 return Ok(());
251 }
252
253 let truncation_started_at = Instant::now();
254 let sql_store = self.env.meta_store_ref();
255 let txn = sql_store.conn.begin().await?;
256 let batch_size = self.env.opts.table_change_log_delete_batch_size as usize;
257 let mut rows_affected = 0;
258 let mut may_delete_object_ids = HashSet::new();
259 for batch in truncate_epochs.chunks(batch_size) {
260 let mut condition = Condition::any();
261 for (table_id, truncate_epoch) in batch {
262 let truncate_epoch = risingwave_meta_model::Epoch::try_from(*truncate_epoch)
263 .context("table change log truncate epoch exceeds meta store range")
264 .map_err(Error::Internal)?;
265 condition = condition.add(
266 Condition::all()
267 .add(
268 risingwave_meta_model::hummock_table_change_log::Column::TableId
269 .eq(*table_id),
270 )
271 .add(
272 risingwave_meta_model::hummock_table_change_log::Column::CheckpointEpoch
273 .lt(truncate_epoch),
274 ),
275 );
276 }
277 let (change_logs_to_delete, deleted_count) = match txn.get_database_backend() {
278 DbBackend::Postgres => {
279 let mut delete =
280 risingwave_meta_model::hummock_table_change_log::Entity::delete_many()
281 .filter(condition)
282 .into_query();
283 delete.returning_all();
284 let statement = DbBackend::Postgres.build(&delete);
285 let change_logs_to_delete = txn
286 .query_all(statement)
287 .await?
288 .iter()
289 .map(|row| {
290 risingwave_meta_model::hummock_table_change_log::Model::from_query_result(
291 row, "",
292 )
293 })
294 .collect::<std::result::Result<Vec<_>, _>>()?;
295 let deleted_count = change_logs_to_delete.len() as u64;
296 (change_logs_to_delete, deleted_count)
297 }
298 DbBackend::MySql | DbBackend::Sqlite => {
299 let change_logs_to_delete =
302 risingwave_meta_model::hummock_table_change_log::Entity::find()
303 .filter(condition.clone())
304 .all(&txn)
305 .await?;
306 let deleted_count =
307 risingwave_meta_model::hummock_table_change_log::Entity::delete_many()
308 .filter(condition)
309 .exec(&txn)
310 .await?
311 .rows_affected;
312 (change_logs_to_delete, deleted_count)
313 }
314 };
315 for change_log_to_delete in change_logs_to_delete {
316 let deleted_change_log =
317 TableChangeLog::new([to_table_change_log(change_log_to_delete)]);
318 may_delete_object_ids.extend(deleted_change_log.get_object_ids());
319 }
320 rows_affected += deleted_count;
321 }
322 txn.commit().await?;
323
324 for (table_id, truncate_epoch) in truncate_epochs {
325 if let Some(change_log) = versioning.table_change_log.get_mut(&table_id) {
326 change_log.truncate(truncate_epoch);
327 }
328 }
329 let may_delete_object_ids = Arc::new(may_delete_object_ids);
332 self.gc_manager
333 .change_log_retention
334 .retain_batch(RetainedChangeLogBatch::ObjectIds(
335 may_delete_object_ids.clone(),
336 ));
337 drop(versioning);
338 let may_delete_object_count = may_delete_object_ids.len();
339 self.gc_manager
340 .add_may_delete_object_ids(may_delete_object_ids.iter().copied());
341 let truncation_time = truncation_started_at.elapsed();
342 tracing::info!(
343 resolution_time = ?resolution_time,
344 truncation_time = ?truncation_time,
345 table_count,
346 rows_affected,
347 may_delete_object_count,
348 "truncated table change logs"
349 );
350 Ok(())
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use std::collections::{HashMap, HashSet};
357
358 use risingwave_common::id::JobId;
359 use risingwave_pb::hummock::StateTableInfoDelta;
360
361 use super::*;
362 use crate::controller::streaming_job::{
363 CommittedEpochTruncateJob, ProgressTableChangeLogSubscriber,
364 };
365
366 #[test]
367 fn test_resolve_progress_table_truncate_epochs() {
368 let upstream_table_id = TableId::new(1);
369 let subscribers = vec![
370 ProgressTableChangeLogSubscriber {
371 subscriber_job_id: JobId::new(2),
372 upstream_table_id,
373 progress_table_id: TableId::new(3),
374 },
375 ProgressTableChangeLogSubscriber {
376 subscriber_job_id: JobId::new(4),
377 upstream_table_id,
378 progress_table_id: TableId::new(5),
379 },
380 ];
381 let progress = HashMap::from([
382 (TableId::new(3), (upstream_table_id, 100)),
383 (TableId::new(5), (upstream_table_id, 80)),
384 ]);
385
386 let (truncate_epochs, untruncatable) =
387 resolve_progress_table_truncate_epochs(&subscribers, &progress);
388 assert_eq!(truncate_epochs, HashMap::from([(upstream_table_id, 80)]));
389 assert!(untruncatable.is_empty());
390
391 let partial_progress = HashMap::from([(TableId::new(3), (upstream_table_id, 100))]);
392 let (truncate_epochs, untruncatable) =
393 resolve_progress_table_truncate_epochs(&subscribers, &partial_progress);
394 assert_eq!(truncate_epochs, HashMap::from([(upstream_table_id, 100)]));
395 assert_eq!(untruncatable, HashSet::from([upstream_table_id]));
396 }
397
398 fn version_with_committed_epochs(
399 committed_epochs: impl IntoIterator<Item = (TableId, u64)>,
400 ) -> HummockVersion {
401 let mut version = HummockVersion::default();
402 let mut delta = version.version_delta_after();
403 for (table_id, committed_epoch) in committed_epochs {
404 delta.state_table_info_delta.insert(
405 table_id,
406 StateTableInfoDelta {
407 committed_epoch,
408 compaction_group_id: 1.into(),
409 },
410 );
411 }
412 version.apply_version_delta(&delta);
413 version
414 }
415
416 #[test]
417 fn test_resolve_table_change_log_truncate_epochs() {
418 let upstream_table_id = TableId::new(1);
419 let job_state_table_id = TableId::new(2);
420 let current_time_epoch = Epoch::from_physical_time(100_000);
421 let subscription_epoch = Epoch::from_physical_time(70_000).0;
422 let job_committed_epoch = Epoch::from_physical_time(80_000).0;
423 let snapshot_epoch = Epoch::from_physical_time(85_000).0;
424 let version = version_with_committed_epochs([
425 (upstream_table_id, subscription_epoch),
426 (job_state_table_id, job_committed_epoch),
427 ]);
428 let info = TableChangeLogTruncateInfo {
429 subscription_retention_seconds: HashMap::from([(upstream_table_id, 10)]),
430 progress_table_subscribers: vec![],
431 committed_epoch_truncate_jobs: vec![CommittedEpochTruncateJob {
432 job_id: JobId::new(3),
433 state_table_ids: HashSet::from([job_state_table_id]),
434 upstream_table_snapshot_epochs: HashMap::from([(
435 upstream_table_id,
436 Some(snapshot_epoch),
437 )]),
438 }],
439 };
440
441 let truncate_epochs =
442 resolve_table_change_log_truncate_epochs(&info, &version, current_time_epoch).unwrap();
443 assert_eq!(truncate_epochs[&upstream_table_id], snapshot_epoch);
444 }
445
446 #[test]
447 fn test_missing_snapshot_epoch_prevents_truncation() {
448 let upstream_table_id = TableId::new(1);
449 let job_state_table_id = TableId::new(2);
450 let upstream_committed_epoch = Epoch::from_physical_time(100_000).0;
451 let job_committed_epoch = Epoch::from_physical_time(80_000).0;
452 let version = version_with_committed_epochs([
453 (upstream_table_id, upstream_committed_epoch),
454 (job_state_table_id, job_committed_epoch),
455 ]);
456 let info = TableChangeLogTruncateInfo {
457 subscription_retention_seconds: HashMap::from([(upstream_table_id, 10)]),
458 progress_table_subscribers: vec![],
459 committed_epoch_truncate_jobs: vec![CommittedEpochTruncateJob {
460 job_id: JobId::new(3),
461 state_table_ids: HashSet::from([job_state_table_id]),
462 upstream_table_snapshot_epochs: HashMap::from([(upstream_table_id, None)]),
463 }],
464 };
465
466 let truncate_epochs = resolve_table_change_log_truncate_epochs(
467 &info,
468 &version,
469 Epoch::from_physical_time(100_000),
470 )
471 .unwrap();
472 assert!(!truncate_epochs.contains_key(&upstream_table_id));
473 }
474
475 #[test]
476 fn test_inconsistent_job_committed_epoch_fails() {
477 let state_table_id_1 = TableId::new(1);
478 let state_table_id_2 = TableId::new(2);
479 let version = version_with_committed_epochs([
480 (state_table_id_1, Epoch::from_physical_time(1).0),
481 (state_table_id_2, Epoch::from_physical_time(2).0),
482 ]);
483 let info = TableChangeLogTruncateInfo {
484 subscription_retention_seconds: HashMap::new(),
485 progress_table_subscribers: vec![],
486 committed_epoch_truncate_jobs: vec![CommittedEpochTruncateJob {
487 job_id: JobId::new(3),
488 state_table_ids: HashSet::from([state_table_id_1, state_table_id_2]),
489 upstream_table_snapshot_epochs: HashMap::from([(
490 TableId::new(4),
491 Some(Epoch::from_physical_time(1).0),
492 )]),
493 }],
494 };
495
496 assert!(
497 resolve_table_change_log_truncate_epochs(
498 &info,
499 &version,
500 Epoch::from_physical_time(100_000),
501 )
502 .is_err()
503 );
504 }
505}