Skip to main content

boatramp_node/
backends.rs

1//! Backend-selection enums for a node's blob + KV stores (node-library N2b).
2//!
3//! These are the domain types the store-construction assembly dispatches on. They
4//! live here (not the binary) so the assembly can move into the library; the
5//! `clap::ValueEnum` derive is behind the optional `clap` feature so the CLI
6//! binary uses them directly in its args, while a non-CLI embedder never pulls
7//! clap. (`build_kv`/`build_blobs` join this module as they migrate off the
8//! binary's `ServeArgs`.)
9
10use std::path::Path;
11use std::sync::Arc;
12
13use boatramp_core::kv::{KvStore, MemoryKv};
14
15use crate::error::Result;
16
17/// Blob (file-content) backend.
18///
19/// `Deserialize` (lowercase: `fs`/`s3`/`gcs`/`azure`) so `[serve].blobs` in `boatramp.cfg` can
20/// select the backend — the config-level analog of the `--blobs` flag, which `boatramp blob
21/// migrate` reads to build a source/destination backend from a config file alone.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
23#[serde(rename_all = "lowercase")]
24#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
25pub enum BlobBackend {
26    /// Local filesystem (`<data-dir>/blobs`).
27    Fs,
28    /// S3-compatible object store (requires `--features s3`).
29    S3,
30    /// Google Cloud Storage (requires `--features gcs`).
31    Gcs,
32    /// Azure Blob Storage (requires `--features azure`).
33    Azure,
34}
35
36/// Metadata (manifest + pointer) backend.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
39pub enum KvBackend {
40    /// Transactional LSM over object storage; durable local default
41    /// (`<data-dir>/kv-slate`). Requires `--features slatedb` (on by default).
42    Slatedb,
43    /// In-memory (ephemeral; lost on restart).
44    Memory,
45    /// Cloudflare KV over REST (requires `--features cloudflare-kv`).
46    Cloudflare,
47}
48
49/// Flush interval for the control-plane SlateDB store: tiny, so a control-plane
50/// write is durable almost immediately (correctness over throughput).
51pub const CONTROL_PLANE_FLUSH: std::time::Duration = std::time::Duration::from_millis(5);
52
53/// Where the SlateDB control-plane store lives when it runs on an S3-compatible
54/// object store (Cloudflare R2) instead of local disk — the durable, remote-state
55/// deployment. Credentials come from the ambient AWS environment
56/// (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`), matching the S3 blob backend.
57#[derive(Debug, Clone)]
58pub struct SlateKvS3 {
59    /// The bucket the store lives in (shared with S3 blobs, under `prefix`).
60    pub bucket: String,
61    /// Custom endpoint (R2: `https://<account>.r2.cloudflarestorage.com`).
62    pub endpoint: Option<String>,
63    /// Region (R2 uses `auto`).
64    pub region: Option<String>,
65    /// Use path-style addressing (R2 accepts it).
66    pub path_style: bool,
67    /// Key prefix within the bucket (keeps the LSM files apart from the blobs).
68    pub prefix: String,
69}
70
71/// Build the metadata KV store for the selected [`KvBackend`]. When `slate_s3` is
72/// set (and the backend is SlateDB), the store runs on R2/S3 (durable across a
73/// scale-to-zero container stop) rather than the local `data_dir`.
74///
75/// `repair_wal` (opt-in) runs the [WAL tail repair](boatramp_storage::wal_repair) over the
76/// SlateDB store BEFORE opening — the P0 crash/snapshot recovery. It applies ONLY to the
77/// SlateDB backend (a no-op for Memory/Cloudflare) and is threaded from `serve --repair-wal` /
78/// `BOATRAMP_KV_REPAIR=1`.
79pub async fn build_kv(
80    kv: KvBackend,
81    data_dir: &Path,
82    slate_s3: Option<&SlateKvS3>,
83    repair_wal: bool,
84) -> Result<Arc<dyn KvStore>> {
85    match kv {
86        KvBackend::Slatedb => build_slatedb_kv(data_dir, slate_s3, repair_wal).await,
87        KvBackend::Memory => Ok(Arc::new(MemoryKv::new())),
88        KvBackend::Cloudflare => build_cloudflare_kv(),
89    }
90}
91
92#[cfg(feature = "slatedb")]
93async fn build_slatedb_kv(
94    data_dir: &Path,
95    slate_s3: Option<&SlateKvS3>,
96    repair_wal: bool,
97) -> Result<Arc<dyn KvStore>> {
98    // Opt-in: repair-then-open (the operator asked to recover a torn trailing WAL tail in
99    // place). `None` is the default cold open, which still fails LOUD on a torn tail.
100    let repair = repair_wal.then_some(boatramp_storage::kv_slatedb::RepairMode::Apply);
101    match slate_s3 {
102        Some(s3) => Ok(Arc::new(
103            boatramp_storage::SlateKv::open_s3_with_flush_repair(
104                &boatramp_storage::S3StoreConfig {
105                    bucket: s3.bucket.clone(),
106                    endpoint: s3.endpoint.clone(),
107                    region: s3.region.clone(),
108                    path_style: s3.path_style,
109                },
110                &s3.prefix,
111                CONTROL_PLANE_FLUSH,
112                repair,
113            )
114            .await?,
115        )),
116        None => Ok(Arc::new(
117            boatramp_storage::SlateKv::open_local_with_flush_repair(
118                data_dir.join("kv-slate"),
119                CONTROL_PLANE_FLUSH,
120                repair,
121            )
122            .await?,
123        )),
124    }
125}
126
127#[cfg(not(feature = "slatedb"))]
128async fn build_slatedb_kv(
129    _data_dir: &Path,
130    _slate_s3: Option<&SlateKvS3>,
131    _repair_wal: bool,
132) -> Result<Arc<dyn KvStore>> {
133    Err(crate::error::Error::NoSlatedbSupport)
134}
135
136#[cfg(feature = "cloudflare-kv")]
137fn build_cloudflare_kv() -> Result<Arc<dyn KvStore>> {
138    Ok(Arc::new(boatramp_storage::CloudflareKv::from_env()?))
139}
140
141#[cfg(not(feature = "cloudflare-kv"))]
142fn build_cloudflare_kv() -> Result<Arc<dyn KvStore>> {
143    Err(crate::error::Error::NoCloudflareKvSupport)
144}