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