#[cfg(test)]
mod tests;
mod json_lite;
mod consul;
mod dns_srv;
mod docker;
mod etcd;
use std::net::ToSocketAddrs;
use std::sync::{Arc, RwLock};
use std::time::Duration;
pub enum DiscoverySource {
Static(Vec<String>),
EnvPrefix(String),
File(String),
Dns { hostname: String, port: u16 },
DnsSrv { record: String },
Consul { addr: String, service: String },
Docker { label: String, socket_path: String },
EtcdWatch { endpoints: Vec<String>, prefix: String },
}
impl DiscoverySource {
fn resolve(&self) -> Vec<String> {
match self {
DiscoverySource::Static(v) => v.clone(),
DiscoverySource::EnvPrefix(prefix) => {
let mut backends = Vec::new();
let mut i = 0usize;
loop {
let key = format!("{}_{}", prefix, i);
match std::env::var(&key) {
Ok(val) => { backends.push(val); i += 1; }
Err(_) => break,
}
}
backends
}
DiscoverySource::File(path) => {
match std::fs::read_to_string(path) {
Ok(contents) => contents
.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.map(str::to_string)
.collect(),
Err(e) => {
eprintln!("service_discovery: cannot read backend file {:?}: {}", path, e);
Vec::new()
}
}
}
DiscoverySource::Dns { hostname, port } => {
let addr_str = format!("{}:{}", hostname, port);
match addr_str.to_socket_addrs() {
Ok(addrs) => addrs
.map(|sa| format!("{}:{}", sa.ip(), sa.port()))
.collect(),
Err(e) => {
eprintln!("service_discovery: DNS lookup for {} failed: {}", addr_str, e);
Vec::new()
}
}
}
DiscoverySource::DnsSrv { record } => dns_srv::resolve(record),
DiscoverySource::Consul { addr, service } => consul::discover(addr, service),
DiscoverySource::Docker { label, socket_path } => docker::discover(socket_path, label),
DiscoverySource::EtcdWatch { endpoints, prefix } => {
let Some(endpoint) = endpoints.first() else { return Vec::new() };
match etcd::kv_range(endpoint, prefix) {
Ok(map) => map.into_values().collect(),
Err(e) => {
eprintln!("service_discovery: etcd range query failed: {}", e);
Vec::new()
}
}
}
}
}
}
#[derive(Clone)]
pub struct BackendPool {
backends: Arc<RwLock<Vec<String>>>,
source: Arc<DiscoverySource>,
poll_interval_secs: u64,
}
impl BackendPool {
fn new(source: DiscoverySource) -> Self {
Self {
backends: Arc::new(RwLock::new(Vec::new())),
source: Arc::new(source),
poll_interval_secs: 30,
}
}
pub fn r#static(backends: Vec<String>) -> Self {
let initial = backends.clone();
let pool = Self::new(DiscoverySource::Static(backends));
*pool.backends.write().unwrap() = initial;
pool
}
pub fn env_prefix(prefix: impl Into<String>) -> Self {
Self::new(DiscoverySource::EnvPrefix(prefix.into()))
}
pub fn file(path: impl Into<String>) -> Self {
Self::new(DiscoverySource::File(path.into()))
}
pub fn dns(hostname: impl Into<String>, port: u16) -> Self {
Self::new(DiscoverySource::Dns { hostname: hostname.into(), port })
}
pub fn dns_srv(record: impl Into<String>) -> Self {
Self::new(DiscoverySource::DnsSrv { record: record.into() })
}
pub fn consul(addr: impl Into<String>, service: impl Into<String>) -> Self {
Self::new(DiscoverySource::Consul { addr: addr.into(), service: service.into() })
}
pub fn docker(label: impl Into<String>) -> Self {
Self::new(DiscoverySource::Docker {
label: label.into(),
socket_path: "/var/run/docker.sock".to_string(),
})
}
pub fn docker_with_socket(label: impl Into<String>, socket_path: impl Into<String>) -> Self {
Self::new(DiscoverySource::Docker { label: label.into(), socket_path: socket_path.into() })
}
pub fn etcd(endpoints: Vec<String>, prefix: impl Into<String>) -> Self {
Self::new(DiscoverySource::EtcdWatch { endpoints, prefix: prefix.into() })
}
pub fn poll_interval_secs(mut self, secs: u64) -> Self {
self.poll_interval_secs = secs;
self
}
pub fn start(&self) {
if matches!(self.source.as_ref(), DiscoverySource::Static(_)) {
return;
}
self.refresh();
if let DiscoverySource::EtcdWatch { endpoints, prefix } = self.source.as_ref() {
let endpoints = endpoints.clone();
let prefix = prefix.clone();
let pool = self.clone();
std::thread::spawn(move || etcd::watch_forever(&endpoints, &prefix, &pool));
return;
}
let pool = self.clone();
let interval = Duration::from_secs(self.poll_interval_secs);
std::thread::spawn(move || loop {
std::thread::sleep(interval);
pool.refresh();
});
}
pub fn backends(&self) -> Vec<String> {
self.backends.read().unwrap().clone()
}
pub fn update(&self, backends: Vec<String>) {
*self.backends.write().unwrap() = backends;
}
pub fn refresh(&self) {
let resolved = self.source.resolve();
*self.backends.write().unwrap() = resolved;
}
}