use std::error::Error;
use std::time::Duration;
use argh::FromArgs;
use config::{Config, Environment, File, FileFormat};
use serde::Deserialize;
use tephra::log::set::SegmentConfig;
use tephra::read::ReadConfig;
use tephra::writer::WriterConfig;
use tephra_proto::DEFAULT_MAX_FRAME_LEN;
use tephra_server::ServerConfig;
#[derive(Debug, FromArgs)]
pub struct Args {
#[argh(option, short = 'c')]
pub config: Option<String>,
#[argh(option, short = 'b')]
pub bind: Option<String>,
#[argh(option, short = 'd')]
pub data_dir: Option<String>,
#[argh(option, short = 'l')]
pub log: Option<String>,
}
#[derive(Debug, PartialEq, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Settings {
pub bind: String,
pub data_dir: String,
pub log: Option<String>,
pub segment: SegmentSettings,
pub writer: WriterSettings,
pub read: ReadSettings,
pub server: ServerSettings,
}
impl Default for Settings {
fn default() -> Self {
Settings {
bind: "127.0.0.1:9000".to_string(),
data_dir: "tephra-data".to_string(),
log: None,
segment: SegmentSettings::default(),
writer: WriterSettings::default(),
read: ReadSettings::default(),
server: ServerSettings::default(),
}
}
}
#[derive(Debug, PartialEq, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct SegmentSettings {
pub size: usize,
}
impl Default for SegmentSettings {
fn default() -> Self {
SegmentSettings {
size: 256 * 1024 * 1024,
}
}
}
#[derive(Debug, PartialEq, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct WriterSettings {
pub queue_capacity: usize,
pub max_batch_records: usize,
pub max_batch_bytes: usize,
pub tips_window: u64,
pub condition_force_scan: bool,
}
impl Default for WriterSettings {
fn default() -> Self {
WriterSettings {
queue_capacity: 1024,
max_batch_records: 1024,
max_batch_bytes: 8 * 1024 * 1024,
tips_window: 1_000_000,
condition_force_scan: false,
}
}
}
#[derive(Debug, PartialEq, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ReadSettings {
pub scan_bias: u32,
}
impl Default for ReadSettings {
fn default() -> Self {
ReadSettings { scan_bias: 4 }
}
}
#[derive(Debug, PartialEq, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ServerSettings {
pub max_frame_len: u32,
pub read_batch_events: usize,
pub read_batch_bytes: usize,
pub subscribe_wait_tick_ms: u64,
pub keepalive_idle_secs: u64,
pub keepalive_interval_secs: u64,
}
impl Default for ServerSettings {
fn default() -> Self {
ServerSettings {
max_frame_len: DEFAULT_MAX_FRAME_LEN,
read_batch_events: 1024,
read_batch_bytes: 512 * 1024,
subscribe_wait_tick_ms: 250,
keepalive_idle_secs: 60,
keepalive_interval_secs: 15,
}
}
}
impl Settings {
pub fn segment_config(&self) -> SegmentConfig {
SegmentConfig::new(self.segment.size)
}
pub fn writer_config(&self) -> WriterConfig {
WriterConfig {
queue_capacity: self.writer.queue_capacity,
max_batch_records: self.writer.max_batch_records,
max_batch_bytes: self.writer.max_batch_bytes,
tips_window: self.writer.tips_window,
verify_tips: false,
condition_force_scan: self.writer.condition_force_scan,
read: ReadConfig {
scan_bias: self.read.scan_bias,
},
}
}
pub fn server_config(&self) -> ServerConfig {
ServerConfig {
max_frame_len: self.server.max_frame_len,
read_batch_events: self.server.read_batch_events,
read_batch_bytes: self.server.read_batch_bytes,
subscribe_wait_tick: Duration::from_millis(self.server.subscribe_wait_tick_ms),
keepalive_idle: Duration::from_secs(self.server.keepalive_idle_secs),
keepalive_interval: Duration::from_secs(self.server.keepalive_interval_secs),
}
}
fn validate(&self) -> Result<(), String> {
if self.writer.queue_capacity == 0 {
return Err("writer.queue_capacity must be at least 1".to_string());
}
if self.writer.max_batch_records == 0 {
return Err("writer.max_batch_records must be at least 1".to_string());
}
if self.server.subscribe_wait_tick_ms == 0 {
return Err("server.subscribe_wait_tick_ms must be at least 1".to_string());
}
if self.server.keepalive_idle_secs == 0 {
return Err("server.keepalive_idle_secs must be at least 1".to_string());
}
if self.server.keepalive_interval_secs == 0 {
return Err("server.keepalive_interval_secs must be at least 1".to_string());
}
Ok(())
}
}
pub fn load(args: &Args) -> Result<Settings, Box<dyn Error>> {
let mut builder = Config::builder();
if let Some(path) = &args.config {
builder = builder.add_source(File::new(path, FileFormat::Toml).required(true));
}
builder = builder.add_source(
Environment::with_prefix("TEPHRA")
.prefix_separator("__")
.separator("__")
.try_parsing(true),
);
let mut settings: Settings = builder.build()?.try_deserialize()?;
if let Some(bind) = &args.bind {
settings.bind = bind.clone();
}
if let Some(data_dir) = &args.data_dir {
settings.data_dir = data_dir.clone();
}
if args.log.is_some() {
settings.log = args.log.clone();
}
settings.validate()?;
Ok(settings)
}
#[cfg(test)]
mod tests {
use super::*;
fn no_args() -> Args {
Args {
config: None,
bind: None,
data_dir: None,
log: None,
}
}
#[test]
fn defaults_match_the_library_defaults() {
let settings = load(&no_args()).unwrap();
let writer = settings.writer_config();
let library_default = WriterConfig::default();
assert_eq!(writer.queue_capacity, library_default.queue_capacity);
assert_eq!(writer.max_batch_records, library_default.max_batch_records);
assert_eq!(writer.max_batch_bytes, library_default.max_batch_bytes);
assert_eq!(writer.tips_window, library_default.tips_window);
assert!(!writer.verify_tips);
assert_eq!(writer.read.scan_bias, ReadConfig::default().scan_bias);
let server = settings.server_config();
let server_default = ServerConfig::default();
assert_eq!(server.max_frame_len, server_default.max_frame_len);
assert_eq!(server.read_batch_events, server_default.read_batch_events);
assert_eq!(server.read_batch_bytes, server_default.read_batch_bytes);
assert_eq!(
server.subscribe_wait_tick,
server_default.subscribe_wait_tick
);
assert_eq!(server.keepalive_idle, server_default.keepalive_idle);
assert_eq!(server.keepalive_interval, server_default.keepalive_interval);
assert_eq!(settings.bind, "127.0.0.1:9000");
assert_eq!(settings.data_dir, "tephra-data");
}
#[test]
fn example_toml_mirrors_the_defaults() {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tephra.example.toml");
let settings: Settings = Config::builder()
.add_source(File::new(path, FileFormat::Toml).required(true))
.build()
.unwrap()
.try_deserialize()
.unwrap();
assert_eq!(settings, Settings::default());
}
#[test]
fn cli_overrides_win() {
let args = Args {
config: None,
bind: Some("0.0.0.0:7000".to_string()),
data_dir: Some("/var/lib/tephra".to_string()),
log: Some("tephra=debug".to_string()),
};
let settings = load(&args).unwrap();
assert_eq!(settings.bind, "0.0.0.0:7000");
assert_eq!(settings.data_dir, "/var/lib/tephra");
assert_eq!(settings.log.as_deref(), Some("tephra=debug"));
}
#[test]
fn zero_writer_counts_are_rejected_not_panicked() {
let mut settings = Settings::default();
settings.writer.queue_capacity = 0;
assert!(settings.validate().is_err());
let mut settings = Settings::default();
settings.writer.max_batch_records = 0;
assert!(settings.validate().is_err());
}
#[test]
fn zero_server_durations_are_rejected() {
let mut settings = Settings::default();
settings.server.subscribe_wait_tick_ms = 0;
assert!(settings.validate().is_err());
let mut settings = Settings::default();
settings.server.keepalive_idle_secs = 0;
assert!(settings.validate().is_err());
let mut settings = Settings::default();
settings.server.keepalive_interval_secs = 0;
assert!(settings.validate().is_err());
}
}