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 foundationdb::options::DatabaseOption;
20use mz_ore::url::SensitiveUrl;
21
22/// Re-export the `foundationdb` crate for convenience.
23pub use foundationdb::*;
24
25/// Default transaction timeout, in milliseconds, applied to a [`Database`] in
26/// tests via [`set_test_transaction_timeout`].
27///
28/// Chosen well below the test harness's 180s termination so that an unavailable
29/// or stalled server surfaces a `transaction_timed_out` error the test can
30/// report and retry on, rather than hanging until the harness kills the process.
31const TEST_TRANSACTION_TIMEOUT_MS: i32 = 60_000;
32
33/// Sets a default transaction timeout on `db` for use in tests.
34///
35/// Production code intentionally does not bound transactions here; that is a
36/// separate product decision. Tests call this so that a FoundationDB server that
37/// becomes unresponsive (for example under heavy parallel CI load) fails the
38/// affected operation promptly instead of blocking forever.
39pub fn set_test_transaction_timeout(db: &Database) {
40    db.set_option(DatabaseOption::TransactionTimeout(
41        TEST_TRANSACTION_TIMEOUT_MS,
42    ))
43    .expect("setting transaction timeout option");
44}
45
46/// FoundationDB network handle.
47/// The first element is `Some` if the network is initialized.
48/// The second element is `true` if the network has ever been initialized.
49static FDB_NETWORK: Mutex<(Option<NetworkAutoStop>, bool)> = Mutex::new((None, false));
50
51/// Initialize the FoundationDB network.
52///
53/// This function is safe to call multiple times - only the first call will
54/// actually initialize the network, subsequent calls return immediately.
55///
56/// The FoundationDB network can be booted once per process and can never be
57/// restarted: after [`shutdown_network()`], any subsequent call to this function
58/// panics.
59///
60/// Before the process exits, drop all `Database` and transaction handles and then
61/// call [`shutdown_network()`]. Skipping the shutdown can segfault the
62/// FoundationDB client during teardown; calling it while a handle is still alive
63/// can instead block on the network thread join.
64pub fn init_network() {
65    let mut guard = FDB_NETWORK.lock().expect("mutex poisoned");
66    if guard.0.is_none() {
67        if guard.1 {
68            panic!("attempted to re-initialize FoundationDB network after shutdown");
69        }
70        // SAFETY: The `foundationdb::boot()` call is unsafe because it must only
71        // be called once per process. We use a mutex to ensure this guarantee
72        // is upheld - subsequent calls to `init_network()` will see `guard.is_some()`
73        // and return early without calling `boot()` again.
74        guard.0 = Some(unsafe { boot() });
75        guard.1 = true;
76    }
77}
78
79/// Shut down the FoundationDB network.
80///
81/// Call this once, after dropping all `Database` and transaction handles, before
82/// the process exits. Not stopping the network can segfault the client during
83/// teardown. The network can never be restarted afterwards: any subsequent call
84/// to [`init_network()`] will panic. Stopping the network joins the network
85/// thread, which can block indefinitely if a handle or in-flight transaction is
86/// still alive.
87pub fn shutdown_network() {
88    let mut guard = FDB_NETWORK.lock().expect("mutex poisoned");
89    if guard.0.is_some() {
90        guard.0 = None;
91    }
92}
93
94/// Configuration parsed from a FoundationDB URL.
95///
96/// FoundationDB URLs have the format:
97/// `foundationdb:?prefix=<prefix>`
98///
99/// The cluster file is determined by FoundationDB's standard discovery mechanism:
100/// 1. The `FDB_CLUSTER_FILE` environment variable
101/// 2. The default path `/etc/foundationdb/fdb.cluster`
102///
103/// This ensures all components using FoundationDB connect to the same cluster.
104#[derive(Clone, Debug)]
105pub struct FdbConfig {
106    /// The prefix path components for the directory layer.
107    pub prefix: Vec<String>,
108}
109
110impl FdbConfig {
111    /// Parse a FoundationDB URL into configuration.
112    ///
113    /// # URL Format
114    ///
115    /// The URL format is: `foundationdb:?prefix=<prefix>`
116    ///
117    /// - The scheme must be `foundationdb`
118    /// - The `prefix` query parameter specifies the directory prefix to use,
119    ///   with path components separated by `/`
120    ///
121    /// The cluster file is NOT specified in the URL. Instead, FoundationDB's
122    /// standard discovery mechanism is used (via `FDB_CLUSTER_FILE` env var
123    /// or the default `/etc/foundationdb/fdb.cluster`).
124    ///
125    /// # Examples
126    ///
127    /// ```ignore
128    /// // Use default cluster file with a prefix
129    /// let url = "foundationdb:?prefix=my_app/consensus";
130    /// ```
131    pub fn parse(url: &SensitiveUrl) -> Result<Self, anyhow::Error> {
132        let mut prefix = None;
133
134        let mut legacy_prefix = None;
135
136        for (key, value) in url.query_pairs() {
137            match &*key {
138                "prefix" => {
139                    prefix = Some(value.split('/').map(|s| s.to_owned()).collect());
140                }
141                "options" => {
142                    tracing::warn!(
143                        "FoundationDB URL 'options' parameter is deprecated; use 'prefix' instead"
144                    );
145                    // Parse a string like `--search_path=<path>` to extract legacy prefix.
146                    if let Some(stripped) = value.strip_prefix("--search_path=") {
147                        legacy_prefix = Some(stripped.split('/').map(|s| s.to_owned()).collect());
148                    } else {
149                        anyhow::bail!("unrecognized FoundationDB URL options parameter: {value}");
150                    }
151                }
152                key => {
153                    anyhow::bail!("unrecognized FoundationDB URL query parameter: {key}={value}");
154                }
155            }
156        }
157
158        if prefix.is_some() && legacy_prefix.is_some() {
159            anyhow::bail!(
160                "cannot specify both 'prefix' and legacy 'options' parameters in FoundationDB URL"
161            );
162        }
163
164        Ok(FdbConfig {
165            prefix: prefix.or(legacy_prefix).unwrap_or_default(),
166        })
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    use std::str::FromStr;
175
176    #[mz_ore::test]
177    fn test_parse_url_with_prefix() {
178        let url = SensitiveUrl::from_str("foundationdb:?prefix=my_app/consensus").unwrap();
179        let config = FdbConfig::parse(&url).unwrap();
180        assert_eq!(config.prefix, vec!["my_app", "consensus"]);
181    }
182
183    #[mz_ore::test]
184    fn test_parse_url_with_nested_prefix() {
185        let url = SensitiveUrl::from_str("foundationdb:?prefix=a/b/c/d").unwrap();
186        let config = FdbConfig::parse(&url).unwrap();
187        assert_eq!(config.prefix, vec!["a", "b", "c", "d"]);
188    }
189
190    #[mz_ore::test]
191    fn test_parse_url_no_prefix() {
192        let url = SensitiveUrl::from_str("foundationdb:").unwrap();
193        let config = FdbConfig::parse(&url).unwrap();
194        assert!(config.prefix.is_empty());
195    }
196
197    #[mz_ore::test]
198    fn test_parse_url_invalid_query_param() {
199        let url = SensitiveUrl::from_str("foundationdb:?unknown=value").unwrap();
200        assert!(FdbConfig::parse(&url).is_err());
201    }
202}