Skip to main content

reddb_server/storage/
layout.rs

1//! Pure tiered storage layout derivation.
2//!
3//! This module maps a configured database path and layout preset to
4//! deterministic sidecar paths. Constructors and accessors perform no I/O;
5//! callers opt into directory creation through [`TieredLayoutPaths::ensure_dirs`].
6
7use std::fs;
8use std::io;
9use std::path::{Path, PathBuf};
10
11use serde::{Deserialize, Serialize};
12
13/// Storage layout preset for future tier-aware startup integration.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "kebab-case")]
16pub enum StorageLayout {
17    /// Keep only required durability sidecars next to the data file.
18    Minimal,
19    /// Default balance: shared support directory for durable metadata.
20    Standard,
21    /// Put hot write/read artifacts into dedicated directories.
22    Performance,
23    /// Enable every known dedicated tier directory.
24    Max,
25}
26
27impl Default for StorageLayout {
28    fn default() -> Self {
29        Self::Standard
30    }
31}
32
33/// Optional per-toggle override applied after preset expansion.
34#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(default)]
36pub struct LayoutOverrides {
37    pub dedicated_wal_dir: Option<bool>,
38    pub dedicated_index_dir: Option<bool>,
39    pub dedicated_cache_dir: Option<bool>,
40    pub dedicated_snapshot_dir: Option<bool>,
41    pub dedicated_blob_dir: Option<bool>,
42    pub dedicated_temp_dir: Option<bool>,
43    pub dedicated_metrics_dir: Option<bool>,
44}
45
46/// Fully expanded layout toggles after applying a preset and overrides.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48pub struct LayoutToggles {
49    pub dedicated_wal_dir: bool,
50    pub dedicated_index_dir: bool,
51    pub dedicated_cache_dir: bool,
52    pub dedicated_snapshot_dir: bool,
53    pub dedicated_blob_dir: bool,
54    pub dedicated_temp_dir: bool,
55    pub dedicated_metrics_dir: bool,
56}
57
58impl StorageLayout {
59    pub fn expand(self, overrides: LayoutOverrides) -> LayoutToggles {
60        let mut toggles = match self {
61            Self::Minimal => LayoutToggles {
62                dedicated_wal_dir: false,
63                dedicated_index_dir: false,
64                dedicated_cache_dir: false,
65                dedicated_snapshot_dir: false,
66                dedicated_blob_dir: false,
67                dedicated_temp_dir: false,
68                dedicated_metrics_dir: false,
69            },
70            Self::Standard => LayoutToggles {
71                dedicated_wal_dir: false,
72                dedicated_index_dir: true,
73                dedicated_cache_dir: false,
74                dedicated_snapshot_dir: true,
75                dedicated_blob_dir: false,
76                dedicated_temp_dir: false,
77                dedicated_metrics_dir: false,
78            },
79            Self::Performance => LayoutToggles {
80                dedicated_wal_dir: true,
81                dedicated_index_dir: true,
82                dedicated_cache_dir: true,
83                dedicated_snapshot_dir: true,
84                dedicated_blob_dir: true,
85                dedicated_temp_dir: false,
86                dedicated_metrics_dir: false,
87            },
88            Self::Max => LayoutToggles {
89                dedicated_wal_dir: true,
90                dedicated_index_dir: true,
91                dedicated_cache_dir: true,
92                dedicated_snapshot_dir: true,
93                dedicated_blob_dir: true,
94                dedicated_temp_dir: true,
95                dedicated_metrics_dir: true,
96            },
97        };
98
99        if let Some(value) = overrides.dedicated_wal_dir {
100            toggles.dedicated_wal_dir = value;
101        }
102        if let Some(value) = overrides.dedicated_index_dir {
103            toggles.dedicated_index_dir = value;
104        }
105        if let Some(value) = overrides.dedicated_cache_dir {
106            toggles.dedicated_cache_dir = value;
107        }
108        if let Some(value) = overrides.dedicated_snapshot_dir {
109            toggles.dedicated_snapshot_dir = value;
110        }
111        if let Some(value) = overrides.dedicated_blob_dir {
112            toggles.dedicated_blob_dir = value;
113        }
114        if let Some(value) = overrides.dedicated_temp_dir {
115            toggles.dedicated_temp_dir = value;
116        }
117        if let Some(value) = overrides.dedicated_metrics_dir {
118            toggles.dedicated_metrics_dir = value;
119        }
120
121        toggles
122    }
123}
124
125/// Deterministic paths derived from a data file and expanded layout.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct TieredLayoutPaths {
128    pub data_file: PathBuf,
129    pub support_dir: PathBuf,
130    pub wal_file: PathBuf,
131    pub logical_wal_file: PathBuf,
132    pub temp_file: PathBuf,
133    pub snapshot_dir: Option<PathBuf>,
134    pub index_dir: Option<PathBuf>,
135    pub cache_dir: Option<PathBuf>,
136    pub blob_dir: Option<PathBuf>,
137    pub metrics_dir: Option<PathBuf>,
138    pub toggles: LayoutToggles,
139}
140
141impl TieredLayoutPaths {
142    pub fn new(
143        data_path: &Path,
144        layout: StorageLayout,
145        overrides: LayoutOverrides,
146    ) -> TieredLayoutPaths {
147        let toggles = layout.expand(overrides);
148        let data_file = data_path.to_path_buf();
149        let support_dir = sibling_path(data_path, &format!("{}.red", file_name(data_path)));
150
151        let wal_file = if toggles.dedicated_wal_dir {
152            support_dir
153                .join("wal")
154                .join(sidecar_file_name(data_path, "rdb-uwal"))
155        } else {
156            data_path.with_extension("rdb-uwal")
157        };
158        let logical_wal_file = if toggles.dedicated_wal_dir {
159            support_dir
160                .join("wal")
161                .join(format!("{}.logical.wal", file_name(data_path)))
162        } else {
163            sibling_path(data_path, &format!("{}.logical.wal", file_name(data_path)))
164        };
165        let temp_file = if toggles.dedicated_temp_dir {
166            support_dir
167                .join("tmp")
168                .join(sidecar_file_name(data_path, "rdb-tmp"))
169        } else {
170            data_path.with_extension("rdb-tmp")
171        };
172
173        TieredLayoutPaths {
174            data_file,
175            support_dir: support_dir.clone(),
176            wal_file,
177            logical_wal_file,
178            temp_file,
179            snapshot_dir: toggles
180                .dedicated_snapshot_dir
181                .then(|| support_dir.join("snapshots")),
182            index_dir: toggles
183                .dedicated_index_dir
184                .then(|| support_dir.join("indexes")),
185            cache_dir: toggles
186                .dedicated_cache_dir
187                .then(|| support_dir.join("cache")),
188            blob_dir: toggles
189                .dedicated_blob_dir
190                .then(|| support_dir.join("blobs")),
191            metrics_dir: toggles
192                .dedicated_metrics_dir
193                .then(|| support_dir.join("metrics")),
194            toggles,
195        }
196    }
197
198    pub fn dirs_to_create(&self) -> Vec<PathBuf> {
199        let mut dirs = Vec::new();
200        push_parent(&mut dirs, &self.data_file);
201        push_parent(&mut dirs, &self.wal_file);
202        push_parent(&mut dirs, &self.logical_wal_file);
203        push_parent(&mut dirs, &self.temp_file);
204        push_optional(&mut dirs, self.snapshot_dir.as_ref());
205        push_optional(&mut dirs, self.index_dir.as_ref());
206        push_optional(&mut dirs, self.cache_dir.as_ref());
207        push_optional(&mut dirs, self.blob_dir.as_ref());
208        push_optional(&mut dirs, self.metrics_dir.as_ref());
209        dirs.sort();
210        dirs.dedup();
211        dirs
212    }
213
214    pub fn ensure_dirs(&self) -> io::Result<()> {
215        for dir in self.dirs_to_create() {
216            fs::create_dir_all(dir)?;
217        }
218        Ok(())
219    }
220}
221
222fn file_name(path: &Path) -> String {
223    path.file_name()
224        .and_then(|name| name.to_str())
225        .unwrap_or("data.rdb")
226        .to_string()
227}
228
229fn sibling_path(path: &Path, file_name: &str) -> PathBuf {
230    match path.parent() {
231        Some(parent) if !parent.as_os_str().is_empty() => parent.join(file_name),
232        _ => PathBuf::from(file_name),
233    }
234}
235
236fn sidecar_file_name(path: &Path, extension: &str) -> String {
237    path.with_extension(extension)
238        .file_name()
239        .and_then(|name| name.to_str())
240        .unwrap_or("data.rdb")
241        .to_string()
242}
243
244fn push_parent(dirs: &mut Vec<PathBuf>, path: &Path) {
245    if let Some(parent) = path.parent() {
246        if !parent.as_os_str().is_empty() {
247            dirs.push(parent.to_path_buf());
248        }
249    }
250}
251
252fn push_optional(dirs: &mut Vec<PathBuf>, path: Option<&PathBuf>) {
253    if let Some(path) = path {
254        dirs.push(path.clone());
255    }
256}