Skip to main content

dbx_core/engine/
feature_flags.rs

1// Phase 0.2: Feature Flag 시스템
2//
3// TDD 방식으로 구현:
4// 1. Red: 테스트 작성 (실패)
5// 2. Green: 최소 구현 (통과)
6// 3. Refactor: 코드 개선
7
8use 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/// Feature Flag 정의
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub enum Feature {
19    /// 바이너리 직렬화
20    BinarySerialization,
21
22    /// 멀티스레딩
23    MultiThreading,
24
25    /// MVCC 확장
26    MvccExtension,
27
28    /// 쿼리 플랜 캐시
29    QueryPlanCache,
30
31    /// 병렬 쿼리 실행
32    ParallelQuery,
33
34    /// WAL 병렬 쓰기
35    ParallelWal,
36
37    /// 병렬 체크포인트
38    ParallelCheckpoint,
39
40    /// 스키마 버저닝
41    SchemaVersioning,
42
43    /// 인덱스 버저닝
44    IndexVersioning,
45}
46
47impl Feature {
48    /// Feature를 문자열로 변환
49    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    /// 문자열에서 Feature 파싱
64    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    /// 환경 변수 이름
80    pub fn env_var_name(&self) -> String {
81        format!("DBX_FEATURE_{}", self.as_str().to_uppercase())
82    }
83}
84
85/// Feature Flag 관리자
86pub struct FeatureFlags {
87    /// Feature 상태 (Feature → enabled)
88    flags: Arc<RwLock<HashMap<Feature, bool>>>,
89
90    /// 영속성 파일 경로
91    persistence_path: Option<PathBuf>,
92}
93
94impl FeatureFlags {
95    /// 새 Feature Flag 관리자 생성
96    pub fn new() -> Self {
97        Self {
98            flags: Arc::new(RwLock::new(HashMap::new())),
99            persistence_path: None,
100        }
101    }
102
103    /// 영속성 경로 설정
104    pub fn with_persistence(mut self, path: PathBuf) -> Self {
105        self.persistence_path = Some(path);
106        self
107    }
108
109    /// Feature 활성화
110    pub fn enable(&self, feature: Feature) {
111        self.flags.write().unwrap().insert(feature, true);
112    }
113
114    /// Feature 비활성화
115    pub fn disable(&self, feature: Feature) {
116        self.flags.write().unwrap().insert(feature, false);
117    }
118
119    /// Feature 토글
120    pub fn toggle(&self, feature: Feature, enabled: bool) {
121        self.flags.write().unwrap().insert(feature, enabled);
122    }
123
124    /// Feature 활성화 여부 확인
125    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    /// 환경 변수에서 로드
135    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    /// 파일에서 로드
158    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    /// 파일에 저장
174    pub fn save_to_file(&self) -> DbxResult<()> {
175        if let Some(path) = &self.persistence_path {
176            let flags = self.flags.read().unwrap();
177
178            // Feature → String 변환
179            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            // 디렉토리 생성
187            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    /// 모든 Feature 상태 조회
197    pub fn all(&self) -> HashMap<Feature, bool> {
198        self.flags.read().unwrap().clone()
199    }
200
201    /// 모든 Feature 초기화
202    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    // TDD: Red - 테스트 작성 (실패)
219
220    #[test]
221    fn test_feature_flag_enable_disable() {
222        let flags = FeatureFlags::new();
223
224        // 기본값: 비활성화
225        assert!(!flags.is_enabled(Feature::BinarySerialization));
226
227        // 활성화
228        flags.enable(Feature::BinarySerialization);
229        assert!(flags.is_enabled(Feature::BinarySerialization));
230
231        // 비활성화
232        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        // Feature 설정
253        flags.enable(Feature::BinarySerialization);
254        flags.enable(Feature::MultiThreading);
255        flags.disable(Feature::MvccExtension);
256
257        // 저장
258        flags.save_to_file().unwrap();
259
260        // 새 인스턴스로 로드
261        let flags2 = FeatureFlags::new().with_persistence(temp_path.clone());
262        flags2.load_from_file().unwrap();
263
264        // 확인
265        assert!(flags2.is_enabled(Feature::BinarySerialization));
266        assert!(flags2.is_enabled(Feature::MultiThreading));
267        assert!(!flags2.is_enabled(Feature::MvccExtension));
268
269        // 정리
270        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)
278        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        // 환경 변수에서 로드
285        flags.load_from_env();
286
287        // 확인
288        assert!(flags.is_enabled(Feature::BinarySerialization));
289        assert!(flags.is_enabled(Feature::MultiThreading));
290        assert!(!flags.is_enabled(Feature::MvccExtension));
291
292        // 정리 (unsafe)
293        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        // 모든 Feature 조회
308        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        // 초기화
317        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}