dbx_core/engine/
feature_flags.rs1use crate::error::DbxResult;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::env;
12use std::fs;
13use std::path::PathBuf;
14use std::sync::{Arc, RwLock};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub enum Feature {
19 BinarySerialization,
21
22 MultiThreading,
24
25 MvccExtension,
27
28 QueryPlanCache,
30
31 ParallelQuery,
33
34 ParallelWal,
36
37 ParallelCheckpoint,
39
40 SchemaVersioning,
42
43 IndexVersioning,
45}
46
47impl Feature {
48 pub fn as_str(&self) -> &'static str {
50 match self {
51 Feature::BinarySerialization => "binary_serialization",
52 Feature::MultiThreading => "multi_threading",
53 Feature::MvccExtension => "mvcc_extension",
54 Feature::QueryPlanCache => "query_plan_cache",
55 Feature::ParallelQuery => "parallel_query",
56 Feature::ParallelWal => "parallel_wal",
57 Feature::ParallelCheckpoint => "parallel_checkpoint",
58 Feature::SchemaVersioning => "schema_versioning",
59 Feature::IndexVersioning => "index_versioning",
60 }
61 }
62
63 pub fn parse_feature(s: &str) -> Option<Self> {
65 match s {
66 "binary_serialization" => Some(Feature::BinarySerialization),
67 "multi_threading" => Some(Feature::MultiThreading),
68 "mvcc_extension" => Some(Feature::MvccExtension),
69 "query_plan_cache" => Some(Feature::QueryPlanCache),
70 "parallel_query" => Some(Feature::ParallelQuery),
71 "parallel_wal" => Some(Feature::ParallelWal),
72 "parallel_checkpoint" => Some(Feature::ParallelCheckpoint),
73 "schema_versioning" => Some(Feature::SchemaVersioning),
74 "index_versioning" => Some(Feature::IndexVersioning),
75 _ => None,
76 }
77 }
78
79 pub fn env_var_name(&self) -> String {
81 format!("DBX_FEATURE_{}", self.as_str().to_uppercase())
82 }
83}
84
85pub struct FeatureFlags {
87 flags: Arc<RwLock<HashMap<Feature, bool>>>,
89
90 persistence_path: Option<PathBuf>,
92}
93
94impl FeatureFlags {
95 pub fn new() -> Self {
97 Self {
98 flags: Arc::new(RwLock::new(HashMap::new())),
99 persistence_path: None,
100 }
101 }
102
103 pub fn with_persistence(mut self, path: PathBuf) -> Self {
105 self.persistence_path = Some(path);
106 self
107 }
108
109 pub fn enable(&self, feature: Feature) {
111 self.flags.write().unwrap().insert(feature, true);
112 }
113
114 pub fn disable(&self, feature: Feature) {
116 self.flags.write().unwrap().insert(feature, false);
117 }
118
119 pub fn toggle(&self, feature: Feature, enabled: bool) {
121 self.flags.write().unwrap().insert(feature, enabled);
122 }
123
124 pub fn is_enabled(&self, feature: Feature) -> bool {
126 self.flags
127 .read()
128 .unwrap()
129 .get(&feature)
130 .copied()
131 .unwrap_or(false)
132 }
133
134 pub fn load_from_env(&self) {
136 let all_features = [
137 Feature::BinarySerialization,
138 Feature::MultiThreading,
139 Feature::MvccExtension,
140 Feature::QueryPlanCache,
141 Feature::ParallelQuery,
142 Feature::ParallelWal,
143 Feature::ParallelCheckpoint,
144 Feature::SchemaVersioning,
145 Feature::IndexVersioning,
146 ];
147
148 for feature in &all_features {
149 let env_var = feature.env_var_name();
150 if let Ok(value) = env::var(&env_var) {
151 let enabled = value.to_lowercase() == "true" || value == "1";
152 self.toggle(*feature, enabled);
153 }
154 }
155 }
156
157 pub fn load_from_file(&self) -> DbxResult<()> {
159 if let Some(path) = self.persistence_path.as_ref().filter(|p| p.exists()) {
160 let json = fs::read_to_string(path)?;
161 let loaded: HashMap<String, bool> = serde_json::from_str(&json)?;
162
163 let mut flags = self.flags.write().unwrap();
164 for (key, value) in loaded {
165 if let Some(feature) = Feature::parse_feature(&key) {
166 flags.insert(feature, value);
167 }
168 }
169 }
170 Ok(())
171 }
172
173 pub fn save_to_file(&self) -> DbxResult<()> {
175 if let Some(path) = &self.persistence_path {
176 let flags = self.flags.read().unwrap();
177
178 let serializable: HashMap<String, bool> = flags
180 .iter()
181 .map(|(k, v)| (k.as_str().to_string(), *v))
182 .collect();
183
184 let json = serde_json::to_string_pretty(&serializable)?;
185
186 if let Some(parent) = path.parent() {
188 fs::create_dir_all(parent)?;
189 }
190
191 fs::write(path, json)?;
192 }
193 Ok(())
194 }
195
196 pub fn all(&self) -> HashMap<Feature, bool> {
198 self.flags.read().unwrap().clone()
199 }
200
201 pub fn reset(&self) {
203 self.flags.write().unwrap().clear();
204 }
205}
206
207impl Default for FeatureFlags {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use std::env;
217
218 #[test]
221 fn test_feature_flag_enable_disable() {
222 let flags = FeatureFlags::new();
223
224 assert!(!flags.is_enabled(Feature::BinarySerialization));
226
227 flags.enable(Feature::BinarySerialization);
229 assert!(flags.is_enabled(Feature::BinarySerialization));
230
231 flags.disable(Feature::BinarySerialization);
233 assert!(!flags.is_enabled(Feature::BinarySerialization));
234 }
235
236 #[test]
237 fn test_feature_flag_toggle() {
238 let flags = FeatureFlags::new();
239
240 flags.toggle(Feature::MultiThreading, true);
241 assert!(flags.is_enabled(Feature::MultiThreading));
242
243 flags.toggle(Feature::MultiThreading, false);
244 assert!(!flags.is_enabled(Feature::MultiThreading));
245 }
246
247 #[test]
248 fn test_feature_flag_persistence() {
249 let temp_path = PathBuf::from("target/test_feature_flags.json");
250 let flags = FeatureFlags::new().with_persistence(temp_path.clone());
251
252 flags.enable(Feature::BinarySerialization);
254 flags.enable(Feature::MultiThreading);
255 flags.disable(Feature::MvccExtension);
256
257 flags.save_to_file().unwrap();
259
260 let flags2 = FeatureFlags::new().with_persistence(temp_path.clone());
262 flags2.load_from_file().unwrap();
263
264 assert!(flags2.is_enabled(Feature::BinarySerialization));
266 assert!(flags2.is_enabled(Feature::MultiThreading));
267 assert!(!flags2.is_enabled(Feature::MvccExtension));
268
269 let _ = fs::remove_file(temp_path);
271 }
272
273 #[test]
274 fn test_feature_flag_env_var() {
275 let flags = FeatureFlags::new();
276
277 unsafe {
279 env::set_var("DBX_FEATURE_BINARY_SERIALIZATION", "true");
280 env::set_var("DBX_FEATURE_MULTI_THREADING", "1");
281 env::set_var("DBX_FEATURE_MVCC_EXTENSION", "false");
282 }
283
284 flags.load_from_env();
286
287 assert!(flags.is_enabled(Feature::BinarySerialization));
289 assert!(flags.is_enabled(Feature::MultiThreading));
290 assert!(!flags.is_enabled(Feature::MvccExtension));
291
292 unsafe {
294 env::remove_var("DBX_FEATURE_BINARY_SERIALIZATION");
295 env::remove_var("DBX_FEATURE_MULTI_THREADING");
296 env::remove_var("DBX_FEATURE_MVCC_EXTENSION");
297 }
298 }
299
300 #[test]
301 fn test_feature_all_and_reset() {
302 let flags = FeatureFlags::new();
303
304 flags.enable(Feature::BinarySerialization);
305 flags.enable(Feature::MultiThreading);
306
307 let all = flags.all();
309 assert_eq!(all.len(), 2);
310 assert!(
311 all.get(&Feature::BinarySerialization)
312 .copied()
313 .unwrap_or(false)
314 );
315
316 flags.reset();
318 assert_eq!(flags.all().len(), 0);
319 }
320
321 #[test]
322 fn test_feature_from_str() {
323 assert_eq!(
324 Feature::parse_feature("binary_serialization"),
325 Some(Feature::BinarySerialization)
326 );
327 assert_eq!(
328 Feature::parse_feature("multi_threading"),
329 Some(Feature::MultiThreading)
330 );
331 assert_eq!(Feature::parse_feature("invalid"), None);
332 }
333}