Skip to main content

tael_server/
config.rs

1use std::path::PathBuf;
2
3/// Selected storage backend. `TaelBackend` (the purpose-built tiered engine)
4/// is the default; pass `--storage duckdb` or set `TAEL_STORAGE=duckdb` to use
5/// the legacy embedded-DuckDB backend instead.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum StorageBackend {
8    Duckdb,
9    TaelBackend,
10}
11
12impl StorageBackend {
13    /// Parse a backend name (from the `--storage` flag or `TAEL_STORAGE`).
14    /// Anything that isn't explicitly `duckdb` selects the default tael-backend.
15    pub fn parse(s: &str) -> Self {
16        match s.trim().to_lowercase().as_str() {
17            "duckdb" | "duck" => StorageBackend::Duckdb,
18            _ => StorageBackend::TaelBackend,
19        }
20    }
21}
22
23pub struct ServerConfig {
24    pub otlp_grpc_addr: String,
25    pub rest_api_addr: String,
26    pub data_dir: String,
27    pub wal_dir: String,
28    pub storage: StorageBackend,
29    /// When non-empty, this process runs as a stateless **query tier**: reads
30    /// are served by a `FanoutStore` that scatter-gathers across these shard
31    /// base URLs (`http://shard-0:7701,...`) instead of a local engine. Set via
32    /// `TAEL_QUERY_SHARDS`. See `docs/tael-server-scaling-ha.md` §3.
33    pub query_shards: Vec<String>,
34    /// Standby base URLs this node ships its WAL to as a **leader**
35    /// (`http://standby-1:7701,...`). Set via `TAEL_WAL_STANDBYS`. Only honored
36    /// by the tael-backend engine. See §5.1.
37    pub wal_standbys: Vec<String>,
38    /// How many standbys must ack a write before it returns. `None` = all
39    /// (fully synchronous); `Some(0)` = async best-effort. Set via
40    /// `TAEL_WAL_REQUIRED_ACKS`.
41    pub wal_required_acks: Option<usize>,
42    /// Cluster coordination (chitchat) for automatic leader election + epoch
43    /// fencing of WAL replication. `Some` when `TAEL_CLUSTER_LISTEN` is set.
44    /// See `docs/tael-server-scaling-ha.md` §5.1.
45    pub cluster: Option<ClusterSettings>,
46}
47
48/// Gossip-cluster settings for HA leader election (chitchat).
49#[derive(Debug, Clone)]
50pub struct ClusterSettings {
51    /// Stable unique node id within the replication group (election orders on it).
52    pub node_id: String,
53    /// UDP gossip listen address (`TAEL_CLUSTER_LISTEN`).
54    pub listen_addr: String,
55    /// Address peers reach this node on (`TAEL_CLUSTER_ADVERTISE`; default = listen).
56    pub advertise_addr: String,
57    /// Seed peers' gossip addresses (`TAEL_CLUSTER_SEEDS`).
58    pub seeds: Vec<String>,
59    /// Replication-group id peers must share (`TAEL_CLUSTER_ID`, default `tael`).
60    pub cluster_id: String,
61}
62
63impl ServerConfig {
64    pub fn from_env() -> Self {
65        let mut config = Self {
66            otlp_grpc_addr: std::env::var("TAEL_OTLP_GRPC_ADDR")
67                .unwrap_or_else(|_| "127.0.0.1:4317".into()),
68            rest_api_addr: std::env::var("TAEL_REST_API_ADDR")
69                .unwrap_or_else(|_| "127.0.0.1:7701".into()),
70            data_dir: std::env::var("TAEL_DATA_DIR").unwrap_or_else(|_| default_data_dir()),
71            wal_dir: std::env::var("TAEL_WAL_DIR")
72                .or_else(|_| std::env::var("WALRUS_DATA_DIR"))
73                .unwrap_or_else(|_| default_wal_dir()),
74            // Default to the tael-backend engine; `TAEL_STORAGE` can override.
75            storage: std::env::var("TAEL_STORAGE")
76                .map(|s| StorageBackend::parse(&s))
77                .unwrap_or(StorageBackend::TaelBackend),
78            query_shards: parse_csv_env("TAEL_QUERY_SHARDS"),
79            wal_standbys: parse_csv_env("TAEL_WAL_STANDBYS"),
80            wal_required_acks: std::env::var("TAEL_WAL_REQUIRED_ACKS")
81                .ok()
82                .and_then(|s| s.trim().parse().ok()),
83            cluster: cluster_from_env(),
84        };
85        // A `--storage <duckdb|tael-backend>` flag (or `--storage=…`) takes
86        // precedence over the env var.
87        if let Some(s) = storage_flag() {
88            config.storage = s;
89        }
90        config
91    }
92}
93
94fn default_data_dir() -> String {
95    default_tael_home().join("data").display().to_string()
96}
97
98fn default_wal_dir() -> String {
99    default_tael_home().join("wal_files").display().to_string()
100}
101
102fn default_tael_home() -> PathBuf {
103    home_dir()
104        .map(|home| home.join(".tael"))
105        .unwrap_or_else(|| PathBuf::from(".tael"))
106}
107
108fn home_dir() -> Option<PathBuf> {
109    std::env::var_os("HOME")
110        .map(PathBuf::from)
111        .or_else(|| std::env::var_os("USERPROFILE").map(PathBuf::from))
112        .or_else(
113            || match (std::env::var_os("HOMEDRIVE"), std::env::var_os("HOMEPATH")) {
114                (Some(drive), Some(path)) => {
115                    let mut home = PathBuf::from(drive);
116                    home.push(path);
117                    Some(home)
118                }
119                _ => None,
120            },
121        )
122}
123
124/// Build cluster settings from `TAEL_CLUSTER_*`. Returns `None` (coordination
125/// off) unless `TAEL_CLUSTER_LISTEN` is set.
126fn cluster_from_env() -> Option<ClusterSettings> {
127    let listen_addr = std::env::var("TAEL_CLUSTER_LISTEN").ok()?;
128    let advertise_addr =
129        std::env::var("TAEL_CLUSTER_ADVERTISE").unwrap_or_else(|_| listen_addr.clone());
130    // Default node id to the advertise address — unique per node, stable.
131    let node_id = std::env::var("TAEL_NODE_ID").unwrap_or_else(|_| advertise_addr.clone());
132    Some(ClusterSettings {
133        node_id,
134        listen_addr,
135        advertise_addr,
136        seeds: parse_csv_env("TAEL_CLUSTER_SEEDS"),
137        cluster_id: std::env::var("TAEL_CLUSTER_ID").unwrap_or_else(|_| "tael".to_string()),
138    })
139}
140
141/// Parse a comma-separated env var into a trimmed, non-empty list.
142fn parse_csv_env(var: &str) -> Vec<String> {
143    std::env::var(var)
144        .ok()
145        .map(|s| {
146            s.split(',')
147                .map(|p| p.trim().to_string())
148                .filter(|p| !p.is_empty())
149                .collect()
150        })
151        .unwrap_or_default()
152}
153
154/// Scan the process args for `--storage <value>` / `--storage=<value>`.
155fn storage_flag() -> Option<StorageBackend> {
156    let mut args = std::env::args().skip(1);
157    while let Some(arg) = args.next() {
158        if arg == "--storage" {
159            return args.next().map(|v| StorageBackend::parse(&v));
160        }
161        if let Some(v) = arg.strip_prefix("--storage=") {
162            return Some(StorageBackend::parse(v));
163        }
164    }
165    None
166}