use anyhow::{Context, Result};
use feather_reader::config::Config;
use feather_reader::{store, web, AppState, VERSION};
use tokio::sync::watch;
use tracing::info;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
#[path = "scheduler.rs"]
mod scheduler;
#[tokio::main]
async fn main() -> Result<()> {
let config = Config::from_env().context("loading configuration")?;
init_tracing();
info!(version = VERSION, bind = %config.bind, db = %config.db_path.display(), "starting featherreader");
let db = store::init(&config)
.await
.context("initializing the SQLite store")?;
check_watermark_vs_disk(&config);
match store::ensure_seed(&db, config.admin_seed_dids()).await {
Ok(new_seats) => {
if new_seats > 0 {
info!(new_seats, "seeded admin DIDs into beta_access");
}
}
Err(err) => return Err(err).context("seeding admin beta_access DIDs"),
}
let bind = config.bind;
let state = AppState::new(config, db).context("building application state")?;
let (shutdown_tx, shutdown_rx) = watch::channel(());
tokio::spawn(async move {
shutdown_signal().await;
let _ = shutdown_tx.send(());
});
let scheduler_handles = scheduler::spawn(state.clone(), shutdown_rx.clone());
let router = web::router(state);
let listener = tokio::net::TcpListener::bind(bind)
.await
.with_context(|| format!("binding to {bind}"))?;
info!(addr = %bind, "listening");
axum::serve(
listener,
router.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.with_graceful_shutdown(wait_for_shutdown(shutdown_rx))
.await
.context("HTTP server error")?;
for h in scheduler_handles {
let _ = h.await;
}
info!("shutdown complete");
Ok(())
}
fn check_watermark_vs_disk(config: &Config) {
let watermark = config.db_size_watermark_bytes;
if watermark <= 0 {
info!("DB-size watermark disabled (0): the poller will not pause on disk pressure");
return;
}
info!(
watermark_bytes = watermark,
db = %config.db_path.display(),
"DB-size watermark effective (poller pauses new fetches at/above this)"
);
match available_disk_bytes(&config.db_path) {
Some(avail) if watermark as u64 >= avail => {
tracing::warn!(
watermark_bytes = watermark,
available_bytes = avail,
db = %config.db_path.display(),
"DB-size watermark is >= free space on its volume: it cannot protect the disk \
(the volume fills before the poller pauses). Set \
FEATHERREADER_DB_SIZE_WATERMARK_BYTES BELOW the volume size."
);
}
Some(avail) => info!(
available_bytes = avail,
"DB volume free space checked; watermark below it"
),
None => debug_no_statvfs(),
}
}
fn debug_no_statvfs() {
info!(
"could not read DB volume free space (statvfs unavailable); watermark value logged above"
);
}
#[cfg(unix)]
fn available_disk_bytes(path: &std::path::Path) -> Option<u64> {
use std::os::unix::ffi::OsStrExt;
let dir = path.parent().filter(|p| !p.as_os_str().is_empty());
let target = dir.unwrap_or_else(|| std::path::Path::new("."));
let cstr = std::ffi::CString::new(target.as_os_str().as_bytes()).ok()?;
let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
let rc = unsafe { libc::statvfs(cstr.as_ptr(), &mut stat) };
if rc != 0 {
return None;
}
let frsize = stat.f_frsize as u128;
let bavail = stat.f_bavail as u128;
Some((frsize.saturating_mul(bavail)).min(u64::MAX as u128) as u64)
}
#[cfg(not(unix))]
fn available_disk_bytes(_path: &std::path::Path) -> Option<u64> {
None
}
fn init_tracing() {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry()
.with(filter)
.with(fmt::layer())
.init();
}
async fn shutdown_signal() {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
match signal(SignalKind::terminate()) {
Ok(mut sigterm) => {
tokio::select! {
r = tokio::signal::ctrl_c() => {
if let Err(err) = r {
tracing::error!(%err, "failed to install Ctrl-C handler");
}
}
_ = sigterm.recv() => {}
}
}
Err(err) => {
tracing::error!(%err, "failed to install SIGTERM handler");
let _ = tokio::signal::ctrl_c().await;
}
}
info!("shutdown signal received");
}
#[cfg(not(unix))]
{
if let Err(err) = tokio::signal::ctrl_c().await {
tracing::error!(%err, "failed to install Ctrl-C handler");
}
info!("shutdown signal received");
}
}
async fn wait_for_shutdown(mut rx: watch::Receiver<()>) {
let _ = rx.changed().await;
}