risingwave_meta/hummock/manager/
context.rs1use std::collections::{BTreeMap, HashMap, HashSet};
16use std::sync::Arc;
17
18use fail::fail_point;
19use itertools::Itertools;
20use risingwave_common::catalog::TableId;
21use risingwave_hummock_sdk::version::HummockVersion;
22use risingwave_hummock_sdk::{
23 HummockContextId, HummockSstableObjectId, HummockVersionId, INVALID_VERSION_ID,
24 LocalSstableInfo,
25};
26use risingwave_meta_model::hummock_gc_history;
27use risingwave_pb::hummock::{HummockPinnedVersion, ValidationTask};
28use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
29
30use crate::controller::SqlMetaStore;
31use crate::hummock::HummockManager;
32use crate::hummock::error::{Error, Result};
33use crate::hummock::manager::commit_multi_var;
34use crate::hummock::manager::worker::{HummockManagerEvent, HummockManagerEventSender};
35use crate::hummock::metrics_utils::trigger_pin_unpin_version_state;
36use crate::manager::{META_NODE_ID, MetadataManager};
37use crate::model::BTreeMapTransaction;
38use crate::rpc::metrics::MetaMetrics;
39
40const GC_HISTORY_QUERY_BATCH_SIZE: usize = 1000;
41
42pub struct HummockVersionSafePoint {
46 pub id: HummockVersionId,
47 event_sender: HummockManagerEventSender,
48 _change_log_guard: super::gc::change_log_retention::ChangeLogObjectGuard,
49}
50
51impl Drop for HummockVersionSafePoint {
52 fn drop(&mut self) {
53 if self
54 .event_sender
55 .send(HummockManagerEvent::DropSafePoint(self.id))
56 .is_err()
57 {
58 tracing::debug!("failed to drop hummock version safe point {}", self.id);
59 }
60 }
61}
62
63#[derive(Default)]
64pub(super) struct ContextInfo {
65 pub pinned_versions: BTreeMap<HummockContextId, HummockPinnedVersion>,
66 pub version_safe_points: Vec<HummockVersionId>,
68}
69
70impl ContextInfo {
71 async fn release_contexts(
75 &mut self,
76 context_ids: impl AsRef<[HummockContextId]>,
77 meta_store_ref: SqlMetaStore,
78 ) -> Result<()> {
79 fail_point!("release_contexts_metastore_err", |_| Err(Error::MetaStore(
80 anyhow::anyhow!("failpoint metastore error")
81 )));
82 fail_point!("release_contexts_internal_err", |_| Err(Error::Internal(
83 anyhow::anyhow!("failpoint internal error")
84 )));
85
86 let mut pinned_versions = BTreeMapTransaction::new(&mut self.pinned_versions);
87 for context_id in context_ids.as_ref() {
88 pinned_versions.remove(*context_id);
89 }
90 commit_multi_var!(meta_store_ref, pinned_versions)?;
91
92 Ok(())
93 }
94}
95
96impl HummockManager {
97 pub async fn release_contexts(
98 &self,
99 context_ids: impl AsRef<[HummockContextId]>,
100 ) -> Result<()> {
101 let mut context_info = self
102 .context_info
103 .write_with_process_name("release_contexts")
104 .await;
105 context_info
106 .release_contexts(context_ids, self.env.meta_store())
107 .await?;
108 #[cfg(test)]
109 {
110 drop(context_info);
111 self.check_state_consistency().await;
112 }
113 Ok(())
114 }
115
116 pub async fn check_context(&self, context_id: HummockContextId) -> Result<bool> {
118 Ok(self
119 .context_info
120 .read_with_process_name("check_context")
121 .await
122 .check_context(context_id, &self.metadata_manager)
123 .await)
124 }
125
126 async fn check_context_with_meta_node(
127 &self,
128 context_id: HummockContextId,
129 context_info: &ContextInfo,
130 ) -> Result<()> {
131 if context_id == META_NODE_ID {
132 } else if !context_info
134 .check_context(context_id, &self.metadata_manager)
135 .await
136 {
137 return Err(Error::InvalidContext(context_id));
139 }
140 Ok(())
141 }
142
143 #[cfg(any(test, feature = "test"))]
144 pub async fn get_min_pinned_version_id(&self) -> HummockVersionId {
145 self.context_info
146 .read_with_process_name("get_min_pinned_version_id")
147 .await
148 .min_pinned_version_id()
149 }
150}
151
152impl ContextInfo {
153 pub(super) async fn check_context(
157 &self,
158 context_id: HummockContextId,
159 metadata_manager: &MetadataManager,
160 ) -> bool {
161 metadata_manager
164 .get_worker_info_by_id(context_id)
165 .await
166 .is_some()
167 }
168}
169
170impl HummockManager {
171 pub(super) async fn release_invalid_contexts(&self) -> Result<Vec<HummockContextId>> {
173 let (active_context_ids, mut context_info) = {
174 let compaction_guard = self
175 .compaction
176 .read_with_process_name("release_invalid_contexts")
177 .await;
178 let context_info = self
179 .context_info
180 .write_with_process_name("release_invalid_contexts")
181 .await;
182 let mut active_context_ids = HashSet::new();
183 active_context_ids.extend(
184 compaction_guard
185 .compact_task_assignment
186 .values()
187 .map(|c| c.context_id),
188 );
189 active_context_ids.extend(context_info.pinned_versions.keys());
190 (active_context_ids, context_info)
191 };
192
193 let mut invalid_context_ids = vec![];
194 for active_context_id in &active_context_ids {
195 if !context_info
196 .check_context(*active_context_id, &self.metadata_manager)
197 .await
198 {
199 invalid_context_ids.push(*active_context_id);
200 }
201 }
202
203 context_info
204 .release_contexts(&invalid_context_ids, self.env.meta_store())
205 .await?;
206
207 Ok(invalid_context_ids)
208 }
209
210 pub async fn commit_epoch_sanity_check(
211 &self,
212 tables_to_commit: &HashMap<TableId, u64>,
213 sstables: &[LocalSstableInfo],
214 sst_to_context: &HashMap<HummockSstableObjectId, HummockContextId>,
215 current_version: &HummockVersion,
216 ) -> Result<()> {
217 use risingwave_pb::hummock::subscribe_compaction_event_response::Event as ResponseEvent;
218
219 let context_info = self
220 .context_info
221 .read_with_process_name("commit_epoch_sanity_check")
222 .await;
223 let mut checked_contexts = HashSet::new();
224 for (sst_id, context_id) in sst_to_context {
225 if !checked_contexts.insert(*context_id) {
226 continue;
227 }
228 #[cfg(test)]
229 {
230 if *context_id == crate::manager::META_NODE_ID {
231 continue;
232 }
233 }
234 if !context_info
235 .check_context(*context_id, &self.metadata_manager)
236 .await
237 {
238 return Err(Error::InvalidSst(*sst_id));
239 }
240 }
241 drop(context_info);
242
243 for (table_id, committed_epoch) in tables_to_commit {
245 if let Some(info) = current_version.state_table_info.info().get(table_id)
246 && *committed_epoch <= info.committed_epoch
247 {
248 return Err(anyhow::anyhow!(
249 "table {} Epoch {} <= committed_epoch {}",
250 table_id,
251 committed_epoch,
252 info.committed_epoch,
253 )
254 .into());
255 }
256 }
257
258 if !sstables.is_empty() {
260 let now = self.now().await?;
262 check_sst_retention(
263 now,
264 self.env.opts.min_sst_retention_time_sec,
265 sstables
266 .iter()
267 .map(|s| (s.sst_info.object_id, s.created_at)),
268 )?;
269 if self.env.opts.gc_history_retention_time_sec != 0 {
270 let ids = sstables.iter().map(|s| s.sst_info.object_id).collect_vec();
271 check_gc_history(&self.meta_store_ref().conn, ids).await?;
272 }
273 }
274
275 async {
276 if !self.env.opts.enable_committed_sst_sanity_check {
277 return;
278 }
279 if sstables.is_empty() {
280 return;
281 }
282 let compactor = match self.compactor_manager.next_compactor() {
283 None => {
284 tracing::warn!("Skip committed SST sanity check due to no available worker");
285 return;
286 }
287 Some(compactor) => compactor,
288 };
289 let sst_infos = sstables
290 .iter()
291 .map(|LocalSstableInfo { sst_info, .. }| sst_info.clone())
292 .collect_vec();
293 if compactor
294 .send_event(
295 #[expect(deprecated)]
296 ResponseEvent::ValidationTask(ValidationTask {
297 sst_infos: sst_infos.into_iter().map(|sst| sst.into()).collect_vec(),
298 sst_id_to_worker_id: sst_to_context
299 .iter()
300 .map(|(object_id, worker_id)| (*object_id, *worker_id))
301 .collect(),
302 }),
303 )
304 .is_err()
305 {
306 tracing::warn!("Skip committed SST sanity check due to send failure");
307 }
308 }
309 .await;
310 Ok(())
311 }
312
313 pub async fn release_meta_context(&self) -> Result<()> {
314 self.release_contexts([META_NODE_ID]).await
315 }
316
317 pub(crate) async fn report_compaction_sanity_check(
318 &self,
319 object_timestamps: &HashMap<HummockSstableObjectId, u64>,
320 ) -> Result<()> {
321 if object_timestamps.is_empty() {
323 return Ok(());
324 }
325 let now = self.now().await?;
326 check_sst_retention(
327 now,
328 self.env.opts.min_sst_retention_time_sec,
329 object_timestamps.iter().map(|(k, v)| (*k, *v)),
330 )?;
331 if self.env.opts.gc_history_retention_time_sec != 0 {
332 let ids = object_timestamps.keys().copied().collect_vec();
333 check_gc_history(&self.meta_store_ref().conn, ids).await?;
334 }
335 Ok(())
336 }
337}
338
339fn check_sst_retention(
340 now: u64,
341 retention_sec: u64,
342 sst_infos: impl Iterator<Item = (HummockSstableObjectId, u64)>,
343) -> Result<()> {
344 let sst_retention_watermark = now.saturating_sub(retention_sec);
345 for (object_id, created_at) in sst_infos {
346 if created_at < sst_retention_watermark {
347 return Err(anyhow::anyhow!("object {object_id} is rejected from being committed since it's below watermark: object timestamp {created_at}, meta node timestamp {now}, retention_sec {retention_sec}, watermark {sst_retention_watermark}").into());
348 }
349 }
350 Ok(())
351}
352
353async fn check_gc_history(
354 db: &DatabaseConnection,
355 object_ids: impl IntoIterator<Item = HummockSstableObjectId>,
356) -> Result<()> {
357 let object_ids = object_ids.into_iter().collect_vec();
358 let mut expired_object_ids = Vec::new();
359 for object_ids in object_ids.chunks(GC_HISTORY_QUERY_BATCH_SIZE) {
360 expired_object_ids.extend(
361 hummock_gc_history::Entity::find()
362 .filter(hummock_gc_history::Column::ObjectId.is_in(object_ids.iter().copied()))
363 .all(db)
364 .await?,
365 );
366 }
367 if expired_object_ids.is_empty() {
368 return Ok(());
369 }
370 tracing::error!(
371 ?expired_object_ids,
372 "new SSTs are rejected because they have already been GCed"
373 );
374 Err(Error::InvalidSst(expired_object_ids[0].object_id))
375}
376
377impl HummockManager {
379 pub async fn pin_version(&self, context_id: HummockContextId) -> Result<Arc<HummockVersion>> {
382 let versioning = self.versioning.read_with_process_name("pin_version").await;
383 let mut context_info = self
384 .context_info
385 .write_with_process_name("pin_version")
386 .await;
387 self.check_context_with_meta_node(context_id, &context_info)
388 .await?;
389 let mut pinned_versions = BTreeMapTransaction::new(&mut context_info.pinned_versions);
390 let mut context_pinned_version = pinned_versions.new_entry_txn_or_default(
391 context_id,
392 HummockPinnedVersion {
393 context_id,
394 min_pinned_id: INVALID_VERSION_ID,
395 },
396 );
397 let version_id = versioning.current_version.id;
398 let ret = versioning.current_version.clone();
399 if context_pinned_version.min_pinned_id == INVALID_VERSION_ID
400 || context_pinned_version.min_pinned_id > version_id
401 {
402 context_pinned_version.min_pinned_id = version_id;
403 commit_multi_var!(self.meta_store_ref(), context_pinned_version)?;
404 trigger_pin_unpin_version_state(&self.metrics, &context_info.pinned_versions);
405 }
406
407 #[cfg(test)]
408 {
409 drop(context_info);
410 drop(versioning);
411 self.check_state_consistency().await;
412 }
413
414 Ok(ret)
415 }
416
417 pub async fn unpin_version_before(
421 &self,
422 context_id: HummockContextId,
423 unpin_before: HummockVersionId,
424 ) -> Result<()> {
425 let mut context_info = self
426 .context_info
427 .write_with_process_name("unpin_version_before")
428 .await;
429 self.check_context_with_meta_node(context_id, &context_info)
430 .await?;
431 let mut pinned_versions = BTreeMapTransaction::new(&mut context_info.pinned_versions);
432 let mut context_pinned_version = pinned_versions.new_entry_txn_or_default(
433 context_id,
434 HummockPinnedVersion {
435 context_id,
436 min_pinned_id: HummockVersionId::default(),
437 },
438 );
439 assert!(
440 context_pinned_version.min_pinned_id <= unpin_before,
441 "val must be monotonically non-decreasing. old = {}, new = {}.",
442 context_pinned_version.min_pinned_id,
443 unpin_before
444 );
445 context_pinned_version.min_pinned_id = unpin_before;
446 commit_multi_var!(self.meta_store_ref(), context_pinned_version)?;
447 trigger_pin_unpin_version_state(&self.metrics, &context_info.pinned_versions);
448
449 #[cfg(test)]
450 {
451 drop(context_info);
452 self.check_state_consistency().await;
453 }
454
455 Ok(())
456 }
457}
458
459impl HummockManager {
461 pub async fn register_safe_point(&self) -> HummockVersionSafePoint {
462 let versioning = self
463 .versioning
464 .read_with_process_name("register_safe_point")
465 .await;
466 let mut wl = self
467 .context_info
468 .write_with_process_name("register_safe_point")
469 .await;
470 let safe_point = HummockVersionSafePoint {
471 id: versioning.current_version.id,
472 event_sender: self.event_sender.clone(),
473 _change_log_guard: self.gc_manager.change_log_retention.pin(false),
474 };
475 wl.version_safe_points.push(safe_point.id);
476 trigger_safepoint_stat(&self.metrics, &wl.version_safe_points);
477 safe_point
478 }
479
480 pub async fn unregister_safe_point(&self, safe_point: HummockVersionId) {
481 let mut wl = self
482 .context_info
483 .write_with_process_name("unregister_safe_point")
484 .await;
485 let version_safe_points = &mut wl.version_safe_points;
486 if let Some(pos) = version_safe_points.iter().position(|sp| *sp == safe_point) {
487 version_safe_points.remove(pos);
488 }
489 trigger_safepoint_stat(&self.metrics, &wl.version_safe_points);
490 }
491}
492
493fn trigger_safepoint_stat(metrics: &MetaMetrics, safepoints: &[HummockVersionId]) {
494 if let Some(sp) = safepoints.iter().min() {
495 metrics.min_safepoint_version_id.set(sp.as_i64_id());
496 } else {
497 metrics.min_safepoint_version_id.set(u64::MAX as _);
498 }
499}
500
501#[cfg(test)]
502mod tests {
503 use sea_orm::ActiveModelTrait;
504 use sea_orm::ActiveValue::Set;
505
506 use super::*;
507 use crate::hummock::test_utils::setup_compute_env;
508
509 #[tokio::test]
510 async fn test_check_gc_history_in_batches() {
511 let (env, ..) = setup_compute_env(80).await;
512 let object_ids = (1..=GC_HISTORY_QUERY_BATCH_SIZE as u64 + 1)
513 .map(Into::into)
514 .collect_vec();
515 let expired_object_id = *object_ids.last().unwrap();
516 hummock_gc_history::ActiveModel {
517 object_id: Set(expired_object_id),
518 mark_delete_at: Set(Default::default()),
519 }
520 .insert(&env.meta_store_ref().conn)
521 .await
522 .unwrap();
523
524 let error = check_gc_history(&env.meta_store_ref().conn, object_ids)
525 .await
526 .unwrap_err();
527 assert!(matches!(error, Error::InvalidSst(id) if id == expired_object_id));
528 }
529}