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.
44/// It also keeps changelog references removed during its lifetime in a separate tracker.
45pub 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    /// `version_safe_points` is similar to `pinned_versions` expect for being a transient state.
67    pub version_safe_points: Vec<HummockVersionId>,
68}
69
70impl ContextInfo {
71    /// Release resources pinned by these contexts, including:
72    /// - Version
73    /// - Snapshot
74    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    /// Checks whether `context_id` is valid.
117    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            // Using the preserved meta id is allowed.
133        } else if !context_info
134            .check_context(context_id, &self.metadata_manager)
135            .await
136        {
137            // The worker is not found in cluster.
138            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    /// Checks whether `context_id` is valid.
154    ///
155    /// Need `&self` to sync with `release_context`
156    pub(super) async fn check_context(
157        &self,
158        context_id: HummockContextId,
159        metadata_manager: &MetadataManager,
160    ) -> bool {
161        // Worker existence is kept in memory and updated under the cluster controller lock, so
162        // checking it does not need a meta store query.
163        metadata_manager
164            .get_worker_info_by_id(context_id)
165            .await
166            .is_some()
167    }
168}
169
170impl HummockManager {
171    /// Release invalid contexts, aka worker node ids which are no longer valid in `ClusterManager`.
172    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        // sanity check on monotonically increasing table committed epoch
244        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        // HummockManager::now requires a write to the meta store. Thus, it should be avoided whenever feasible.
259        if !sstables.is_empty() {
260            // Sanity check to ensure SSTs to commit have not been full GCed yet.
261            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        // HummockManager::now requires a write to the meta store. Thus, it should be avoided whenever feasible.
322        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
377// pin and unpin method
378impl HummockManager {
379    /// Pin the current greatest hummock version. The pin belongs to `context_id`
380    /// and will be unpinned when `context_id` is invalidated.
381    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    /// Unpin all pins which belongs to `context_id` and has an id which is older than
418    /// `unpin_before`. All versions >= `unpin_before` will be treated as if they are all pinned by
419    /// this `context_id` so they will not be vacuumed.
420    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
459// safe point
460impl 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}