use std::net::SocketAddr;
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::Arc;
use yah_object_store::R2ObjectStore;
use yah_scryer::{
FederationState, LongTierConfig, LongTierStore, MS_PER_DAY, ObjectStore, OperatorTagAcl,
PromotionConfig, PromotionConsumer, Scryer, ScryerConfig, SnapshotConfig, SnapshotProducer,
serve_federation,
};
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().collect();
let listen = parse_arg(&args, "--listen").unwrap_or_else(|| "127.0.0.1:6543".to_string());
let data_dir =
parse_arg(&args, "--data").unwrap_or_else(|| "/var/lib/yah/scryer/".to_string());
let addr: SocketAddr = match listen.parse() {
Ok(a) => a,
Err(e) => {
eprintln!("yah-scryer: invalid --listen {listen:?}: {e}");
return ExitCode::from(1);
}
};
let data_path = PathBuf::from(&data_dir);
if let Err(e) = std::fs::create_dir_all(&data_path) {
eprintln!("yah-scryer: cannot create data dir {data_path:?}: {e}");
return ExitCode::from(1);
}
let cfg = ScryerConfig::new(data_path.join("events.db"));
let mut scryer = match Scryer::new(cfg, None) {
Ok(s) => s,
Err(e) => {
eprintln!("yah-scryer: cannot open events.db: {e}");
return ExitCode::from(1);
}
};
let retention_days: u64 = parse_arg(&args, "--retention-days")
.and_then(|s| s.parse().ok())
.unwrap_or(7);
let retention_ms = retention_days.saturating_mul(MS_PER_DAY);
let promote_interval = std::time::Duration::from_secs(
parse_arg(&args, "--promote-interval-secs")
.and_then(|s| s.parse().ok())
.unwrap_or(3600),
);
let snapshot_interval: Option<std::time::Duration> = parse_arg(&args, "--snapshot-interval-secs")
.and_then(|s| s.parse::<u64>().ok())
.map(std::time::Duration::from_secs);
let (long_tier, snapshot): (Option<Arc<LongTierStore>>, Option<SnapshotProducer>) = match (
parse_arg(&args, "--long-tier-bucket"),
parse_arg(&args, "--r2-account"),
) {
(Some(bucket), Some(account)) => {
let machine_id = parse_arg(&args, "--machine-id")
.or_else(|| std::env::var("YAH_MACHINE_ID").ok())
.or_else(|| std::env::var("HOSTNAME").ok())
.unwrap_or_default();
if machine_id.is_empty() {
eprintln!(
"yah-scryer: long tier requires a machine id — pass --machine-id \
or set $YAH_MACHINE_ID / $HOSTNAME"
);
return ExitCode::from(1);
}
let obj_store = match R2ObjectStore::from_vault(account, bucket) {
Ok(s) => Arc::new(s) as Arc<dyn ObjectStore>,
Err(e) => {
eprintln!("yah-scryer: cannot build R2 object store: {e}");
return ExitCode::from(1);
}
};
let snapshot = snapshot_interval.map(|iv| {
SnapshotProducer::new(
Arc::clone(&obj_store),
SnapshotConfig::new(vec![machine_id.clone()], retention_ms).with_interval(iv),
)
});
let lt = Arc::new(LongTierStore::new(
LongTierConfig { machine_id, retention_ms },
obj_store,
));
scryer = scryer.with_long_tier(Arc::clone(<), retention_ms);
(Some(lt), snapshot)
}
(Some(_), None) | (None, Some(_)) => {
eprintln!(
"yah-scryer: long tier needs both --long-tier-bucket and --r2-account; \
ignoring partial config"
);
(None, None)
}
(None, None) => (None, None),
};
let scryer = Arc::new(scryer);
let promo_scryer = Arc::clone(&scryer);
let state = FederationState::new(scryer, Arc::new(OperatorTagAcl));
let runtime = match tokio::runtime::Builder::new_multi_thread().enable_all().build() {
Ok(r) => r,
Err(e) => {
eprintln!("yah-scryer: cannot build tokio runtime: {e}");
return ExitCode::from(1);
}
};
runtime.block_on(async move {
let (local, handle) = match serve_federation(state, addr).await {
Ok(pair) => pair,
Err(e) => {
eprintln!("yah-scryer: bind {addr} failed: {e}");
return ExitCode::from(1);
}
};
eprintln!("yah-scryer listening on {local}");
if let Some(lt) = long_tier {
eprintln!(
"yah-scryer: long-tier promotion enabled (retention {retention_days}d, \
interval {}s)",
promote_interval.as_secs()
);
let cfg = PromotionConfig::new(retention_ms).with_interval(promote_interval);
PromotionConsumer::new(promo_scryer, lt, cfg).spawn();
}
if let Some(producer) = snapshot {
eprintln!(
"yah-scryer: Mode-2 analytics snapshot producer enabled (interval {}s)",
snapshot_interval.map(|d| d.as_secs()).unwrap_or(0)
);
producer.spawn();
}
tokio::select! {
_ = handle => {}
_ = shutdown_signal() => {
eprintln!("yah-scryer: shutdown signal received");
}
}
ExitCode::SUCCESS
})
}
fn parse_arg(args: &[String], name: &str) -> Option<String> {
let mut iter = args.iter().peekable();
while let Some(arg) = iter.next() {
if arg == name {
return iter.next().cloned();
}
if let Some(rest) = arg.strip_prefix(&format!("{name}=")) {
return Some(rest.to_string());
}
}
None
}
#[cfg(unix)]
async fn shutdown_signal() {
use tokio::signal::unix::{SignalKind, signal};
let mut term = signal(SignalKind::terminate()).expect("install SIGTERM handler");
let mut int = signal(SignalKind::interrupt()).expect("install SIGINT handler");
tokio::select! {
_ = term.recv() => {}
_ = int.recv() => {}
}
}
#[cfg(not(unix))]
async fn shutdown_signal() {
let _ = tokio::signal::ctrl_c().await;
}