Skip to main content

sui_indexer_alt_reader/
consistent_reader.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::future::Future;
5use std::sync::Arc;
6use std::time::Duration;
7
8use anyhow::Context;
9use anyhow::anyhow;
10use prometheus::Registry;
11use sui_indexer_alt_consistent_api::proto::rpc::consistent::v1alpha::consistent_service_client::ConsistentServiceClient;
12use sui_types::base_types::ObjectDigest;
13use sui_types::base_types::ObjectID;
14use sui_types::base_types::ObjectRef;
15use sui_types::base_types::SequenceNumber;
16use tonic::transport::Channel;
17use tracing::instrument;
18use url::Url;
19
20pub use sui_indexer_alt_consistent_api::proto::rpc::consistent::v1alpha as proto;
21
22use crate::metrics::ConsistentReaderMetrics;
23
24#[derive(clap::Args, Debug, Clone, Default)]
25pub struct ConsistentReaderArgs {
26    /// URL of the consistent store gRPC service
27    #[arg(long)]
28    pub consistent_store_url: Option<Url>,
29
30    /// Time spent waiting for a request to complete, in milliseconds
31    #[arg(long)]
32    pub consistent_store_statement_timeout_ms: Option<u64>,
33}
34
35/// A reader backed by the consistent store gRPC service.
36#[derive(Clone)]
37pub struct ConsistentReader {
38    client: Option<Client>,
39    timeout: Option<Duration>,
40    metrics: Arc<ConsistentReaderMetrics>,
41}
42
43/// Response from a paginated query.
44pub struct Page<T> {
45    pub results: Vec<Edge<T>>,
46    pub has_previous_page: bool,
47    pub has_next_page: bool,
48}
49
50/// An individual element from a paginated query, includes the value and its token (pagination
51/// cursor).
52pub struct Edge<T> {
53    pub token: Vec<u8>,
54    pub value: T,
55}
56
57type Client = ConsistentServiceClient<Channel>;
58
59#[derive(thiserror::Error, Debug)]
60pub enum Error {
61    #[error(transparent)]
62    Internal(#[from] anyhow::Error),
63
64    #[error("{}", .0.message())]
65    OutOfRange(#[source] tonic::Status),
66
67    #[error("Consistent store client not configured")]
68    NotConfigured,
69}
70
71impl ConsistentReaderArgs {
72    pub fn statement_timeout(&self) -> Option<Duration> {
73        self.consistent_store_statement_timeout_ms
74            .map(Duration::from_millis)
75    }
76}
77
78impl ConsistentReader {
79    pub async fn new(
80        prefix: Option<&str>,
81        args: ConsistentReaderArgs,
82        registry: &Registry,
83    ) -> Result<Self, Error> {
84        let client = if let Some(url) = &args.consistent_store_url {
85            let mut endpoint = Channel::from_shared(url.to_string())
86                .context("Failed to create channel for gRPC endpoint")?;
87
88            if let Some(timeout) = args.statement_timeout() {
89                endpoint = endpoint.timeout(timeout);
90            }
91
92            let channel = endpoint.connect_lazy();
93
94            Some(ConsistentServiceClient::new(channel))
95        } else {
96            None
97        };
98
99        let timeout = args.statement_timeout();
100        let metrics = ConsistentReaderMetrics::new(prefix, registry);
101
102        Ok(Self {
103            client,
104            timeout,
105            metrics,
106        })
107    }
108
109    /// Get the consistent store's watermarks, as of the given `checkpoint`.
110    #[instrument(skip(self), level = "debug")]
111    pub async fn available_range(
112        &self,
113        checkpoint: u64,
114    ) -> Result<proto::AvailableRangeResponse, Error> {
115        self.request(
116            "available_range",
117            Some(checkpoint),
118            |mut client, request| async move { client.available_range(request).await },
119            proto::AvailableRangeRequest {},
120        )
121        .await
122    }
123
124    /// Batch get balances for multiple (owner, coin_type) pairs at a given checkpoint.
125    #[instrument(skip(self), level = "debug")]
126    pub async fn batch_get_balances(
127        &self,
128        checkpoint: u64,
129        requests: Vec<(String, String)>,
130    ) -> Result<Vec<proto::Balance>, Error> {
131        let response = self
132            .request(
133                "batch_get_balances",
134                Some(checkpoint),
135                |mut client, request| async move { client.batch_get_balances(request).await },
136                proto::BatchGetBalancesRequest {
137                    requests: requests
138                        .into_iter()
139                        .map(|(owner, coin_type)| proto::GetBalanceRequest {
140                            owner: Some(owner),
141                            coin_type: Some(coin_type),
142                        })
143                        .collect(),
144                },
145            )
146            .await?;
147
148        let mut results = vec![];
149        for balance in response.balances {
150            results.push(balance);
151        }
152
153        Ok(results)
154    }
155
156    /// Get the balance for a single (owner, coin_type) pair at a given checkpoint.
157    #[instrument(skip(self), level = "debug")]
158    pub async fn get_balance(
159        &self,
160        checkpoint: Option<u64>,
161        address: String,
162        coin_type: String,
163    ) -> Result<proto::Balance, Error> {
164        self.request(
165            "get_balance",
166            checkpoint,
167            |mut client, request| async move { client.get_balance(request).await },
168            proto::GetBalanceRequest {
169                owner: Some(address),
170                coin_type: Some(coin_type),
171            },
172        )
173        .await
174    }
175
176    /// Paginate coin balances for `address`, at checkpoint `checkpoint`.
177    #[instrument(skip(self), level = "debug")]
178    pub async fn list_balances(
179        &self,
180        checkpoint: Option<u64>,
181        address: String,
182        page_size: Option<u32>,
183        after_token: Option<Vec<u8>>,
184        before_token: Option<Vec<u8>>,
185        is_from_front: bool,
186    ) -> Result<Page<proto::Balance>, Error> {
187        let response = self
188            .request(
189                "list_balances",
190                checkpoint,
191                |mut client, request| async move { client.list_balances(request).await },
192                proto::ListBalancesRequest {
193                    owner: Some(address),
194                    page_size,
195                    after_token: after_token.map(Into::into),
196                    before_token: before_token.map(Into::into),
197                    end: if is_from_front {
198                        Some(proto::End::Front.into())
199                    } else {
200                        Some(proto::End::Back.into())
201                    },
202                },
203            )
204            .await?;
205
206        let has_next_page = response.has_next_page();
207        let has_previous_page = response.has_previous_page();
208
209        let results = response
210            .balances
211            .into_iter()
212            .map(|b| Edge {
213                token: b.page_token.clone().unwrap_or_default().into(),
214                value: b,
215            })
216            .collect();
217
218        Ok(Page {
219            results,
220            has_next_page,
221            has_previous_page,
222        })
223    }
224
225    /// Paginate live objects with type filter `object_type`, at checkpoint `checkpoint`.
226    #[instrument(skip(self), level = "debug")]
227    pub async fn list_objects_by_type(
228        &self,
229        checkpoint: Option<u64>,
230        object_type: String,
231        page_size: Option<u32>,
232        after_token: Option<Vec<u8>>,
233        before_token: Option<Vec<u8>>,
234        is_from_front: bool,
235    ) -> Result<Page<ObjectRef>, Error> {
236        let response = self
237            .request(
238                "list_objects_by_type",
239                checkpoint,
240                |mut client, request| async move { client.list_objects_by_type(request).await },
241                proto::ListObjectsByTypeRequest {
242                    object_type: Some(object_type),
243                    page_size,
244                    after_token: after_token.map(Into::into),
245                    before_token: before_token.map(Into::into),
246                    end: if is_from_front {
247                        Some(proto::End::Front.into())
248                    } else {
249                        Some(proto::End::Back.into())
250                    },
251                },
252            )
253            .await?;
254
255        let has_next_page = response.has_next_page();
256        let has_previous_page = response.has_previous_page();
257
258        let results = response
259            .objects
260            .into_iter()
261            .map(TryFrom::try_from)
262            .collect::<Result<Vec<_>, _>>()?;
263
264        Ok(Page {
265            results,
266            has_next_page,
267            has_previous_page,
268        })
269    }
270
271    /// Paginate live objects at `checkpoint`, with owner described by `kind` and `address`, and an
272    /// optional, `object_type`. If `object_type` is prefaced with `!`, objects of the type prefix
273    /// will be excluded from the page of results.
274    #[instrument(skip(self), level = "debug")]
275    pub async fn list_owned_objects(
276        &self,
277        checkpoint: Option<u64>,
278        kind: proto::owner::OwnerKind,
279        address: Option<String>,
280        object_type: Option<String>,
281        page_size: Option<u32>,
282        after_token: Option<Vec<u8>>,
283        before_token: Option<Vec<u8>>,
284        is_from_front: bool,
285    ) -> Result<Page<ObjectRef>, Error> {
286        let response = self
287            .request(
288                "list_owned_objects",
289                checkpoint,
290                |mut client, request| async move { client.list_owned_objects(request).await },
291                proto::ListOwnedObjectsRequest {
292                    owner: Some(proto::Owner {
293                        kind: Some(kind.into()),
294                        address,
295                    }),
296                    object_type,
297                    page_size,
298                    after_token: after_token.map(Into::into),
299                    before_token: before_token.map(Into::into),
300                    end: if is_from_front {
301                        Some(proto::End::Front.into())
302                    } else {
303                        Some(proto::End::Back.into())
304                    },
305                },
306            )
307            .await?;
308
309        let has_next_page = response.has_next_page();
310        let has_previous_page = response.has_previous_page();
311
312        let results = response
313            .objects
314            .into_iter()
315            .map(TryFrom::try_from)
316            .collect::<Result<Vec<_>, _>>()?;
317
318        Ok(Page {
319            results,
320            has_next_page,
321            has_previous_page,
322        })
323    }
324
325    async fn request<I, O, Fut, F>(
326        &self,
327        method: &str,
328        checkpoint: Option<u64>,
329        response: F,
330        input: I,
331    ) -> Result<O, Error>
332    where
333        F: FnOnce(Client, tonic::Request<I>) -> Fut,
334        Fut: Future<Output = Result<tonic::Response<O>, tonic::Status>>,
335    {
336        let Some(client) = self.client.clone() else {
337            return Err(Error::NotConfigured);
338        };
339
340        self.metrics
341            .requests_received
342            .with_label_values(&[method])
343            .inc();
344
345        let _timer = self
346            .metrics
347            .latency
348            .with_label_values(&[method])
349            .start_timer();
350
351        let mut request = tonic::Request::new(input);
352
353        if let Some(timeout) = self.timeout {
354            request.set_timeout(timeout);
355        }
356
357        if let Some(checkpoint) = checkpoint {
358            request.metadata_mut().insert(
359                proto::CHECKPOINT_HEIGHT_METADATA,
360                checkpoint
361                    .to_string()
362                    .parse()
363                    .with_context(|| format!("Invalid checkpoint {checkpoint}"))?,
364            );
365        }
366
367        let response = response(client, request)
368            .await
369            .map(|r| r.into_inner())
370            .map_err(Into::into);
371
372        if response.is_ok() {
373            self.metrics
374                .requests_succeeded
375                .with_label_values(&[method])
376                .inc();
377        } else {
378            self.metrics
379                .requests_failed
380                .with_label_values(&[method])
381                .inc()
382        }
383
384        response
385    }
386}
387
388impl TryFrom<proto::Object> for Edge<ObjectRef> {
389    type Error = Error;
390
391    fn try_from(proto: proto::Object) -> Result<Self, Error> {
392        let object_id: ObjectID = proto
393            .object_id
394            .context("object ID missing")?
395            .parse()
396            .context("invalid object ID")?;
397
398        let digest: ObjectDigest = proto
399            .digest
400            .context("digest missing")?
401            .parse()
402            .context("invalid digest")?;
403
404        let version: SequenceNumber = proto.version.context("version missing")?.into();
405        let token: Vec<u8> = proto.page_token.unwrap_or_default().into();
406
407        Ok(Edge {
408            token,
409            value: (object_id, version, digest),
410        })
411    }
412}
413
414impl From<tonic::Status> for Error {
415    fn from(status: tonic::Status) -> Self {
416        match status.code() {
417            tonic::Code::OutOfRange => Error::OutOfRange(status),
418            _ => Error::Internal(anyhow!(status.code()).context(status.message().to_string())),
419        }
420    }
421}