Skip to main content

risingwave_meta/hummock/manager/
context.rs

1// Copyright 2022 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::{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
42/// `HummockVersionSafePoint` prevents hummock versions GE than it from being GC.
43/// It's used by meta node itself to temporarily pin versions.
44pub 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    /// `version_safe_points` is similar to `pinned_versions` expect for being a transient state.
65    pub version_safe_points: Vec<HummockVersionId>,
66}
67
68impl ContextInfo {
69    /// Release resources pinned by these contexts, including:
70    /// - Version
71    /// - Snapshot
72    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    /// Checks whether `context_id` is valid.
115    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            // Using the preserved meta id is allowed.
131        } else if !context_info
132            .check_context(context_id, &self.metadata_manager)
133            .await
134        {
135            // The worker is not found in cluster.
136            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    /// Checks whether `context_id` is valid.
152    ///
153    /// Need `&self` to sync with `release_context`
154    pub(super) async fn check_context(
155        &self,
156        context_id: HummockContextId,
157        metadata_manager: &MetadataManager,
158    ) -> bool {
159        // Worker existence is kept in memory and updated under the cluster controller lock, so
160        // checking it does not need a meta store query.
161        metadata_manager
162            .get_worker_info_by_id(context_id)
163            .await
164            .is_some()
165    }
166}
167
168impl HummockManager {
169    /// Release invalid contexts, aka worker node ids which are no longer valid in `ClusterManager`.
170    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        // sanity check on monotonically increasing table committed epoch
242        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        // HummockManager::now requires a write to the meta store. Thus, it should be avoided whenever feasible.
257        if !sstables.is_empty() {
258            // Sanity check to ensure SSTs to commit have not been full GCed yet.
259            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        // HummockManager::now requires a write to the meta store. Thus, it should be avoided whenever feasible.
320        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
375// pin and unpin method
376impl HummockManager {
377    /// Pin the current greatest hummock version. The pin belongs to `context_id`
378    /// and will be unpinned when `context_id` is invalidated.
379    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    /// Unpin all pins which belongs to `context_id` and has an id which is older than
416    /// `unpin_before`. All versions >= `unpin_before` will be treated as if they are all pinned by
417    /// this `context_id` so they will not be vacuumed.
418    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
457// safe point
458impl 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}