Skip to main content

mz_foundationdb/
lib.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Common FoundationDB utilities for Materialize.
11//!
12//! This crate provides shared functionality for FoundationDB-backed
13//! implementations across Materialize, including network initialization
14//! and URL parsing utilities.
15
16use std::sync::Mutex;
17
18use foundationdb::api::NetworkAutoStop;
19use mz_ore::url::SensitiveUrl;
20
21/// Re-export the `foundationdb` crate for convenience.
22pub use foundationdb::*;
23
24/// FoundationDB network handle.
25/// The first element is `Some` if the network is initialized.
26/// The second element is `true` if the network has ever been initialized.
27static FDB_NETWORK: Mutex<(Option<NetworkAutoStop>, bool)> = Mutex::new((None, false));
28
29/// Initialize the FoundationDB network.
30///
31/// This function is safe to call multiple times - only the first call will
32/// actually initialize the network, subsequent calls return immediately.
33///
34/// After calling `shutdown_network()`, any subsequent calls to this function
35/// will panic.
36///
37/// The user is required to call [`shutdown_network()`] before the process exits to
38/// ensure a clean shutdown of the FoundationDB network. Otherwise, strange memory
39/// corruption issues during shutdown may occur. This is a limitation of the
40/// FoundationDB C API.
41pub fn init_network() {
42    let mut guard = FDB_NETWORK.lock().expect("mutex poisoned");
43    if guard.0.is_none() {
44        if guard.1 {
45            panic!("attempted to re-initialize FoundationDB network after shutdown");
46        }
47        // SAFETY: The `foundationdb::boot()` call is unsafe because it must only
48        // be called once per process. We use a mutex to ensure this guarantee
49        // is upheld - subsequent calls to `init_network()` will see `guard.is_some()`
50        // and return early without calling `boot()` again.
51        guard.0 = Some(unsafe { boot() });
52        guard.1 = true;
53    }
54}
55
56/// Shut down the FoundationDB network.
57///
58/// After calling this function, any subsequent calls to `init_network()` will panic.
59pub fn shutdown_network() {
60    let mut guard = FDB_NETWORK.lock().expect("mutex poisoned");
61    if guard.0.is_some() {
62        guard.0 = None;
63    }
64}
65
66/// Configuration parsed from a FoundationDB URL.
67///
68/// FoundationDB URLs have the format:
69/// `foundationdb:?prefix=<prefix>`
70///
71/// The cluster file is determined by FoundationDB's standard discovery mechanism:
72/// 1. The `FDB_CLUSTER_FILE` environment variable
73/// 2. The default path `/etc/foundationdb/fdb.cluster`
74///
75/// This ensures all components using FoundationDB connect to the same cluster.
76#[derive(Clone, Debug)]
77pub struct FdbConfig {
78    /// The prefix path components for the directory layer.
79    pub prefix: Vec<String>,
80}
81
82impl FdbConfig {
83    /// Parse a FoundationDB URL into configuration.
84    ///
85    /// # URL Format
86    ///
87    /// The URL format is: `foundationdb:?prefix=<prefix>`
88    ///
89    /// - The scheme must be `foundationdb`
90    /// - The `prefix` query parameter specifies the directory prefix to use,
91    ///   with path components separated by `/`
92    ///
93    /// The cluster file is NOT specified in the URL. Instead, FoundationDB's
94    /// standard discovery mechanism is used (via `FDB_CLUSTER_FILE` env var
95    /// or the default `/etc/foundationdb/fdb.cluster`).
96    ///
97    /// # Examples
98    ///
99    /// ```ignore
100    /// // Use default cluster file with a prefix
101    /// let url = "foundationdb:?prefix=my_app/consensus";
102    /// ```
103    pub fn parse(url: &SensitiveUrl) -> Result<Self, anyhow::Error> {
104        let mut prefix = None;
105
106        let mut legacy_prefix = None;
107
108        for (key, value) in url.query_pairs() {
109            match &*key {
110                "prefix" => {
111                    prefix = Some(value.split('/').map(|s| s.to_owned()).collect());
112                }
113                "options" => {
114                    tracing::warn!(
115                        "FoundationDB URL 'options' parameter is deprecated; use 'prefix' instead"
116                    );
117                    // Parse a string like `--search_path=<path>` to extract legacy prefix.
118                    if let Some(stripped) = value.strip_prefix("--search_path=") {
119                        legacy_prefix = Some(stripped.split('/').map(|s| s.to_owned()).collect());
120                    } else {
121                        anyhow::bail!("unrecognized FoundationDB URL options parameter: {value}");
122                    }
123                }
124                key => {
125                    anyhow::bail!("unrecognized FoundationDB URL query parameter: {key}={value}");
126                }
127            }
128        }
129
130        if prefix.is_some() && legacy_prefix.is_some() {
131            anyhow::bail!(
132                "cannot specify both 'prefix' and legacy 'options' parameters in FoundationDB URL"
133            );
134        }
135
136        Ok(FdbConfig {
137            prefix: prefix.or(legacy_prefix).unwrap_or_default(),
138        })
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    use std::str::FromStr;
147
148    #[mz_ore::test]
149    fn test_parse_url_with_prefix() {
150        let url = SensitiveUrl::from_str("foundationdb:?prefix=my_app/consensus").unwrap();
151        let config = FdbConfig::parse(&url).unwrap();
152        assert_eq!(config.prefix, vec!["my_app", "consensus"]);
153    }
154
155    #[mz_ore::test]
156    fn test_parse_url_with_nested_prefix() {
157        let url = SensitiveUrl::from_str("foundationdb:?prefix=a/b/c/d").unwrap();
158        let config = FdbConfig::parse(&url).unwrap();
159        assert_eq!(config.prefix, vec!["a", "b", "c", "d"]);
160    }
161
162    #[mz_ore::test]
163    fn test_parse_url_no_prefix() {
164        let url = SensitiveUrl::from_str("foundationdb:").unwrap();
165        let config = FdbConfig::parse(&url).unwrap();
166        assert!(config.prefix.is_empty());
167    }
168
169    #[mz_ore::test]
170    fn test_parse_url_invalid_query_param() {
171        let url = SensitiveUrl::from_str("foundationdb:?unknown=value").unwrap();
172        assert!(FdbConfig::parse(&url).is_err());
173    }
174}