1use 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 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 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 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 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 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#[derive(Clone)]
88pub struct JournalingKeyValueDatabase<D> {
89 database: D,
90}
91
92#[derive(Clone)]
94pub struct JournalingKeyValueStore<S> {
95 store: S,
97 has_exclusive_access: bool,
99}
100
101#[derive(Error, Debug)]
103pub enum JournalingError<E> {
104 #[error(transparent)]
106 Inner(#[from] E),
107
108 #[error(transparent)]
110 BcsError(bcs::Error),
111
112 #[error("Refusing to use the journal without exclusive database access to the root object.")]
114 JournalRequiresExclusiveAccess,
115
116 #[error("Journal resolution failed: {0}")]
119 JournalResolutionFailed(JournalingResolutionError<E>),
120}
121
122#[derive(Error, Debug)]
124pub enum JournalingResolutionError<E> {
125 #[error(transparent)]
127 Inner(#[from] E),
128
129 #[error(transparent)]
131 BcsError(bcs::Error),
132
133 #[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
162const JOURNAL_TAG: u8 = 0;
164sa::const_assert!(JOURNAL_TAG < MIN_VIEW_TAG);
167
168#[repr(u8)]
169enum KeyTag {
170 Journal = 1,
172 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#[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 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 let mut batch = self
438 .store
439 .read_value::<S::Batch>(&block_key)
440 .await?
441 .ok_or(JournalingResolutionError::FailureToRetrieveJournalBlock)?;
442 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 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 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 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 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}