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}