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, 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    /// Per-log routing overrides. See [`LogRoutingOverrides`].
45    #[serde(default)]
46    pub logs: LogRoutingOverrides,
47}
48
49/// Where a log stream should be written.
50///
51/// `Stderr` is the safe default — operators see lines without files
52/// accumulating in user data directories. `File(path)` is selected
53/// automatically by the `performance` / `max` tiers under
54/// `<dbname>.rdb.red/logs/` and can be overridden to an arbitrary path.
55/// `Syslog` is recognised by the routing layer; the actual syslog
56/// sink integration lives outside this pure layout module.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "kebab-case", tag = "kind", content = "path")]
59pub enum LogDestination {
60    Stderr,
61    File(PathBuf),
62    Syslog,
63}
64
65impl LogDestination {
66    /// Human-readable destination tag for `reddb status` / diagnostics.
67    pub fn describe(&self) -> String {
68        match self {
69            Self::Stderr => "stderr".to_string(),
70            Self::Syslog => "syslog".to_string(),
71            Self::File(path) => format!("file:{}", path.display()),
72        }
73    }
74
75    /// Returns the file path if this destination writes to a file.
76    pub fn file_path(&self) -> Option<&Path> {
77        match self {
78            Self::File(path) => Some(path.as_path()),
79            _ => None,
80        }
81    }
82}
83
84/// Per-log destination overrides. `None` keeps the tier default.
85#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(default)]
87pub struct LogRoutingOverrides {
88    pub audit_log: Option<LogDestination>,
89    pub slow_log: Option<LogDestination>,
90}
91
92/// Fully expanded layout toggles after applying a preset and overrides.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94pub struct LayoutToggles {
95    pub dedicated_wal_dir: bool,
96    pub dedicated_index_dir: bool,
97    pub dedicated_cache_dir: bool,
98    pub dedicated_snapshot_dir: bool,
99    pub dedicated_blob_dir: bool,
100    pub dedicated_temp_dir: bool,
101    pub dedicated_metrics_dir: bool,
102}
103
104impl StorageLayout {
105    /// Default audit-log destination for this tier, before any override.
106    /// `Performance` / `Max` write to a file under `<support_dir>/logs/`;
107    /// `Standard` / `Minimal` default to stderr so no log files land in
108    /// the user's data directory.
109    pub fn default_audit_log_in(self, support_dir: &Path) -> LogDestination {
110        match self {
111            Self::Performance | Self::Max => {
112                LogDestination::File(support_dir.join("logs").join("audit.log"))
113            }
114            Self::Minimal | Self::Standard => LogDestination::Stderr,
115        }
116    }
117
118    /// Default slow-query log destination for this tier. See
119    /// [`Self::default_audit_log_in`].
120    pub fn default_slow_log_in(self, support_dir: &Path) -> LogDestination {
121        match self {
122            Self::Performance | Self::Max => {
123                LogDestination::File(support_dir.join("logs").join("slow.log"))
124            }
125            Self::Minimal | Self::Standard => LogDestination::Stderr,
126        }
127    }
128
129    pub fn expand(self, overrides: &LayoutOverrides) -> LayoutToggles {
130        let mut toggles = match self {
131            Self::Minimal => LayoutToggles {
132                dedicated_wal_dir: false,
133                dedicated_index_dir: false,
134                dedicated_cache_dir: false,
135                dedicated_snapshot_dir: false,
136                dedicated_blob_dir: false,
137                dedicated_temp_dir: false,
138                dedicated_metrics_dir: false,
139            },
140            Self::Standard => LayoutToggles {
141                dedicated_wal_dir: false,
142                dedicated_index_dir: true,
143                dedicated_cache_dir: false,
144                dedicated_snapshot_dir: true,
145                dedicated_blob_dir: false,
146                dedicated_temp_dir: false,
147                dedicated_metrics_dir: false,
148            },
149            Self::Performance => LayoutToggles {
150                dedicated_wal_dir: true,
151                dedicated_index_dir: true,
152                dedicated_cache_dir: true,
153                dedicated_snapshot_dir: true,
154                dedicated_blob_dir: true,
155                dedicated_temp_dir: false,
156                dedicated_metrics_dir: false,
157            },
158            Self::Max => LayoutToggles {
159                dedicated_wal_dir: true,
160                dedicated_index_dir: true,
161                dedicated_cache_dir: true,
162                dedicated_snapshot_dir: true,
163                dedicated_blob_dir: true,
164                dedicated_temp_dir: true,
165                dedicated_metrics_dir: true,
166            },
167        };
168
169        if let Some(value) = overrides.dedicated_wal_dir {
170            toggles.dedicated_wal_dir = value;
171        }
172        if let Some(value) = overrides.dedicated_index_dir {
173            toggles.dedicated_index_dir = value;
174        }
175        if let Some(value) = overrides.dedicated_cache_dir {
176            toggles.dedicated_cache_dir = value;
177        }
178        if let Some(value) = overrides.dedicated_snapshot_dir {
179            toggles.dedicated_snapshot_dir = value;
180        }
181        if let Some(value) = overrides.dedicated_blob_dir {
182            toggles.dedicated_blob_dir = value;
183        }
184        if let Some(value) = overrides.dedicated_temp_dir {
185            toggles.dedicated_temp_dir = value;
186        }
187        if let Some(value) = overrides.dedicated_metrics_dir {
188            toggles.dedicated_metrics_dir = value;
189        }
190
191        toggles
192    }
193}
194
195/// Deterministic paths derived from a data file and expanded layout.
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct TieredLayoutPaths {
198    pub data_file: PathBuf,
199    pub support_dir: PathBuf,
200    pub wal_file: PathBuf,
201    pub logical_wal_file: PathBuf,
202    pub temp_file: PathBuf,
203    pub snapshot_dir: Option<PathBuf>,
204    pub index_dir: Option<PathBuf>,
205    pub cache_dir: Option<PathBuf>,
206    pub blob_dir: Option<PathBuf>,
207    pub metrics_dir: Option<PathBuf>,
208    /// `<support_dir>/logs/` when any log destination resolves to a
209    /// file under the support tree. `None` keeps log files out of the
210    /// user's data directory entirely.
211    pub logs_dir: Option<PathBuf>,
212    pub audit_log_destination: LogDestination,
213    pub slow_log_destination: LogDestination,
214    pub toggles: LayoutToggles,
215}
216
217impl TieredLayoutPaths {
218    pub fn new(
219        data_path: &Path,
220        layout: StorageLayout,
221        overrides: LayoutOverrides,
222    ) -> TieredLayoutPaths {
223        let toggles = layout.expand(&overrides);
224        let data_file = data_path.to_path_buf();
225        let support_dir = sibling_path(data_path, &format!("{}.red", file_name(data_path)));
226
227        let wal_file = if toggles.dedicated_wal_dir {
228            support_dir
229                .join("wal")
230                .join(sidecar_file_name(data_path, "rdb-uwal"))
231        } else {
232            data_path.with_extension("rdb-uwal")
233        };
234        let logical_wal_file = if toggles.dedicated_wal_dir {
235            support_dir
236                .join("wal")
237                .join(format!("{}.logical.wal", file_name(data_path)))
238        } else {
239            sibling_path(data_path, &format!("{}.logical.wal", file_name(data_path)))
240        };
241        let temp_file = if toggles.dedicated_temp_dir {
242            support_dir
243                .join("tmp")
244                .join(sidecar_file_name(data_path, "rdb-tmp"))
245        } else {
246            data_path.with_extension("rdb-tmp")
247        };
248
249        let audit_log_destination = overrides
250            .logs
251            .audit_log
252            .clone()
253            .unwrap_or_else(|| layout.default_audit_log_in(&support_dir));
254        let slow_log_destination = overrides
255            .logs
256            .slow_log
257            .clone()
258            .unwrap_or_else(|| layout.default_slow_log_in(&support_dir));
259        let logs_dir = match (
260            audit_log_destination.file_path(),
261            slow_log_destination.file_path(),
262        ) {
263            (None, None) => None,
264            _ => Some(support_dir.join("logs")),
265        };
266
267        TieredLayoutPaths {
268            data_file,
269            support_dir: support_dir.clone(),
270            wal_file,
271            logical_wal_file,
272            temp_file,
273            snapshot_dir: toggles
274                .dedicated_snapshot_dir
275                .then(|| support_dir.join("snapshots")),
276            index_dir: toggles
277                .dedicated_index_dir
278                .then(|| support_dir.join("indexes")),
279            cache_dir: toggles
280                .dedicated_cache_dir
281                .then(|| support_dir.join("cache")),
282            blob_dir: toggles
283                .dedicated_blob_dir
284                .then(|| support_dir.join("blobs")),
285            metrics_dir: toggles
286                .dedicated_metrics_dir
287                .then(|| support_dir.join("metrics")),
288            logs_dir,
289            audit_log_destination,
290            slow_log_destination,
291            toggles,
292        }
293    }
294
295    pub fn dirs_to_create(&self) -> Vec<PathBuf> {
296        let mut dirs = Vec::new();
297        push_parent(&mut dirs, &self.data_file);
298        push_parent(&mut dirs, &self.wal_file);
299        push_parent(&mut dirs, &self.logical_wal_file);
300        push_parent(&mut dirs, &self.temp_file);
301        push_optional(&mut dirs, self.snapshot_dir.as_ref());
302        push_optional(&mut dirs, self.index_dir.as_ref());
303        push_optional(&mut dirs, self.cache_dir.as_ref());
304        push_optional(&mut dirs, self.blob_dir.as_ref());
305        push_optional(&mut dirs, self.metrics_dir.as_ref());
306        push_optional(&mut dirs, self.logs_dir.as_ref());
307        if let Some(path) = self.audit_log_destination.file_path() {
308            push_parent(&mut dirs, path);
309        }
310        if let Some(path) = self.slow_log_destination.file_path() {
311            push_parent(&mut dirs, path);
312        }
313        dirs.sort();
314        dirs.dedup();
315        dirs
316    }
317
318    pub fn ensure_dirs(&self) -> io::Result<()> {
319        for dir in self.dirs_to_create() {
320            fs::create_dir_all(dir)?;
321        }
322        Ok(())
323    }
324}
325
326fn file_name(path: &Path) -> String {
327    path.file_name()
328        .and_then(|name| name.to_str())
329        .unwrap_or("data.rdb")
330        .to_string()
331}
332
333fn sibling_path(path: &Path, file_name: &str) -> PathBuf {
334    match path.parent() {
335        Some(parent) if !parent.as_os_str().is_empty() => parent.join(file_name),
336        _ => PathBuf::from(file_name),
337    }
338}
339
340fn sidecar_file_name(path: &Path, extension: &str) -> String {
341    path.with_extension(extension)
342        .file_name()
343        .and_then(|name| name.to_str())
344        .unwrap_or("data.rdb")
345        .to_string()
346}
347
348fn push_parent(dirs: &mut Vec<PathBuf>, path: &Path) {
349    if let Some(parent) = path.parent() {
350        if !parent.as_os_str().is_empty() {
351            dirs.push(parent.to_path_buf());
352        }
353    }
354}
355
356fn push_optional(dirs: &mut Vec<PathBuf>, path: Option<&PathBuf>) {
357    if let Some(path) = path {
358        dirs.push(path.clone());
359    }
360}