Skip to main content

linera_views/backends/
journaling.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Turns a `DirectKeyValueStore` into a `KeyValueStore` by adding journaling.
5//!
6//! Journaling aims to allow writing arbitrarily large batches of data in an atomic way.
7//! This is useful for database backends that limit the number of keys and/or the size of
8//! the data that can be written atomically (i.e. in the same database transaction).
9//!
10//! Journaling requires to set aside a range of keys to hold a possible "header" and an
11//! array of unwritten entries called "blocks".
12//!
13//! When a new batch to be written exceeds the capacity of the underlying storage, the
14//! "slow path" is taken: the batch of operations is first written into blocks, then the
15//! journal header is (atomically) updated to make the batch of updates persistent.
16//!
17//! Before any new read or write operation, if a journal is present, it must first be
18//! cleared. This is done by processing every block of the journal successively. Every
19//! time the data in a block are written, the journal header is updated in the same
20//! transaction to mark the block as processed.
21
22use serde::{Deserialize, Serialize};
23use static_assertions as sa;
24use thiserror::Error;
25
26#[cfg(with_metrics)]
27mod metrics {
28    use std::sync::LazyLock;
29
30    use linera_base::prometheus_util::{
31        exponential_bucket_interval, register_histogram, register_int_counter,
32    };
33    use prometheus::{Histogram, IntCounter};
34
35    /// Number of write_batch calls that used the fast path (single atomic batch).
36    pub static JOURNAL_FASTPATH_COUNT: LazyLock<IntCounter> = LazyLock::new(|| {
37        register_int_counter(
38            "journal_fastpath_count",
39            "Number of write_batch calls using the fast path",
40        )
41    });
42
43    /// Number of write_batch calls that required journaling.
44    pub static JOURNAL_SLOWPATH_COUNT: LazyLock<IntCounter> = LazyLock::new(|| {
45        register_int_counter(
46            "journal_slowpath_count",
47            "Number of write_batch calls requiring journaling",
48        )
49    });
50
51    /// Number of journal resolution failures.
52    pub static JOURNAL_RESOLUTION_FAILURES: LazyLock<IntCounter> = LazyLock::new(|| {
53        register_int_counter(
54            "journal_resolution_failures",
55            "Number of journal resolution failures (potential data inconsistency)",
56        )
57    });
58
59    /// Number of pending journals found during `clear_journal` (on chain reload).
60    pub static JOURNAL_PENDING_ON_LOAD: LazyLock<IntCounter> = LazyLock::new(|| {
61        register_int_counter(
62            "journal_pending_on_load",
63            "Number of pending journals found during chain reload",
64        )
65    });
66
67    /// Histogram of batch sizes (number of operations) for write_batch calls.
68    pub static JOURNAL_BATCH_LEN: LazyLock<Histogram> = LazyLock::new(|| {
69        register_histogram(
70            "journal_batch_len",
71            "Number of operations in write_batch calls",
72            exponential_bucket_interval(1.0, 10000.0),
73        )
74    });
75}
76
77use crate::{
78    batch::{Batch, BatchValueWriter, DeletePrefixExpander, SimplifiedBatch},
79    store::{
80        DirectKeyValueStore, KeyValueDatabase, KeyValueStoreError, ReadableKeyValueStore,
81        WithError, WritableKeyValueStore,
82    },
83    views::MIN_VIEW_TAG,
84};
85
86/// A journaling key-value database.
87#[derive(Clone)]
88pub struct JournalingKeyValueDatabase<D> {
89    database: D,
90}
91
92/// A journaling key-value store.
93#[derive(Clone)]
94pub struct JournalingKeyValueStore<S> {
95    /// The inner store.
96    store: S,
97    /// Whether we have exclusive R/W access to the keys under root key.
98    has_exclusive_access: bool,
99}
100
101/// Error type for the journaling key-value store layer.
102#[derive(Error, Debug)]
103pub enum JournalingError<E> {
104    /// Error from the inner store.
105    #[error(transparent)]
106    Inner(#[from] E),
107
108    /// BCS serialization error.
109    #[error(transparent)]
110    BcsError(bcs::Error),
111
112    /// Refusing to use the journal without exclusive access.
113    #[error("Refusing to use the journal without exclusive database access to the root object.")]
114    JournalRequiresExclusiveAccess,
115
116    /// Journal resolution failed; storage may be in an inconsistent state.
117    /// The view must be reloaded to complete the pending journal.
118    #[error("Journal resolution failed: {0}")]
119    JournalResolutionFailed(JournalingResolutionError<E>),
120}
121
122/// Error type for the journaling key-value store layer.
123#[derive(Error, Debug)]
124pub enum JournalingResolutionError<E> {
125    /// Error from the inner store.
126    #[error(transparent)]
127    Inner(#[from] E),
128
129    /// BCS serialization error.
130    #[error(transparent)]
131    BcsError(bcs::Error),
132
133    /// The journal block could not be retrieved.
134    #[error("The journal block could not be retrieved, it could be missing or corrupted.")]
135    FailureToRetrieveJournalBlock,
136}
137
138impl<E: KeyValueStoreError> From<bcs::Error> for JournalingError<E> {
139    fn from(error: bcs::Error) -> Self {
140        JournalingError::BcsError(error)
141    }
142}
143
144impl<E: KeyValueStoreError + 'static> KeyValueStoreError for JournalingError<E> {
145    const BACKEND: &'static str = "journaling";
146
147    fn must_reload_view(&self) -> bool {
148        match self {
149            JournalingError::Inner(error) => error.must_reload_view(),
150            JournalingError::JournalResolutionFailed(_) => true,
151            JournalingError::BcsError(_) | JournalingError::JournalRequiresExclusiveAccess => false,
152        }
153    }
154}
155
156impl<E: KeyValueStoreError> From<bcs::Error> for JournalingResolutionError<E> {
157    fn from(error: bcs::Error) -> Self {
158        JournalingResolutionError::BcsError(error)
159    }
160}
161
162/// The tag used for the journal stuff.
163const JOURNAL_TAG: u8 = 0;
164// To prevent collisions, the tag value 0 is reserved for journals.
165// The tags used by views must be greater or equal than `MIN_VIEW_TAG`.
166sa::const_assert!(JOURNAL_TAG < MIN_VIEW_TAG);
167
168#[repr(u8)]
169enum KeyTag {
170    /// Prefix for the storing of the header of the journal.
171    Journal = 1,
172    /// Prefix for the block entry.
173    Entry,
174}
175
176fn get_journaling_key(tag: u8, pos: u32) -> Result<Vec<u8>, bcs::Error> {
177    let mut key = vec![JOURNAL_TAG];
178    key.extend([tag]);
179    bcs::serialize_into(&mut key, &pos)?;
180    Ok(key)
181}
182
183/// The header that contains the current state of the journal.
184#[derive(Serialize, Deserialize, Debug, Default)]
185struct JournalHeader {
186    block_count: u32,
187}
188
189impl<S> DeletePrefixExpander for &JournalingKeyValueStore<S>
190where
191    S: DirectKeyValueStore,
192{
193    type Error = S::Error;
194
195    async fn expand_delete_prefix(&self, key_prefix: &[u8]) -> Result<Vec<Vec<u8>>, Self::Error> {
196        self.store.find_keys_by_prefix(key_prefix).await
197    }
198}
199
200impl<D> WithError for JournalingKeyValueDatabase<D>
201where
202    D: WithError,
203    D::Error: 'static,
204{
205    type Error = JournalingError<D::Error>;
206}
207
208impl<S> WithError for JournalingKeyValueStore<S>
209where
210    S: WithError,
211    S::Error: 'static,
212{
213    type Error = JournalingError<S::Error>;
214}
215
216impl<S> ReadableKeyValueStore for JournalingKeyValueStore<S>
217where
218    S: ReadableKeyValueStore,
219    S::Error: 'static,
220{
221    const MAX_KEY_SIZE: usize = S::MAX_KEY_SIZE;
222
223    fn max_stream_queries(&self) -> usize {
224        self.store.max_stream_queries()
225    }
226
227    fn root_key(&self) -> Result<Vec<u8>, Self::Error> {
228        Ok(self.store.root_key()?)
229    }
230
231    async fn read_value_bytes(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
232        Ok(self.store.read_value_bytes(key).await?)
233    }
234
235    async fn contains_key(&self, key: &[u8]) -> Result<bool, Self::Error> {
236        Ok(self.store.contains_key(key).await?)
237    }
238
239    async fn contains_keys(&self, keys: &[Vec<u8>]) -> Result<Vec<bool>, Self::Error> {
240        Ok(self.store.contains_keys(keys).await?)
241    }
242
243    async fn read_multi_values_bytes(
244        &self,
245        keys: &[Vec<u8>],
246    ) -> Result<Vec<Option<Vec<u8>>>, Self::Error> {
247        Ok(self.store.read_multi_values_bytes(keys).await?)
248    }
249
250    async fn find_keys_by_prefix(&self, key_prefix: &[u8]) -> Result<Vec<Vec<u8>>, Self::Error> {
251        Ok(self.store.find_keys_by_prefix(key_prefix).await?)
252    }
253
254    async fn find_key_values_by_prefix(
255        &self,
256        key_prefix: &[u8],
257    ) -> Result<Vec<(Vec<u8>, Vec<u8>)>, Self::Error> {
258        Ok(self.store.find_key_values_by_prefix(key_prefix).await?)
259    }
260}
261
262impl<D> KeyValueDatabase for JournalingKeyValueDatabase<D>
263where
264    D: KeyValueDatabase,
265    D::Error: 'static,
266{
267    type Config = D::Config;
268    type Store = JournalingKeyValueStore<D::Store>;
269
270    fn get_name() -> String {
271        format!("journaling {}", D::get_name())
272    }
273
274    async fn connect(config: &Self::Config, namespace: &str) -> Result<Self, Self::Error> {
275        let database = D::connect(config, namespace).await?;
276        Ok(Self { database })
277    }
278
279    fn open_shared(&self, root_key: &[u8]) -> Result<Self::Store, Self::Error> {
280        let store = self.database.open_shared(root_key)?;
281        Ok(JournalingKeyValueStore {
282            store,
283            has_exclusive_access: false,
284        })
285    }
286
287    fn open_exclusive(&self, root_key: &[u8]) -> Result<Self::Store, Self::Error> {
288        let store = self.database.open_exclusive(root_key)?;
289        Ok(JournalingKeyValueStore {
290            store,
291            has_exclusive_access: true,
292        })
293    }
294
295    async fn list_all(config: &Self::Config) -> Result<Vec<String>, Self::Error> {
296        Ok(D::list_all(config).await?)
297    }
298
299    async fn list_root_keys(&self) -> Result<Vec<Vec<u8>>, Self::Error> {
300        Ok(self.database.list_root_keys().await?)
301    }
302
303    async fn delete_all(config: &Self::Config) -> Result<(), Self::Error> {
304        Ok(D::delete_all(config).await?)
305    }
306
307    async fn exists(config: &Self::Config, namespace: &str) -> Result<bool, Self::Error> {
308        Ok(D::exists(config, namespace).await?)
309    }
310
311    async fn create(config: &Self::Config, namespace: &str) -> Result<(), Self::Error> {
312        Ok(D::create(config, namespace).await?)
313    }
314
315    async fn delete(config: &Self::Config, namespace: &str) -> Result<(), Self::Error> {
316        Ok(D::delete(config, namespace).await?)
317    }
318}
319
320impl<S> WritableKeyValueStore for JournalingKeyValueStore<S>
321where
322    S: DirectKeyValueStore,
323    S::Error: 'static,
324{
325    const MAX_VALUE_SIZE: usize = S::MAX_VALUE_SIZE;
326
327    async fn write_batch(&self, batch: Batch) -> Result<(), Self::Error> {
328        let batch = S::Batch::from_batch(self, batch).await?;
329        #[cfg(with_metrics)]
330        metrics::JOURNAL_BATCH_LEN.observe(batch.len() as f64);
331        if Self::is_fastpath_feasible(&batch) {
332            tracing::trace!(
333                batch_len = batch.len(),
334                batch_bytes = batch.num_bytes(),
335                "write_batch: using fast path"
336            );
337            #[cfg(with_metrics)]
338            metrics::JOURNAL_FASTPATH_COUNT.inc();
339            Ok(self.store.write_batch(batch).await?)
340        } else {
341            tracing::warn!(
342                batch_len = batch.len(),
343                batch_bytes = batch.num_bytes(),
344                max_batch_size = S::MAX_BATCH_SIZE,
345                max_batch_total_size = S::MAX_BATCH_TOTAL_SIZE,
346                "write_batch: batch exceeds fast path limits, using journal"
347            );
348            #[cfg(with_metrics)]
349            metrics::JOURNAL_SLOWPATH_COUNT.inc();
350            if !self.has_exclusive_access {
351                return Err(JournalingError::JournalRequiresExclusiveAccess);
352            }
353            let header = self.write_journal(batch).await?;
354            tracing::info!(
355                block_count = header.block_count,
356                "write_batch: journal written, resolving"
357            );
358            match self.coherently_resolve_journal(header).await {
359                Ok(()) => Ok(()),
360                Err(e) => {
361                    tracing::error!(
362                        "write_batch: FAILED to resolve journal — \
363                        storage may be in an inconsistent state until \
364                        the journal is cleared on next reload"
365                    );
366                    #[cfg(with_metrics)]
367                    metrics::JOURNAL_RESOLUTION_FAILURES.inc();
368                    Err(JournalingError::JournalResolutionFailed(e))
369                }
370            }
371        }
372    }
373
374    async fn clear_journal(&self) -> Result<(), Self::Error> {
375        let key = get_journaling_key(KeyTag::Journal as u8, 0)?;
376        let value = self.read_value::<JournalHeader>(&key).await?;
377        if let Some(header) = value {
378            tracing::warn!(
379                block_count = header.block_count,
380                "clear_journal: found pending journal, resolving"
381            );
382            #[cfg(with_metrics)]
383            metrics::JOURNAL_PENDING_ON_LOAD.inc();
384            match self.coherently_resolve_journal(header).await {
385                Ok(()) => Ok(()),
386                Err(e) => {
387                    tracing::error!(
388                        "write_batch: FAILED to resolve journal — \
389                        storage may be in an inconsistent state until \
390                        the journal is cleared on next reload"
391                    );
392                    #[cfg(with_metrics)]
393                    metrics::JOURNAL_RESOLUTION_FAILURES.inc();
394                    Err(JournalingError::JournalResolutionFailed(e))
395                }
396            }
397        } else {
398            Ok(())
399        }
400    }
401}
402
403impl<S> JournalingKeyValueStore<S>
404where
405    S: DirectKeyValueStore,
406    S::Error: 'static,
407{
408    /// Resolves the pending operations that were previously stored in the database
409    /// journal.
410    ///
411    /// For each block processed, we atomically update the journal header as well. When
412    /// the last block is processed, this atomically clears the journal and make the store
413    /// finally available again (for the range of keys managed by the journal).
414    ///
415    /// This function respects the constraints of the underlying key-value store `K` if
416    /// the following conditions are met:
417    ///
418    /// (1) each block contains at most `S::MAX_BATCH_SIZE - 2` operations;
419    ///
420    /// (2) the total size of the all operations in a block doesn't exceed:
421    /// `S::MAX_BATCH_TOTAL_SIZE - sizeof(block_key) - sizeof(header_key) - sizeof(bcs_header)`
422    ///
423    /// (3) every operation in a block satisfies the constraints on individual database
424    /// operations represented by `S::MAX_KEY_SIZE` and `S::MAX_VALUE_SIZE`.
425    ///
426    /// (4) `block_key` and `header_key` don't exceed `S::MAX_KEY_SIZE` and `bcs_header`
427    /// doesn't exceed `S::MAX_VALUE_SIZE`.
428    async fn coherently_resolve_journal(
429        &self,
430        mut header: JournalHeader,
431    ) -> Result<(), JournalingResolutionError<S::Error>> {
432        let total_blocks = header.block_count;
433        let header_key = get_journaling_key(KeyTag::Journal as u8, 0)?;
434        while header.block_count > 0 {
435            let block_key = get_journaling_key(KeyTag::Entry as u8, header.block_count - 1)?;
436            // Read the batch of updates (aka. "block") previously saved in the journal.
437            let mut batch = self
438                .store
439                .read_value::<S::Batch>(&block_key)
440                .await?
441                .ok_or(JournalingResolutionError::FailureToRetrieveJournalBlock)?;
442            // Execute the block and delete it from the journal atomically.
443            batch.add_delete(block_key);
444            header.block_count -= 1;
445            if header.block_count > 0 {
446                let value = bcs::to_bytes(&header)?;
447                batch.add_insert(header_key.clone(), value);
448            } else {
449                batch.add_delete(header_key.clone());
450            }
451            tracing::debug!(
452                remaining_blocks = header.block_count,
453                total_blocks,
454                "resolving journal block"
455            );
456            self.store.write_batch(batch).await?;
457        }
458        tracing::info!(total_blocks, "journal fully resolved");
459        Ok(())
460    }
461
462    /// Writes the content of `batch` to the journal as a succession of blocks that can be
463    /// interpreted later by `coherently_resolve_journal`.
464    ///
465    /// Starting with a batch of operations that is typically too large to be executed in
466    /// one go (see `is_fastpath_feasible()` below), the goal of this function is to split
467    /// the batch into smaller blocks so that `coherently_resolve_journal` respects the
468    /// constraints of the underlying key-value store (see analysis above).
469    ///
470    /// For efficiency reasons, we write as many blocks as possible in each "transaction"
471    /// batch, using one write-operation per block. Then we also update the journal header
472    /// with the final number of blocks.
473    ///
474    /// As a result, the constraints of the underlying database are respected if the
475    /// following conditions are met while a "transaction" batch is being built:
476    ///
477    /// (1) The number of blocks per transaction doesn't exceed `S::MAX_BATCH_SIZE`.
478    /// But it is perfectly possible to have `S::MAX_BATCH_SIZE = usize::MAX`.
479    ///
480    /// (2) The total size of BCS-serialized blocks together with their corresponding keys
481    /// does not exceed `S::MAX_BATCH_TOTAL_SIZE`.
482    ///
483    /// (3) The size of each BCS-serialized block doesn't exceed `S::MAX_VALUE_SIZE`.
484    ///
485    /// (4) When processing a journal block, we have to do two other operations.
486    ///   (a) removing the existing block. The cost is `key_len`.
487    ///   (b) updating or removing the journal. The cost is `key_len + header_value_len`
488    ///       or `key_len`. An upper bound is thus
489    ///       `journal_len_upper_bound = key_len + header_value_len`.
490    ///   Thus the following has to be taken as upper bound on the block size:
491    ///   `S::MAX_BATCH_TOTAL_SIZE - key_len - journal_len_upper_bound`.
492    ///
493    /// NOTE:
494    /// * Since a block must contain at least one operation and M bytes of the
495    ///   serialization overhead (typically M is 2 or 3 bytes of vector sizes), condition (3)
496    ///   requires that each operation in the original batch satisfies:
497    ///   `sizeof(key) + sizeof(value) + M <= S::MAX_VALUE_SIZE`
498    ///
499    /// * Similarly, a transaction must contain at least one block so it is desirable that
500    ///   the maximum size of a block insertion `1 + sizeof(block_key) + S::MAX_VALUE_SIZE`
501    ///   plus M bytes of overhead doesn't exceed the threshold of condition (2).
502    async fn write_journal(
503        &self,
504        batch: S::Batch,
505    ) -> Result<JournalHeader, JournalingError<S::Error>> {
506        let header_key = get_journaling_key(KeyTag::Journal as u8, 0)?;
507        let key_len = header_key.len();
508        let header_value_len = bcs::serialized_size(&JournalHeader::default())?;
509        let journal_len_upper_bound = key_len + header_value_len;
510        // Each block in a transaction comes with a key.
511        let max_transaction_size = S::MAX_BATCH_TOTAL_SIZE;
512        let max_block_size = std::cmp::min(
513            S::MAX_VALUE_SIZE,
514            S::MAX_BATCH_TOTAL_SIZE - key_len - journal_len_upper_bound,
515        );
516
517        let mut iter = batch.into_iter();
518        let mut block_batch = S::Batch::default();
519        let mut block_size = 0;
520        let mut block_count = 0;
521        let mut transaction_batch = S::Batch::default();
522        let mut transaction_size = 0;
523        while iter.write_next_value(&mut block_batch, &mut block_size)? {
524            let (block_flush, transaction_flush) = if transaction_batch.len()
525                == S::MAX_BATCH_SIZE - 1
526            {
527                (true, true)
528            } else if let Some(next_block_size) = iter.next_batch_size(&block_batch, block_size)? {
529                let next_transaction_size = transaction_size + next_block_size + key_len;
530                let transaction_flush = next_transaction_size > max_transaction_size;
531                let block_flush = transaction_flush
532                    || block_batch.len() == S::MAX_BATCH_SIZE - 2
533                    || next_block_size > max_block_size;
534                (block_flush, transaction_flush)
535            } else {
536                (true, true)
537            };
538            if block_flush {
539                block_size += block_batch.overhead_size();
540                let value = bcs::to_bytes(&block_batch)?;
541                block_batch = S::Batch::default();
542                assert_eq!(value.len(), block_size);
543                let key = get_journaling_key(KeyTag::Entry as u8, block_count)?;
544                transaction_batch.add_insert(key, value);
545                block_count += 1;
546                transaction_size += block_size + key_len;
547                block_size = 0;
548            }
549            if transaction_flush {
550                let batch = std::mem::take(&mut transaction_batch);
551                self.store.write_batch(batch).await?;
552                transaction_size = 0;
553            }
554        }
555        // Until the journal header is written nothing is committed.
556        let header = JournalHeader { block_count };
557        if block_count > 0 {
558            let value = bcs::to_bytes(&header)?;
559            let mut batch = S::Batch::default();
560            batch.add_insert(header_key, value);
561            self.store.write_batch(batch).await?;
562        }
563        Ok(header)
564    }
565
566    fn is_fastpath_feasible(batch: &S::Batch) -> bool {
567        batch.len() <= S::MAX_BATCH_SIZE && batch.num_bytes() <= S::MAX_BATCH_TOTAL_SIZE
568    }
569}
570
571impl<S> JournalingKeyValueStore<S> {
572    /// Creates a new journaling store.
573    pub fn new(store: S) -> Self {
574        Self {
575            store,
576            has_exclusive_access: false,
577        }
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584
585    #[derive(Debug, Error)]
586    enum MockError {
587        #[error("requires reload")]
588        MustReload,
589        #[error("benign")]
590        Benign,
591        #[error(transparent)]
592        Bcs(#[from] bcs::Error),
593    }
594
595    impl KeyValueStoreError for MockError {
596        const BACKEND: &'static str = "mock";
597
598        fn must_reload_view(&self) -> bool {
599            matches!(self, MockError::MustReload)
600        }
601    }
602
603    #[test]
604    fn journaling_error_inner_delegates_must_reload_view() {
605        assert!(JournalingError::Inner(MockError::MustReload).must_reload_view());
606        assert!(!JournalingError::Inner(MockError::Benign).must_reload_view());
607        assert!(JournalingError::<MockError>::JournalResolutionFailed(
608            JournalingResolutionError::FailureToRetrieveJournalBlock
609        )
610        .must_reload_view());
611        assert!(!JournalingError::<MockError>::JournalRequiresExclusiveAccess.must_reload_view());
612    }
613}