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