Skip to main content

iota_storage/
key_value_store.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5//! Immutable key/value store trait for storing/retrieving transactions,
6//! effects, and events to/from a scalable.
7
8use std::{sync::Arc, time::Instant};
9
10use async_trait::async_trait;
11pub use iota_node_storage::{
12    KVStoreCheckpointData, KVStoreTransactionData, TransactionKeyValueStoreTrait,
13};
14use iota_sdk_types::{
15    CheckpointContents, CheckpointDigest, ObjectId, TransactionDigest, TransactionEffects,
16    TransactionEvents, Version,
17};
18use iota_types::{
19    base_types::VersionNumber,
20    error::{IotaError, IotaResult, UserInputError},
21    messages_checkpoint::{CertifiedCheckpointSummary, CheckpointSequenceNumber},
22    object::Object,
23    storage::ObjectKey,
24    transaction::TransactionEnvelope,
25};
26use tracing::instrument;
27
28use crate::key_value_store_metrics::KeyValueStoreMetrics;
29
30pub struct TransactionKeyValueStore {
31    store_name: &'static str,
32    metrics: Arc<KeyValueStoreMetrics>,
33    inner: Arc<dyn TransactionKeyValueStoreTrait + Send + Sync>,
34}
35
36impl TransactionKeyValueStore {
37    pub fn new(
38        store_name: &'static str,
39        metrics: Arc<KeyValueStoreMetrics>,
40        inner: Arc<dyn TransactionKeyValueStoreTrait + Send + Sync>,
41    ) -> Self {
42        Self {
43            store_name,
44            metrics,
45            inner,
46        }
47    }
48
49    /// Generic multi_get, allows implementors to get heterogenous values with a
50    /// single round trip.
51    pub async fn multi_get(
52        &self,
53        transaction_keys: &[TransactionDigest],
54        effects_keys: &[TransactionDigest],
55    ) -> IotaResult<KVStoreTransactionData> {
56        let start = Instant::now();
57        let res = self.inner.multi_get(transaction_keys, effects_keys).await;
58        let elapsed = start.elapsed();
59
60        let num_txns = transaction_keys.len() as u64;
61        let num_effects = effects_keys.len() as u64;
62        let total_keys = num_txns + num_effects;
63
64        self.metrics
65            .key_value_store_num_fetches_latency_ms
66            .with_label_values(&[self.store_name, "tx"])
67            .observe(elapsed.as_millis() as f64);
68        self.metrics
69            .key_value_store_num_fetches_batch_size
70            .with_label_values(&[self.store_name, "tx"])
71            .observe(total_keys as f64);
72
73        if let Ok((transactions, effects)) = &res {
74            let txns_not_found = transactions.iter().filter(|v| v.is_none()).count() as u64;
75            let effects_not_found = effects.iter().filter(|v| v.is_none()).count() as u64;
76
77            if num_txns > 0 {
78                self.metrics
79                    .key_value_store_num_fetches_success
80                    .with_label_values(&[self.store_name, "tx"])
81                    .inc_by(num_txns);
82            }
83            if num_effects > 0 {
84                self.metrics
85                    .key_value_store_num_fetches_success
86                    .with_label_values(&[self.store_name, "fx"])
87                    .inc_by(num_effects);
88            }
89
90            if txns_not_found > 0 {
91                self.metrics
92                    .key_value_store_num_fetches_not_found
93                    .with_label_values(&[self.store_name, "tx"])
94                    .inc_by(txns_not_found);
95            }
96            if effects_not_found > 0 {
97                self.metrics
98                    .key_value_store_num_fetches_not_found
99                    .with_label_values(&[self.store_name, "fx"])
100                    .inc_by(effects_not_found);
101            }
102        } else {
103            self.metrics
104                .key_value_store_num_fetches_error
105                .with_label_values(&[self.store_name, "tx"])
106                .inc_by(num_txns);
107            self.metrics
108                .key_value_store_num_fetches_error
109                .with_label_values(&[self.store_name, "fx"])
110                .inc_by(num_effects);
111        }
112
113        res
114    }
115
116    pub async fn multi_get_checkpoints(
117        &self,
118        checkpoint_summaries: &[CheckpointSequenceNumber],
119        checkpoint_contents: &[CheckpointSequenceNumber],
120        checkpoint_summaries_by_digest: &[CheckpointDigest],
121    ) -> IotaResult<(
122        Vec<Option<CertifiedCheckpointSummary>>,
123        Vec<Option<CheckpointContents>>,
124        Vec<Option<CertifiedCheckpointSummary>>,
125    )> {
126        let start = Instant::now();
127        let res = self
128            .inner
129            .multi_get_checkpoints(
130                checkpoint_summaries,
131                checkpoint_contents,
132                checkpoint_summaries_by_digest,
133            )
134            .await;
135        let elapsed = start.elapsed();
136
137        let num_summaries =
138            checkpoint_summaries.len() as u64 + checkpoint_summaries_by_digest.len() as u64;
139        let num_contents = checkpoint_contents.len() as u64;
140
141        self.metrics
142            .key_value_store_num_fetches_latency_ms
143            .with_label_values(&[self.store_name, "checkpoint"])
144            .observe(elapsed.as_millis() as f64);
145        self.metrics
146            .key_value_store_num_fetches_batch_size
147            .with_label_values(&[self.store_name, "checkpoint_summary"])
148            .observe(num_summaries as f64);
149        self.metrics
150            .key_value_store_num_fetches_batch_size
151            .with_label_values(&[self.store_name, "checkpoint_content"])
152            .observe(num_contents as f64);
153
154        if let Ok((summaries, contents, summaries_by_digest)) = &res {
155            let summaries_not_found = summaries.iter().filter(|v| v.is_none()).count() as u64
156                + summaries_by_digest.iter().filter(|v| v.is_none()).count() as u64;
157            let contents_not_found = contents.iter().filter(|v| v.is_none()).count() as u64;
158
159            if num_summaries > 0 {
160                self.metrics
161                    .key_value_store_num_fetches_success
162                    .with_label_values(&[self.store_name, "ckpt_summary"])
163                    .inc_by(num_summaries);
164            }
165            if num_contents > 0 {
166                self.metrics
167                    .key_value_store_num_fetches_success
168                    .with_label_values(&[self.store_name, "ckpt_contents"])
169                    .inc_by(num_contents);
170            }
171
172            if summaries_not_found > 0 {
173                self.metrics
174                    .key_value_store_num_fetches_not_found
175                    .with_label_values(&[self.store_name, "ckpt_summary"])
176                    .inc_by(summaries_not_found);
177            }
178            if contents_not_found > 0 {
179                self.metrics
180                    .key_value_store_num_fetches_not_found
181                    .with_label_values(&[self.store_name, "ckpt_contents"])
182                    .inc_by(contents_not_found);
183            }
184        } else {
185            self.metrics
186                .key_value_store_num_fetches_error
187                .with_label_values(&[self.store_name, "ckpt_summary"])
188                .inc_by(num_summaries);
189            self.metrics
190                .key_value_store_num_fetches_error
191                .with_label_values(&[self.store_name, "ckpt_contents"])
192                .inc_by(num_contents);
193        }
194
195        res
196    }
197
198    pub async fn multi_get_checkpoints_summaries(
199        &self,
200        keys: &[CheckpointSequenceNumber],
201    ) -> IotaResult<Vec<Option<CertifiedCheckpointSummary>>> {
202        self.multi_get_checkpoints(keys, &[], &[])
203            .await
204            .map(|(summaries, _, _)| summaries)
205    }
206
207    pub async fn multi_get_checkpoints_contents(
208        &self,
209        keys: &[CheckpointSequenceNumber],
210    ) -> IotaResult<Vec<Option<CheckpointContents>>> {
211        self.multi_get_checkpoints(&[], keys, &[])
212            .await
213            .map(|(_, contents, _)| contents)
214    }
215
216    pub async fn multi_get_checkpoints_summaries_by_digest(
217        &self,
218        keys: &[CheckpointDigest],
219    ) -> IotaResult<Vec<Option<CertifiedCheckpointSummary>>> {
220        self.multi_get_checkpoints(&[], &[], keys)
221            .await
222            .map(|(_, _, summaries)| summaries)
223    }
224
225    pub async fn multi_get_tx(
226        &self,
227        keys: &[TransactionDigest],
228    ) -> IotaResult<Vec<Option<TransactionEnvelope>>> {
229        self.multi_get(keys, &[]).await.map(|(txns, _)| txns)
230    }
231
232    pub async fn multi_get_fx_by_tx_digest(
233        &self,
234        keys: &[TransactionDigest],
235    ) -> IotaResult<Vec<Option<TransactionEffects>>> {
236        self.multi_get(&[], keys).await.map(|(_, fx)| fx)
237    }
238
239    /// Convenience method for fetching single digest, and returning an error if
240    /// it's not found. Prefer using multi_get_tx whenever possible.
241    pub async fn get_tx(&self, digest: TransactionDigest) -> IotaResult<TransactionEnvelope> {
242        self.multi_get_tx(&[digest])
243            .await?
244            .into_iter()
245            .next()
246            .flatten()
247            .ok_or(IotaError::TransactionNotFound { digest })
248    }
249
250    /// Convenience method for fetching single digest, and returning an error if
251    /// it's not found. Prefer using multi_get_fx_by_tx_digest whenever
252    /// possible.
253    pub async fn get_fx_by_tx_digest(
254        &self,
255        digest: TransactionDigest,
256    ) -> IotaResult<TransactionEffects> {
257        self.multi_get_fx_by_tx_digest(&[digest])
258            .await?
259            .into_iter()
260            .next()
261            .flatten()
262            .ok_or(IotaError::TransactionNotFound { digest })
263    }
264
265    /// Convenience method for fetching single checkpoint, and returning an
266    /// error if it's not found. Prefer using
267    /// multi_get_checkpoints_summaries whenever possible.
268    pub async fn get_checkpoint_summary(
269        &self,
270        checkpoint: CheckpointSequenceNumber,
271    ) -> IotaResult<CertifiedCheckpointSummary> {
272        self.multi_get_checkpoints_summaries(&[checkpoint])
273            .await?
274            .into_iter()
275            .next()
276            .flatten()
277            .ok_or(IotaError::UserInput {
278                error: UserInputError::VerifiedCheckpointNotFound(checkpoint),
279            })
280    }
281
282    /// Convenience method for fetching single checkpoint, and returning an
283    /// error if it's not found. Prefer using multi_get_checkpoints_contents
284    /// whenever possible.
285    pub async fn get_checkpoint_contents(
286        &self,
287        checkpoint: CheckpointSequenceNumber,
288    ) -> IotaResult<CheckpointContents> {
289        self.multi_get_checkpoints_contents(&[checkpoint])
290            .await?
291            .into_iter()
292            .next()
293            .flatten()
294            .ok_or(IotaError::UserInput {
295                error: UserInputError::VerifiedCheckpointNotFound(checkpoint),
296            })
297    }
298
299    /// Convenience method for fetching single checkpoint, and returning an
300    /// error if it's not found. Prefer using
301    /// multi_get_checkpoints_summaries_by_digest whenever possible.
302    pub async fn get_checkpoint_summary_by_digest(
303        &self,
304        digest: CheckpointDigest,
305    ) -> IotaResult<CertifiedCheckpointSummary> {
306        self.multi_get_checkpoints_summaries_by_digest(&[digest])
307            .await?
308            .into_iter()
309            .next()
310            .flatten()
311            .ok_or(IotaError::UserInput {
312                error: UserInputError::VerifiedCheckpointDigestNotFound(format!("{digest}")),
313            })
314    }
315
316    pub async fn get_transaction_perpetual_checkpoint(
317        &self,
318        digest: TransactionDigest,
319    ) -> IotaResult<Option<CheckpointSequenceNumber>> {
320        self.inner
321            .get_transaction_perpetual_checkpoint(digest)
322            .await
323    }
324
325    pub async fn get_object(
326        &self,
327        object_id: ObjectId,
328        version: VersionNumber,
329    ) -> IotaResult<Option<Object>> {
330        self.inner.get_object(object_id, version).await
331    }
332
333    pub async fn multi_get_objects(
334        &self,
335        object_keys: &[ObjectKey],
336    ) -> IotaResult<Vec<Option<Object>>> {
337        self.inner.multi_get_objects(object_keys).await
338    }
339
340    pub async fn multi_get_transactions_perpetual_checkpoints(
341        &self,
342        digests: &[TransactionDigest],
343    ) -> IotaResult<Vec<Option<CheckpointSequenceNumber>>> {
344        self.inner
345            .multi_get_transactions_perpetual_checkpoints(digests)
346            .await
347    }
348
349    pub async fn multi_get_events_by_tx_digests(
350        &self,
351        digests: &[TransactionDigest],
352    ) -> IotaResult<Vec<Option<TransactionEvents>>> {
353        self.inner.multi_get_events_by_tx_digests(digests).await
354    }
355}
356
357/// `multi_get` and `multi_get_checkpoints` record the same metrics as the
358/// inherent methods; the other methods go straight to the inner store.
359#[async_trait]
360impl TransactionKeyValueStoreTrait for TransactionKeyValueStore {
361    async fn multi_get(
362        &self,
363        transaction_keys: &[TransactionDigest],
364        effects_keys: &[TransactionDigest],
365    ) -> IotaResult<KVStoreTransactionData> {
366        Self::multi_get(self, transaction_keys, effects_keys).await
367    }
368
369    async fn multi_get_checkpoints(
370        &self,
371        checkpoint_summaries: &[CheckpointSequenceNumber],
372        checkpoint_contents: &[CheckpointSequenceNumber],
373        checkpoint_summaries_by_digest: &[CheckpointDigest],
374    ) -> IotaResult<KVStoreCheckpointData> {
375        Self::multi_get_checkpoints(
376            self,
377            checkpoint_summaries,
378            checkpoint_contents,
379            checkpoint_summaries_by_digest,
380        )
381        .await
382    }
383
384    async fn get_transaction_perpetual_checkpoint(
385        &self,
386        digest: TransactionDigest,
387    ) -> IotaResult<Option<CheckpointSequenceNumber>> {
388        self.inner
389            .get_transaction_perpetual_checkpoint(digest)
390            .await
391    }
392
393    async fn get_object(
394        &self,
395        object_id: ObjectId,
396        version: Version,
397    ) -> IotaResult<Option<Object>> {
398        self.inner.get_object(object_id, version).await
399    }
400
401    async fn multi_get_objects(
402        &self,
403        object_keys: &[ObjectKey],
404    ) -> IotaResult<Vec<Option<Object>>> {
405        self.inner.multi_get_objects(object_keys).await
406    }
407
408    async fn multi_get_transactions_perpetual_checkpoints(
409        &self,
410        digests: &[TransactionDigest],
411    ) -> IotaResult<Vec<Option<CheckpointSequenceNumber>>> {
412        self.inner
413            .multi_get_transactions_perpetual_checkpoints(digests)
414            .await
415    }
416
417    async fn multi_get_events_by_tx_digests(
418        &self,
419        digests: &[TransactionDigest],
420    ) -> IotaResult<Vec<Option<TransactionEvents>>> {
421        self.inner.multi_get_events_by_tx_digests(digests).await
422    }
423    async fn evict_objects(&self, object_keys: &[ObjectKey]) {
424        self.inner.evict_objects(object_keys).await
425    }
426
427    async fn evict_events_by_tx_digests(&self, digests: &[TransactionDigest]) {
428        self.inner.evict_events_by_tx_digests(digests).await
429    }
430}
431
432/// A TransactionKeyValueStoreTrait that falls back to a secondary store for any
433/// key for which the primary store returns None.
434///
435/// Will be used to check the local rocksdb store, before falling back to a
436/// remote scalable store.
437pub struct FallbackTransactionKVStore {
438    primary: TransactionKeyValueStore,
439    fallback: TransactionKeyValueStore,
440}
441
442impl FallbackTransactionKVStore {
443    pub fn new_kv(
444        primary: TransactionKeyValueStore,
445        fallback: TransactionKeyValueStore,
446        metrics: Arc<KeyValueStoreMetrics>,
447        label: &'static str,
448    ) -> TransactionKeyValueStore {
449        let store = Arc::new(Self { primary, fallback });
450        TransactionKeyValueStore::new(label, metrics, store)
451    }
452}
453
454#[async_trait]
455impl TransactionKeyValueStoreTrait for FallbackTransactionKVStore {
456    #[instrument(level = "trace", skip_all)]
457    async fn multi_get(
458        &self,
459        transaction_keys: &[TransactionDigest],
460        effects_keys: &[TransactionDigest],
461    ) -> IotaResult<KVStoreTransactionData> {
462        let (mut transactions, mut effects) = self
463            .primary
464            .multi_get(transaction_keys, effects_keys)
465            .await?;
466
467        let (fallback_transaction_keys, indices_transactions) =
468            find_fallback(&transactions, transaction_keys);
469        let (fallback_effects_keys, indices_effects) = find_fallback(&effects, effects_keys);
470
471        if fallback_transaction_keys.is_empty() && fallback_effects_keys.is_empty() {
472            return Ok((transactions, effects));
473        }
474
475        let (fallback_transactions, fallback_effects) = self
476            .fallback
477            .multi_get(&fallback_transaction_keys, &fallback_effects_keys)
478            .await?;
479
480        merge_res(
481            &mut transactions,
482            fallback_transactions,
483            &indices_transactions,
484        );
485        merge_res(&mut effects, fallback_effects, &indices_effects);
486
487        Ok((transactions, effects))
488    }
489
490    #[instrument(level = "trace", skip_all)]
491    async fn multi_get_checkpoints(
492        &self,
493        checkpoint_summaries: &[CheckpointSequenceNumber],
494        checkpoint_contents: &[CheckpointSequenceNumber],
495        checkpoint_summaries_by_digest: &[CheckpointDigest],
496    ) -> IotaResult<(
497        Vec<Option<CertifiedCheckpointSummary>>,
498        Vec<Option<CheckpointContents>>,
499        Vec<Option<CertifiedCheckpointSummary>>,
500    )> {
501        let (mut summaries, mut contents, mut summaries_by_digest) = self
502            .primary
503            .multi_get_checkpoints(
504                checkpoint_summaries,
505                checkpoint_contents,
506                checkpoint_summaries_by_digest,
507            )
508            .await?;
509
510        let (fallback_summaries, indices_summaries) =
511            find_fallback(&summaries, checkpoint_summaries);
512        let (fallback_contents, indices_contents) = find_fallback(&contents, checkpoint_contents);
513        let (fallback_summaries_by_digest, indices_summaries_by_digest) =
514            find_fallback(&summaries_by_digest, checkpoint_summaries_by_digest);
515
516        if fallback_summaries.is_empty()
517            && fallback_contents.is_empty()
518            && fallback_summaries_by_digest.is_empty()
519        {
520            return Ok((summaries, contents, summaries_by_digest));
521        }
522
523        let (fallback_summaries, fallback_contents, fallback_summaries_by_digest) = self
524            .fallback
525            .multi_get_checkpoints(
526                &fallback_summaries,
527                &fallback_contents,
528                &fallback_summaries_by_digest,
529            )
530            .await?;
531
532        merge_res(&mut summaries, fallback_summaries, &indices_summaries);
533        merge_res(&mut contents, fallback_contents, &indices_contents);
534        merge_res(
535            &mut summaries_by_digest,
536            fallback_summaries_by_digest,
537            &indices_summaries_by_digest,
538        );
539
540        Ok((summaries, contents, summaries_by_digest))
541    }
542
543    #[instrument(level = "trace", skip_all)]
544    async fn get_transaction_perpetual_checkpoint(
545        &self,
546        digest: TransactionDigest,
547    ) -> IotaResult<Option<CheckpointSequenceNumber>> {
548        let mut res = self
549            .primary
550            .get_transaction_perpetual_checkpoint(digest)
551            .await?;
552        if res.is_none() {
553            res = self
554                .fallback
555                .get_transaction_perpetual_checkpoint(digest)
556                .await?;
557        }
558        Ok(res)
559    }
560
561    #[instrument(level = "trace", skip_all)]
562    async fn get_object(
563        &self,
564        object_id: ObjectId,
565        version: Version,
566    ) -> IotaResult<Option<Object>> {
567        let mut res = self.primary.get_object(object_id, version).await?;
568        if res.is_none() {
569            res = self.fallback.get_object(object_id, version).await?;
570        }
571        Ok(res)
572    }
573
574    #[instrument(level = "trace", skip_all)]
575    async fn multi_get_objects(
576        &self,
577        object_keys: &[ObjectKey],
578    ) -> IotaResult<Vec<Option<Object>>> {
579        let mut res = self.primary.multi_get_objects(object_keys).await?;
580
581        let (fallback, indices) = find_fallback(&res, object_keys);
582
583        if fallback.is_empty() {
584            return Ok(res);
585        }
586
587        let secondary_res = self.fallback.multi_get_objects(&fallback).await?;
588
589        merge_res(&mut res, secondary_res, &indices);
590
591        Ok(res)
592    }
593
594    #[instrument(level = "trace", skip_all)]
595    async fn multi_get_transactions_perpetual_checkpoints(
596        &self,
597        digests: &[TransactionDigest],
598    ) -> IotaResult<Vec<Option<CheckpointSequenceNumber>>> {
599        let mut res = self
600            .primary
601            .multi_get_transactions_perpetual_checkpoints(digests)
602            .await?;
603
604        let (fallback, indices) = find_fallback(&res, digests);
605
606        if fallback.is_empty() {
607            return Ok(res);
608        }
609
610        let secondary_res = self
611            .fallback
612            .multi_get_transactions_perpetual_checkpoints(&fallback)
613            .await?;
614
615        merge_res(&mut res, secondary_res, &indices);
616
617        Ok(res)
618    }
619
620    #[instrument(level = "trace", skip_all)]
621    async fn multi_get_events_by_tx_digests(
622        &self,
623        digests: &[TransactionDigest],
624    ) -> IotaResult<Vec<Option<TransactionEvents>>> {
625        let mut res = self.primary.multi_get_events_by_tx_digests(digests).await?;
626        let (fallback, indices) = find_fallback(&res, digests);
627        if fallback.is_empty() {
628            return Ok(res);
629        }
630        let secondary_res = self
631            .fallback
632            .multi_get_events_by_tx_digests(&fallback)
633            .await?;
634        merge_res(&mut res, secondary_res, &indices);
635        Ok(res)
636    }
637
638    async fn evict_objects(&self, object_keys: &[ObjectKey]) {
639        self.primary.evict_objects(object_keys).await;
640        self.fallback.evict_objects(object_keys).await;
641    }
642
643    async fn evict_events_by_tx_digests(&self, digests: &[TransactionDigest]) {
644        self.primary.evict_events_by_tx_digests(digests).await;
645        self.fallback.evict_events_by_tx_digests(digests).await;
646    }
647}
648
649fn find_fallback<T, K: Clone>(values: &[Option<T>], keys: &[K]) -> (Vec<K>, Vec<usize>) {
650    let num_nones = values.iter().filter(|v| v.is_none()).count();
651    let mut fallback_keys = Vec::with_capacity(num_nones);
652    let mut fallback_indices = Vec::with_capacity(num_nones);
653    for (i, value) in values.iter().enumerate() {
654        if value.is_none() {
655            fallback_keys.push(keys[i].clone());
656            fallback_indices.push(i);
657        }
658    }
659    (fallback_keys, fallback_indices)
660}
661
662fn merge_res<T>(values: &mut [Option<T>], fallback_values: Vec<Option<T>>, indices: &[usize]) {
663    for (&index, fallback_value) in indices.iter().zip(fallback_values) {
664        values[index] = fallback_value;
665    }
666}