linera_service/
storage.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{fmt, str::FromStr};
5
6use anyhow::anyhow;
7use async_trait::async_trait;
8use linera_client::config::GenesisConfig;
9use linera_execution::WasmRuntime;
10use linera_storage::{DbStorage, Storage, DEFAULT_NAMESPACE};
11#[cfg(feature = "storage-service")]
12use linera_storage_service::{
13    client::ServiceStoreClient,
14    common::{ServiceStoreConfig, ServiceStoreInternalConfig},
15};
16#[cfg(feature = "dynamodb")]
17use linera_views::dynamo_db::{DynamoDbStore, DynamoDbStoreConfig};
18use linera_views::{
19    memory::{MemoryStore, MemoryStoreConfig},
20    store::{CommonStoreConfig, KeyValueStore},
21};
22use serde::{Deserialize, Serialize};
23use tracing::error;
24#[allow(unused_imports)]
25use {anyhow::bail, linera_views::store::AdminKeyValueStore as _};
26#[cfg(all(feature = "rocksdb", feature = "scylladb"))]
27use {
28    linera_storage::ChainStatesFirstAssignment,
29    linera_views::backends::dual::{DualStore, DualStoreConfig},
30    std::path::Path,
31};
32#[cfg(feature = "rocksdb")]
33use {
34    linera_views::rocks_db::{PathWithGuard, RocksDbSpawnMode, RocksDbStore, RocksDbStoreConfig},
35    std::path::PathBuf,
36};
37#[cfg(feature = "scylladb")]
38use {
39    linera_views::scylla_db::{ScyllaDbStore, ScyllaDbStoreConfig},
40    std::num::NonZeroU16,
41    tracing::debug,
42};
43
44/// The configuration of the key value store in use.
45#[derive(Clone, Debug, Deserialize, Serialize)]
46pub enum StoreConfig {
47    /// The storage service key-value store
48    #[cfg(feature = "storage-service")]
49    Service {
50        config: ServiceStoreConfig,
51        namespace: String,
52    },
53    /// The memory key value store
54    Memory {
55        config: MemoryStoreConfig,
56        namespace: String,
57    },
58    /// The RocksDB key value store
59    #[cfg(feature = "rocksdb")]
60    RocksDb {
61        config: RocksDbStoreConfig,
62        namespace: String,
63    },
64    /// The DynamoDB key value store
65    #[cfg(feature = "dynamodb")]
66    DynamoDb {
67        config: DynamoDbStoreConfig,
68        namespace: String,
69    },
70    /// The ScyllaDB key value store
71    #[cfg(feature = "scylladb")]
72    ScyllaDb {
73        config: ScyllaDbStoreConfig,
74        namespace: String,
75    },
76    #[cfg(all(feature = "rocksdb", feature = "scylladb"))]
77    DualRocksDbScyllaDb {
78        config: DualStoreConfig<RocksDbStoreConfig, ScyllaDbStoreConfig>,
79        namespace: String,
80    },
81}
82
83/// The description of a storage implementation.
84#[derive(Clone, Debug)]
85#[cfg_attr(any(test), derive(Eq, PartialEq))]
86pub enum StorageConfig {
87    /// The storage service description.
88    #[cfg(feature = "storage-service")]
89    Service {
90        /// The endpoint used.
91        endpoint: String,
92    },
93    /// The memory description.
94    Memory,
95    /// The RocksDB description.
96    #[cfg(feature = "rocksdb")]
97    RocksDb {
98        /// The path used.
99        path: PathBuf,
100        /// Whether to use `block_in_place` or `spawn_blocking`.
101        spawn_mode: RocksDbSpawnMode,
102    },
103    /// The DynamoDB description.
104    #[cfg(feature = "dynamodb")]
105    DynamoDb {
106        /// Whether to use the DynamoDB Local system
107        use_dynamodb_local: bool,
108    },
109    /// The ScyllaDB description.
110    #[cfg(feature = "scylladb")]
111    ScyllaDb {
112        /// The URI for accessing the database.
113        uri: String,
114    },
115    #[cfg(all(feature = "rocksdb", feature = "scylladb"))]
116    DualRocksDbScyllaDb {
117        /// The path used.
118        path_with_guard: PathWithGuard,
119        /// Whether to use `block_in_place` or `spawn_blocking`.
120        spawn_mode: RocksDbSpawnMode,
121        /// The URI for accessing the database.
122        uri: String,
123    },
124}
125
126impl StorageConfig {
127    pub fn maybe_append_shard_path(&mut self, _shard: usize) -> std::io::Result<()> {
128        match self {
129            #[cfg(all(feature = "rocksdb", feature = "scylladb"))]
130            StorageConfig::DualRocksDbScyllaDb {
131                path_with_guard,
132                spawn_mode: _,
133                uri: _,
134            } => {
135                let shard_str = format!("shard_{}", _shard);
136                path_with_guard.path_buf.push(shard_str);
137                std::fs::create_dir_all(&path_with_guard.path_buf)
138            }
139            _ => Ok(()),
140        }
141    }
142}
143
144/// The description of a storage implementation.
145#[derive(Clone, Debug)]
146#[cfg_attr(any(test), derive(Eq, PartialEq))]
147pub struct StorageConfigNamespace {
148    /// The storage config
149    pub storage_config: StorageConfig,
150    /// The namespace used
151    pub namespace: String,
152}
153
154const MEMORY: &str = "memory";
155const MEMORY_EXT: &str = "memory:";
156#[cfg(feature = "storage-service")]
157const STORAGE_SERVICE: &str = "service:";
158#[cfg(feature = "rocksdb")]
159const ROCKS_DB: &str = "rocksdb:";
160#[cfg(feature = "dynamodb")]
161const DYNAMO_DB: &str = "dynamodb:";
162#[cfg(feature = "scylladb")]
163const SCYLLA_DB: &str = "scylladb:";
164#[cfg(all(feature = "rocksdb", feature = "scylladb"))]
165const DUAL_ROCKS_DB_SCYLLA_DB: &str = "dualrocksdbscylladb:";
166
167impl FromStr for StorageConfigNamespace {
168    type Err = anyhow::Error;
169
170    fn from_str(input: &str) -> Result<Self, Self::Err> {
171        if input == MEMORY {
172            let namespace = DEFAULT_NAMESPACE.to_string();
173            let storage_config = StorageConfig::Memory;
174            return Ok(StorageConfigNamespace {
175                storage_config,
176                namespace,
177            });
178        }
179        if let Some(s) = input.strip_prefix(MEMORY_EXT) {
180            let namespace = s.to_string();
181            let storage_config = StorageConfig::Memory;
182            return Ok(StorageConfigNamespace {
183                storage_config,
184                namespace,
185            });
186        }
187        #[cfg(feature = "storage-service")]
188        if let Some(s) = input.strip_prefix(STORAGE_SERVICE) {
189            if s.is_empty() {
190                bail!(
191                    "For Storage service, the formatting has to be service:endpoint:namespace,\
192example service:tcp:127.0.0.1:7878:table_do_my_test"
193                );
194            }
195            let parts = s.split(':').collect::<Vec<_>>();
196            if parts.len() != 4 {
197                bail!("We should have one endpoint and one namespace");
198            }
199            let protocol = parts[0];
200            if protocol != "tcp" {
201                bail!("Only allowed protocol is tcp");
202            }
203            let endpoint = parts[1];
204            let port = parts[2];
205            let mut endpoint = endpoint.to_string();
206            endpoint.push(':');
207            endpoint.push_str(port);
208            let endpoint = endpoint.to_string();
209            let namespace = parts[3].to_string();
210            let storage_config = StorageConfig::Service { endpoint };
211            return Ok(StorageConfigNamespace {
212                storage_config,
213                namespace,
214            });
215        }
216        #[cfg(feature = "rocksdb")]
217        if let Some(s) = input.strip_prefix(ROCKS_DB) {
218            if s.is_empty() {
219                bail!(
220                    "For RocksDB, the formatting has to be rocksdb:directory or rocksdb:directory:spawn_mode:namespace");
221            }
222            let parts = s.split(':').collect::<Vec<_>>();
223            if parts.len() == 1 {
224                let path = parts[0].to_string().into();
225                let namespace = DEFAULT_NAMESPACE.to_string();
226                let spawn_mode = RocksDbSpawnMode::SpawnBlocking;
227                let storage_config = StorageConfig::RocksDb { path, spawn_mode };
228                return Ok(StorageConfigNamespace {
229                    storage_config,
230                    namespace,
231                });
232            }
233            if parts.len() == 2 || parts.len() == 3 {
234                let path = parts[0].to_string().into();
235                let spawn_mode = match parts[1] {
236                    "spawn_blocking" => Ok(RocksDbSpawnMode::SpawnBlocking),
237                    "block_in_place" => Ok(RocksDbSpawnMode::BlockInPlace),
238                    "runtime" => Ok(RocksDbSpawnMode::get_spawn_mode_from_runtime()),
239                    _ => Err(anyhow!("Failed to parse {} as a spawn_mode", parts[1])),
240                }?;
241                let namespace = if parts.len() == 2 {
242                    DEFAULT_NAMESPACE.to_string()
243                } else {
244                    parts[2].to_string()
245                };
246                let storage_config = StorageConfig::RocksDb { path, spawn_mode };
247                return Ok(StorageConfigNamespace {
248                    storage_config,
249                    namespace,
250                });
251            }
252            bail!("We should have one, two or three parts");
253        }
254        #[cfg(feature = "dynamodb")]
255        if let Some(s) = input.strip_prefix(DYNAMO_DB) {
256            let mut parts = s.splitn(2, ':');
257            let namespace = parts
258                .next()
259                .ok_or_else(|| anyhow!("Missing DynamoDB table name, e.g. {DYNAMO_DB}TABLE"))?
260                .to_string();
261            let use_dynamodb_local = match parts.next() {
262                None | Some("env") => false,
263                Some("dynamodb_local") => true,
264                Some(unknown) => {
265                    bail!(
266                        "Invalid DynamoDB endpoint {unknown:?}. \
267                        Expected {DYNAMO_DB}TABLE:[env|dynamodb_local]"
268                    );
269                }
270            };
271            let storage_config = StorageConfig::DynamoDb { use_dynamodb_local };
272            return Ok(StorageConfigNamespace {
273                storage_config,
274                namespace,
275            });
276        }
277        #[cfg(feature = "scylladb")]
278        if let Some(s) = input.strip_prefix(SCYLLA_DB) {
279            let mut uri: Option<String> = None;
280            let mut namespace: Option<String> = None;
281            let parse_error: &'static str = "Correct format is tcp:db_hostname:port.";
282            if !s.is_empty() {
283                let mut parts = s.split(':');
284                while let Some(part) = parts.next() {
285                    match part {
286                        "tcp" => {
287                            let address = parts.next().ok_or_else(|| {
288                                anyhow!("Failed to find address for {s}. {parse_error}")
289                            })?;
290                            let port_str = parts.next().ok_or_else(|| {
291                                anyhow!("Failed to find port for {s}. {parse_error}")
292                            })?;
293                            let port = NonZeroU16::from_str(port_str).map_err(|_| {
294                                anyhow!(
295                                    "Failed to find parse port {port_str} for {s}. {parse_error}",
296                                )
297                            })?;
298                            if uri.is_some() {
299                                bail!("The uri has already been assigned");
300                            }
301                            uri = Some(format!("{}:{}", &address, port));
302                        }
303                        _ if part.starts_with("table") => {
304                            if namespace.is_some() {
305                                bail!("The namespace has already been assigned");
306                            }
307                            namespace = Some(part.to_string());
308                        }
309                        _ => {
310                            bail!("the entry \"{part}\" is not matching");
311                        }
312                    }
313                }
314            }
315            let uri = uri.unwrap_or("localhost:9042".to_string());
316            let namespace = namespace.unwrap_or(DEFAULT_NAMESPACE.to_string());
317            let storage_config = StorageConfig::ScyllaDb { uri };
318            debug!("ScyllaDB connection info: {:?}", storage_config);
319            return Ok(StorageConfigNamespace {
320                storage_config,
321                namespace,
322            });
323        }
324        #[cfg(all(feature = "rocksdb", feature = "scylladb"))]
325        if let Some(s) = input.strip_prefix(DUAL_ROCKS_DB_SCYLLA_DB) {
326            let parts = s.split(':').collect::<Vec<_>>();
327            if parts.len() != 5 && parts.len() != 6 {
328                bail!(
329                    "For DualRocksDbScyllaDb, the formatting has to be dualrocksdbscylladb:directory:mode:tcp:hostname:port:namespace"
330                );
331            }
332            let path = Path::new(parts[0]);
333            let path = path.to_path_buf();
334            let path_with_guard = PathWithGuard::new(path);
335            let spawn_mode = match parts[1] {
336                "spawn_blocking" => Ok(RocksDbSpawnMode::SpawnBlocking),
337                "block_in_place" => Ok(RocksDbSpawnMode::BlockInPlace),
338                "runtime" => Ok(RocksDbSpawnMode::get_spawn_mode_from_runtime()),
339                _ => Err(anyhow!("Failed to parse {} as a spawn_mode", parts[1])),
340            }?;
341            let protocol = parts[2];
342            if protocol != "tcp" {
343                bail!("The only allowed protocol is tcp");
344            }
345            let address = parts[3];
346            let port_str = parts[4];
347            let port = NonZeroU16::from_str(port_str)
348                .map_err(|_| anyhow!("Failed to find parse port {port_str} for {s}"))?;
349            let uri = format!("{}:{}", &address, port);
350            let storage_config = StorageConfig::DualRocksDbScyllaDb {
351                path_with_guard,
352                spawn_mode,
353                uri,
354            };
355            let namespace = if parts.len() == 5 {
356                DEFAULT_NAMESPACE.to_string()
357            } else {
358                parts[5].to_string()
359            };
360            return Ok(StorageConfigNamespace {
361                storage_config,
362                namespace,
363            });
364        }
365        error!("available storage: memory");
366        #[cfg(feature = "storage-service")]
367        error!("Also available is linera-storage-service");
368        #[cfg(feature = "rocksdb")]
369        error!("Also available is RocksDB");
370        #[cfg(feature = "dynamodb")]
371        error!("Also available is DynamoDB");
372        #[cfg(feature = "scylladb")]
373        error!("Also available is ScyllaDB");
374        #[cfg(all(feature = "rocksdb", feature = "scylladb"))]
375        error!("Also available is DualRocksDbScyllaDb");
376        Err(anyhow!("The input has not matched: {input}"))
377    }
378}
379
380impl StorageConfigNamespace {
381    /// The addition of the common config to get a full configuration
382    pub async fn add_common_config(
383        &self,
384        common_config: CommonStoreConfig,
385    ) -> Result<StoreConfig, anyhow::Error> {
386        let namespace = self.namespace.clone();
387        match &self.storage_config {
388            #[cfg(feature = "storage-service")]
389            StorageConfig::Service { endpoint } => {
390                let endpoint = endpoint.clone();
391                let inner_config = ServiceStoreInternalConfig {
392                    endpoint,
393                    common_config: common_config.reduced(),
394                };
395                let config = ServiceStoreConfig {
396                    inner_config,
397                    storage_cache_config: common_config.storage_cache_config,
398                };
399                Ok(StoreConfig::Service { config, namespace })
400            }
401            StorageConfig::Memory => {
402                let config = MemoryStoreConfig {
403                    common_config: common_config.reduced(),
404                };
405                Ok(StoreConfig::Memory { config, namespace })
406            }
407            #[cfg(feature = "rocksdb")]
408            StorageConfig::RocksDb { path, spawn_mode } => {
409                let path_buf = path.to_path_buf();
410                let path_with_guard = PathWithGuard::new(path_buf);
411                let config = RocksDbStoreConfig::new(*spawn_mode, path_with_guard, common_config);
412                Ok(StoreConfig::RocksDb { config, namespace })
413            }
414            #[cfg(feature = "dynamodb")]
415            StorageConfig::DynamoDb { use_dynamodb_local } => {
416                let config = DynamoDbStoreConfig::new(*use_dynamodb_local, common_config);
417                Ok(StoreConfig::DynamoDb { config, namespace })
418            }
419            #[cfg(feature = "scylladb")]
420            StorageConfig::ScyllaDb { uri } => {
421                let config = ScyllaDbStoreConfig::new(uri.to_string(), common_config);
422                Ok(StoreConfig::ScyllaDb { config, namespace })
423            }
424            #[cfg(all(feature = "rocksdb", feature = "scylladb"))]
425            StorageConfig::DualRocksDbScyllaDb {
426                path_with_guard,
427                spawn_mode,
428                uri,
429            } => {
430                let first_config = RocksDbStoreConfig::new(
431                    *spawn_mode,
432                    path_with_guard.clone(),
433                    common_config.clone(),
434                );
435                let second_config = ScyllaDbStoreConfig::new(uri.to_string(), common_config);
436                let config = DualStoreConfig {
437                    first_config,
438                    second_config,
439                };
440                Ok(StoreConfig::DualRocksDbScyllaDb { config, namespace })
441            }
442        }
443    }
444}
445
446impl fmt::Display for StorageConfigNamespace {
447    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
448        let namespace = &self.namespace;
449        match &self.storage_config {
450            #[cfg(feature = "storage-service")]
451            StorageConfig::Service { endpoint } => {
452                write!(f, "service:tcp:{}:{}", endpoint, namespace)
453            }
454            StorageConfig::Memory => {
455                write!(f, "memory:{}", namespace)
456            }
457            #[cfg(feature = "rocksdb")]
458            StorageConfig::RocksDb { path, spawn_mode } => {
459                let spawn_mode = spawn_mode.to_string();
460                write!(f, "rocksdb:{}:{}:{}", path.display(), spawn_mode, namespace)
461            }
462            #[cfg(feature = "dynamodb")]
463            StorageConfig::DynamoDb { use_dynamodb_local } => match use_dynamodb_local {
464                true => write!(f, "dynamodb:{}:dynamodb_local", namespace),
465                false => write!(f, "dynamodb:{}:env", namespace),
466            },
467            #[cfg(feature = "scylladb")]
468            StorageConfig::ScyllaDb { uri } => {
469                write!(f, "scylladb:tcp:{}:{}", uri, namespace)
470            }
471            #[cfg(all(feature = "rocksdb", feature = "scylladb"))]
472            StorageConfig::DualRocksDbScyllaDb {
473                path_with_guard,
474                spawn_mode,
475                uri,
476            } => {
477                write!(
478                    f,
479                    "dualrocksdbscylladb:{}:{}:tcp:{}:{}",
480                    path_with_guard.path_buf.display(),
481                    spawn_mode,
482                    uri,
483                    namespace
484                )
485            }
486        }
487    }
488}
489
490#[async_trait]
491pub trait Runnable {
492    type Output;
493
494    async fn run<S>(self, storage: S) -> Self::Output
495    where
496        S: Storage + Clone + Send + Sync + 'static;
497}
498
499#[async_trait]
500pub trait RunnableWithStore {
501    type Output;
502
503    async fn run<S>(
504        self,
505        config: S::Config,
506        namespace: String,
507    ) -> Result<Self::Output, anyhow::Error>
508    where
509        S: KeyValueStore + Clone + Send + Sync + 'static,
510        S::Error: Send + Sync;
511}
512
513impl StoreConfig {
514    #[allow(unused_variables)]
515    pub async fn run_with_storage<Job>(
516        self,
517        genesis_config: &GenesisConfig,
518        wasm_runtime: Option<WasmRuntime>,
519        job: Job,
520    ) -> Result<Job::Output, anyhow::Error>
521    where
522        Job: Runnable,
523    {
524        match self {
525            StoreConfig::Memory { config, namespace } => {
526                let store_config = MemoryStoreConfig::new(config.common_config.max_stream_queries);
527                let mut storage = DbStorage::<MemoryStore, _>::maybe_create_and_connect(
528                    &store_config,
529                    &namespace,
530                    wasm_runtime,
531                )
532                .await?;
533                // Memory storage must be initialized every time.
534                genesis_config.initialize_storage(&mut storage).await?;
535                Ok(job.run(storage).await)
536            }
537            #[cfg(feature = "storage-service")]
538            StoreConfig::Service { config, namespace } => {
539                let storage =
540                    DbStorage::<ServiceStoreClient, _>::connect(&config, &namespace, wasm_runtime)
541                        .await?;
542                Ok(job.run(storage).await)
543            }
544            #[cfg(feature = "rocksdb")]
545            StoreConfig::RocksDb { config, namespace } => {
546                let storage =
547                    DbStorage::<RocksDbStore, _>::connect(&config, &namespace, wasm_runtime)
548                        .await?;
549                Ok(job.run(storage).await)
550            }
551            #[cfg(feature = "dynamodb")]
552            StoreConfig::DynamoDb { config, namespace } => {
553                let storage =
554                    DbStorage::<DynamoDbStore, _>::connect(&config, &namespace, wasm_runtime)
555                        .await?;
556                Ok(job.run(storage).await)
557            }
558            #[cfg(feature = "scylladb")]
559            StoreConfig::ScyllaDb { config, namespace } => {
560                let storage =
561                    DbStorage::<ScyllaDbStore, _>::connect(&config, &namespace, wasm_runtime)
562                        .await?;
563                Ok(job.run(storage).await)
564            }
565            #[cfg(all(feature = "rocksdb", feature = "scylladb"))]
566            StoreConfig::DualRocksDbScyllaDb { config, namespace } => {
567                let storage = DbStorage::<
568                    DualStore<RocksDbStore, ScyllaDbStore, ChainStatesFirstAssignment>,
569                    _,
570                >::connect(&config, &namespace, wasm_runtime)
571                .await?;
572                Ok(job.run(storage).await)
573            }
574        }
575    }
576
577    #[allow(unused_variables)]
578    pub async fn run_with_store<Job>(self, job: Job) -> Result<Job::Output, anyhow::Error>
579    where
580        Job: RunnableWithStore,
581    {
582        match self {
583            StoreConfig::Memory { .. } => {
584                Err(anyhow!("Cannot run admin operations on the memory store"))
585            }
586            #[cfg(feature = "storage-service")]
587            StoreConfig::Service { config, namespace } => {
588                Ok(job.run::<ServiceStoreClient>(config, namespace).await?)
589            }
590            #[cfg(feature = "rocksdb")]
591            StoreConfig::RocksDb { config, namespace } => {
592                Ok(job.run::<RocksDbStore>(config, namespace).await?)
593            }
594            #[cfg(feature = "dynamodb")]
595            StoreConfig::DynamoDb { config, namespace } => {
596                Ok(job.run::<DynamoDbStore>(config, namespace).await?)
597            }
598            #[cfg(feature = "scylladb")]
599            StoreConfig::ScyllaDb { config, namespace } => {
600                Ok(job.run::<ScyllaDbStore>(config, namespace).await?)
601            }
602            #[cfg(all(feature = "rocksdb", feature = "scylladb"))]
603            StoreConfig::DualRocksDbScyllaDb { config, namespace } => Ok(job
604                .run::<DualStore<RocksDbStore, ScyllaDbStore, ChainStatesFirstAssignment>>(
605                    config, namespace,
606                )
607                .await?),
608        }
609    }
610
611    pub async fn initialize(self, config: &GenesisConfig) -> Result<(), anyhow::Error> {
612        self.run_with_store(InitializeStorageJob(config)).await
613    }
614}
615
616struct InitializeStorageJob<'a>(&'a GenesisConfig);
617
618#[async_trait]
619impl RunnableWithStore for InitializeStorageJob<'_> {
620    type Output = ();
621
622    async fn run<S>(
623        self,
624        config: S::Config,
625        namespace: String,
626    ) -> Result<Self::Output, anyhow::Error>
627    where
628        S: KeyValueStore + Clone + Send + Sync + 'static,
629        S::Error: Send + Sync,
630    {
631        let mut storage =
632            DbStorage::<S, _>::maybe_create_and_connect(&config, &namespace, None).await?;
633        self.0.initialize_storage(&mut storage).await?;
634        Ok(())
635    }
636}
637
638#[test]
639fn test_memory_storage_config_from_str() {
640    assert_eq!(
641        StorageConfigNamespace::from_str("memory:").unwrap(),
642        StorageConfigNamespace {
643            storage_config: StorageConfig::Memory,
644            namespace: "".into()
645        }
646    );
647    assert_eq!(
648        StorageConfigNamespace::from_str("memory").unwrap(),
649        StorageConfigNamespace {
650            storage_config: StorageConfig::Memory,
651            namespace: DEFAULT_NAMESPACE.into()
652        }
653    );
654    assert_eq!(
655        StorageConfigNamespace::from_str("memory:table_linera").unwrap(),
656        StorageConfigNamespace {
657            storage_config: StorageConfig::Memory,
658            namespace: DEFAULT_NAMESPACE.into()
659        }
660    );
661}
662
663#[cfg(feature = "storage-service")]
664#[test]
665fn test_shared_store_config_from_str() {
666    assert_eq!(
667        StorageConfigNamespace::from_str("service:tcp:127.0.0.1:8942:linera").unwrap(),
668        StorageConfigNamespace {
669            storage_config: StorageConfig::Service {
670                endpoint: "127.0.0.1:8942".to_string()
671            },
672            namespace: "linera".into()
673        }
674    );
675    assert!(StorageConfigNamespace::from_str("service:tcp:127.0.0.1:8942").is_err());
676    assert!(StorageConfigNamespace::from_str("service:tcp:127.0.0.1:linera").is_err());
677}
678
679#[cfg(feature = "rocksdb")]
680#[test]
681fn test_rocks_db_storage_config_from_str() {
682    assert!(StorageConfigNamespace::from_str("rocksdb_foo.db").is_err());
683    assert_eq!(
684        StorageConfigNamespace::from_str("rocksdb:foo.db").unwrap(),
685        StorageConfigNamespace {
686            storage_config: StorageConfig::RocksDb {
687                path: "foo.db".into(),
688                spawn_mode: RocksDbSpawnMode::SpawnBlocking,
689            },
690            namespace: DEFAULT_NAMESPACE.to_string()
691        }
692    );
693    assert_eq!(
694        StorageConfigNamespace::from_str("rocksdb:foo.db:block_in_place").unwrap(),
695        StorageConfigNamespace {
696            storage_config: StorageConfig::RocksDb {
697                path: "foo.db".into(),
698                spawn_mode: RocksDbSpawnMode::BlockInPlace,
699            },
700            namespace: DEFAULT_NAMESPACE.to_string()
701        }
702    );
703    assert_eq!(
704        StorageConfigNamespace::from_str("rocksdb:foo.db:block_in_place:chosen_namespace").unwrap(),
705        StorageConfigNamespace {
706            storage_config: StorageConfig::RocksDb {
707                path: "foo.db".into(),
708                spawn_mode: RocksDbSpawnMode::BlockInPlace,
709            },
710            namespace: "chosen_namespace".into()
711        }
712    );
713}
714
715#[cfg(feature = "dynamodb")]
716#[test]
717fn test_aws_storage_config_from_str() {
718    assert_eq!(
719        StorageConfigNamespace::from_str("dynamodb:table").unwrap(),
720        StorageConfigNamespace {
721            storage_config: StorageConfig::DynamoDb {
722                use_dynamodb_local: false
723            },
724            namespace: "table".to_string()
725        }
726    );
727    assert_eq!(
728        StorageConfigNamespace::from_str("dynamodb:table:env").unwrap(),
729        StorageConfigNamespace {
730            storage_config: StorageConfig::DynamoDb {
731                use_dynamodb_local: false
732            },
733            namespace: "table".to_string()
734        }
735    );
736    assert_eq!(
737        StorageConfigNamespace::from_str("dynamodb:table:dynamodb_local").unwrap(),
738        StorageConfigNamespace {
739            storage_config: StorageConfig::DynamoDb {
740                use_dynamodb_local: true
741            },
742            namespace: "table".to_string()
743        }
744    );
745    assert!(StorageConfigNamespace::from_str("dynamodb").is_err());
746    assert!(StorageConfigNamespace::from_str("dynamodb:").is_err());
747    assert!(StorageConfigNamespace::from_str("dynamodb:1").is_err());
748    assert!(StorageConfigNamespace::from_str("dynamodb:wrong:endpoint").is_err());
749}
750
751#[cfg(feature = "scylladb")]
752#[test]
753fn test_scylla_db_storage_config_from_str() {
754    assert_eq!(
755        StorageConfigNamespace::from_str("scylladb:").unwrap(),
756        StorageConfigNamespace {
757            storage_config: StorageConfig::ScyllaDb {
758                uri: "localhost:9042".to_string()
759            },
760            namespace: DEFAULT_NAMESPACE.to_string()
761        }
762    );
763    assert_eq!(
764        StorageConfigNamespace::from_str("scylladb:tcp:db_hostname:230:table_other_storage")
765            .unwrap(),
766        StorageConfigNamespace {
767            storage_config: StorageConfig::ScyllaDb {
768                uri: "db_hostname:230".to_string()
769            },
770            namespace: "table_other_storage".to_string()
771        }
772    );
773    assert_eq!(
774        StorageConfigNamespace::from_str("scylladb:tcp:db_hostname:230").unwrap(),
775        StorageConfigNamespace {
776            storage_config: StorageConfig::ScyllaDb {
777                uri: "db_hostname:230".to_string()
778            },
779            namespace: DEFAULT_NAMESPACE.to_string()
780        }
781    );
782    assert!(StorageConfigNamespace::from_str("scylladb:-10").is_err());
783    assert!(StorageConfigNamespace::from_str("scylladb:70000").is_err());
784    assert!(StorageConfigNamespace::from_str("scylladb:230:234").is_err());
785    assert!(StorageConfigNamespace::from_str("scylladb:tcp:address1").is_err());
786    assert!(StorageConfigNamespace::from_str("scylladb:tcp:address1:tcp:/address2").is_err());
787    assert!(StorageConfigNamespace::from_str("scylladb:wrong").is_err());
788}