Skip to main content

sui_rpc_store/reader/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Adapter that exposes [`RpcStoreSchema`] through the trait stack
5//! `sui-rpc-api` consumes — [`ObjectStore`], [`ReadStore`],
6//! [`RuntimeObjectResolver`], [`RpcStateReader`], and [`RpcIndexes`].
7//!
8//! The adapter type, [`RpcStoreReader`], is generic over a
9//! [`Reader`] so a single struct serves both tip reads (`R = Db`)
10//! and point-in-time reads bound to a snapshot (`R = Snapshot`).
11//! Callers requesting "give me the latest" hold the tip reader;
12//! callers requesting "show me state at checkpoint X" hold the
13//! snapshot-bound one. The choice of consistency context is made
14//! once, at the point [`RpcStoreReader::at_snapshot`] is called.
15//!
16//! [`ObjectStore`]: sui_types::storage::ObjectStore
17//! [`ReadStore`]: sui_types::storage::ReadStore
18//! [`RuntimeObjectResolver`]: sui_types::storage::RuntimeObjectResolver
19//! [`RpcStateReader`]: sui_types::storage::RpcStateReader
20//! [`RpcIndexes`]: sui_types::storage::RpcIndexes
21//! [`Reader`]: sui_consistent_store::reader::Reader
22
23mod child_resolver;
24mod indexes;
25#[cfg(test)]
26mod integration_test;
27mod layout;
28mod object_store;
29mod read_store;
30mod state_reader;
31
32use std::sync::Arc;
33
34use sui_consistent_store::Db;
35use sui_consistent_store::SchemaAtSnapshot;
36use sui_consistent_store::Snapshot;
37use sui_consistent_store::reader::Reader;
38
39use crate::RpcStoreSchema;
40
41/// Adapter exposing [`RpcStoreSchema`] through the
42/// `sui-rpc-api` reader-trait stack.
43///
44/// Construct one of two ways:
45///
46/// - [`RpcStoreReader::new`] binds to tip reads (`R = Db`). Use
47///   this for callers that want the latest committed state.
48/// - [`RpcStoreReader::at_snapshot`] takes a captured
49///   [`Snapshot`] and returns a [`RpcStoreReader<Snapshot>`] whose
50///   every read returns the state at that snapshot. Use this for
51///   "show me state at checkpoint X" requests.
52///
53/// `RpcStoreReader` holds an `Arc<RpcStoreSchema<R>>` so trait
54/// impls can hand a `&self` to any of the inherent read helpers
55/// already defined on the schema. The wrapper itself is `Clone`
56/// (cheap, `Arc`-backed) so it can be handed to
57/// `sui-rpc-api::StateReader::new(Arc::new(reader))`.
58pub struct RpcStoreReader<R: Reader = Db> {
59    /// The `Db` handle. Held separately from `schema` so trait
60    /// impls that need framework-level access (chain id, pipeline
61    /// watermarks) don't have to walk through a typed CF.
62    db: Db,
63
64    /// Typed handles to every CF the read paths exercise.
65    schema: Arc<RpcStoreSchema<R>>,
66
67    /// The pipelines registered on this deployment, whose watermarks
68    /// bound the reported indexed tip
69    /// (`get_highest_indexed_checkpoint_seq_number`). Kept explicit
70    /// rather than derived from the watermark CF's rows so a stale
71    /// row left behind by a pipeline that is no longer registered
72    /// cannot pin the reported tip, and so a registered pipeline
73    /// that has not committed yet correctly reads as "nothing fully
74    /// indexed".
75    pipelines: Arc<[&'static str]>,
76}
77
78impl<R: Reader> RpcStoreReader<R> {
79    /// Bind the adapter to an existing [`RpcStoreSchema`].
80    ///
81    /// `db` must be the same [`Db`] the schema was opened against.
82    /// Holding both separately is cheap (each is `Arc`-backed) and
83    /// avoids a `schema.epochs.reader().db()` style detour inside
84    /// hot read paths.
85    ///
86    /// The registered pipeline set defaults to the embedded
87    /// fullnode's cohorts ([`LIVE_COHORT`] + [`HISTORY_COHORT`]) —
88    /// the only in-tree deployment. A deployment that registers a
89    /// different set (e.g. a standalone node running the raw
90    /// chain-data pipelines) must override it via
91    /// [`Self::with_pipelines`] so the indexed-tip bound covers
92    /// exactly what it serves.
93    ///
94    /// [`LIVE_COHORT`]: crate::LIVE_COHORT
95    /// [`HISTORY_COHORT`]: crate::HISTORY_COHORT
96    pub fn new(db: Db, schema: Arc<RpcStoreSchema<R>>) -> Self {
97        let pipelines = crate::LIVE_COHORT
98            .iter()
99            .chain(crate::HISTORY_COHORT)
100            .copied()
101            .collect();
102        Self {
103            db,
104            schema,
105            pipelines,
106        }
107    }
108
109    /// Override the registered pipeline set whose watermarks bound
110    /// the reported indexed tip. See [`Self::new`] for the default.
111    pub fn with_pipelines(mut self, pipelines: impl IntoIterator<Item = &'static str>) -> Self {
112        self.pipelines = pipelines.into_iter().collect();
113        self
114    }
115
116    /// Borrow the underlying [`Db`] handle. Used by trait impls
117    /// that read directly from the framework CFs (chain id,
118    /// pipeline watermarks) rather than going through the typed
119    /// user schema.
120    pub fn db(&self) -> &Db {
121        &self.db
122    }
123
124    /// Borrow the typed schema this adapter is bound to. Trait
125    /// impls reach through this to call any of the inherent read
126    /// helpers `RpcStoreSchema` exposes per-CF.
127    pub fn schema(&self) -> &RpcStoreSchema<R> {
128        &self.schema
129    }
130
131    /// The highest checkpoint the live-object cohort (owned objects, types,
132    /// balances) has committed -- `min(checkpoint_hi_inclusive)` across its
133    /// pipelines, or `None` if any has no watermark yet.
134    ///
135    /// The embedded indexer follows the tip asynchronously, so this lags the
136    /// executed tip and bounds the checkpoint at which the live-object index
137    /// surface is readable. The history cohort backfills independently and is
138    /// deliberately excluded here -- its availability is exposed separately.
139    pub fn highest_live_committed_checkpoint(
140        &self,
141    ) -> sui_types::storage::error::Result<
142        Option<sui_types::messages_checkpoint::CheckpointSequenceNumber>,
143    > {
144        self.min_committed(crate::LIVE_COHORT.iter().copied())
145    }
146
147    /// The highest checkpoint every pipeline in `pipelines` has
148    /// committed (`min(checkpoint_hi_inclusive)`), or `None` if any of
149    /// them has no watermark yet. Private, but reachable from the
150    /// sibling trait-impl modules (children of this module).
151    fn min_committed(
152        &self,
153        pipelines: impl IntoIterator<Item = &'static str>,
154    ) -> sui_types::storage::error::Result<
155        Option<sui_types::messages_checkpoint::CheckpointSequenceNumber>,
156    > {
157        let framework = self.db().framework();
158        let mut min_hi: Option<u64> = None;
159        for name in pipelines {
160            let key = sui_consistent_store::PipelineTaskKey::new(name);
161            let Some(watermark) = framework
162                .watermarks
163                .get(&key)
164                .map_err(sui_types::storage::error::Error::custom)?
165            else {
166                return Ok(None);
167            };
168            let hi = watermark.checkpoint_hi_inclusive;
169            min_hi = Some(min_hi.map_or(hi, |m| m.min(hi)));
170        }
171        Ok(min_hi)
172    }
173}
174
175impl RpcStoreReader<Db> {
176    /// Re-project this reader against a captured [`Snapshot`].
177    ///
178    /// The returned [`RpcStoreReader<Snapshot>`] reads every CF
179    /// through the snapshot's `ReadOptions`, so reads are
180    /// consistent with the point in time at which the snapshot
181    /// was taken (rather than the tip). The original tip reader
182    /// is unaffected.
183    pub fn at_snapshot(&self, snap: &Snapshot) -> RpcStoreReader<Snapshot> {
184        RpcStoreReader {
185            db: self.db.clone(),
186            schema: Arc::new(self.schema.at(snap)),
187            pipelines: self.pipelines.clone(),
188        }
189    }
190}
191
192impl<R: Reader> Clone for RpcStoreReader<R> {
193    fn clone(&self) -> Self {
194        Self {
195            db: self.db.clone(),
196            schema: self.schema.clone(),
197            pipelines: self.pipelines.clone(),
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use sui_consistent_store::DbOptions;
205
206    use super::*;
207
208    #[test]
209    fn new_binds_db_and_schema() {
210        let dir = tempfile::tempdir().unwrap();
211        let (db, schema) = Db::open::<RpcStoreSchema>(dir.path(), DbOptions::default()).unwrap();
212        let reader = RpcStoreReader::new(db.clone(), Arc::new(schema));
213        // Smoke check: both handles are reachable and cloneable.
214        let _ = reader.clone();
215        assert!(reader.schema().get_pruning_watermarks().unwrap().is_none());
216    }
217
218    #[test]
219    fn at_snapshot_returns_snapshot_bound_reader() {
220        let dir = tempfile::tempdir().unwrap();
221        let (db, schema) = Db::open::<RpcStoreSchema>(dir.path(), DbOptions::default()).unwrap();
222        let reader = RpcStoreReader::new(db.clone(), Arc::new(schema));
223
224        db.take_snapshot(sui_consistent_store::Watermark::for_checkpoint(0));
225        let snap = db.at_snapshot(0).expect("snapshot retained");
226        let snap_reader = reader.at_snapshot(&snap);
227        // Both readers see the same (empty) state on a fresh DB.
228        assert!(reader.schema().get_pruning_watermarks().unwrap().is_none());
229        assert!(
230            snap_reader
231                .schema()
232                .get_pruning_watermarks()
233                .unwrap()
234                .is_none()
235        );
236    }
237}