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