use std::collections::HashMap;
use std::sync::{Arc, LazyLock, RwLock};
use serde::{Deserialize, Serialize};
use tracing::{debug, warn};
use crate::ports::ScrapingService;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ServiceStatus {
Healthy,
Degraded(String),
Unavailable(String),
Unknown,
}
impl ServiceStatus {
pub const fn is_available(&self) -> bool {
matches!(self, Self::Healthy | Self::Degraded(_))
}
}
struct RegistryEntry {
service: Arc<dyn ScrapingService>,
status: ServiceStatus,
}
pub struct ServiceRegistry {
entries: Arc<RwLock<HashMap<String, RegistryEntry>>>,
}
#[allow(clippy::unwrap_used)]
impl ServiceRegistry {
pub fn new() -> Self {
Self {
entries: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn builder() -> RegistryBuilder {
RegistryBuilder::new()
}
pub fn register(&self, name: String, service: Arc<dyn ScrapingService>) {
let entry = RegistryEntry {
service,
status: ServiceStatus::Unknown,
};
self.entries.write().unwrap().insert(name, entry);
}
pub fn get(&self, name: &str) -> Option<Arc<dyn ScrapingService>> {
self.entries
.read()
.unwrap()
.get(name)
.map(|e| Arc::clone(&e.service))
}
pub fn status(&self, name: &str) -> Option<ServiceStatus> {
self.entries
.read()
.unwrap()
.get(name)
.map(|e| e.status.clone())
}
pub fn names(&self) -> Vec<String> {
self.entries.read().unwrap().keys().cloned().collect()
}
pub fn deregister(&self, name: &str) -> bool {
self.entries.write().unwrap().remove(name).is_some()
}
#[allow(clippy::unused_async)]
pub async fn health_check_all(&self) -> HashMap<String, ServiceStatus> {
let entries_snapshot: Vec<(String, Arc<dyn ScrapingService>)> = {
let guard = self.entries.read().unwrap();
guard
.iter()
.map(|(k, v)| (k.clone(), Arc::clone(&v.service)))
.collect()
};
let mut results = HashMap::new();
for (name, svc) in entries_snapshot {
let status = Self::probe_service(svc);
debug!(service = %name, ?status, "health check");
{
let mut guard = self.entries.write().unwrap();
if let Some(entry) = guard.get_mut(&name) {
entry.status = status.clone();
}
}
results.insert(name, status);
}
results
}
#[allow(clippy::needless_pass_by_value)]
fn probe_service(svc: Arc<dyn ScrapingService>) -> ServiceStatus {
let name = svc.name();
if name.is_empty() {
warn!("Service returned empty name during health probe");
ServiceStatus::Degraded("empty service name".to_string())
} else {
ServiceStatus::Healthy
}
}
pub fn update_status(&self, name: &str, status: ServiceStatus) {
let mut guard = self.entries.write().unwrap();
if let Some(entry) = guard.get_mut(name) {
entry.status = status;
}
}
}
impl Default for ServiceRegistry {
fn default() -> Self {
Self::new()
}
}
pub struct RegistryBuilder {
entries: HashMap<String, Arc<dyn ScrapingService>>,
}
#[allow(clippy::unwrap_used)] impl RegistryBuilder {
fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
#[must_use]
pub fn register(mut self, name: impl Into<String>, service: Arc<dyn ScrapingService>) -> Self {
self.entries.insert(name.into(), service);
self
}
pub fn build(self) -> ServiceRegistry {
let registry = ServiceRegistry::new();
{
let mut guard = registry.entries.write().unwrap();
for (name, service) in self.entries {
guard.insert(
name,
RegistryEntry {
service,
status: ServiceStatus::Unknown,
},
);
}
}
registry
}
}
pub fn global_registry() -> &'static ServiceRegistry {
static INSTANCE: LazyLock<ServiceRegistry> = LazyLock::new(ServiceRegistry::new);
&INSTANCE
}
#[cfg(test)]
mod tests {
use super::*;
use crate::adapters::noop::NoopService as NoopScraper;
fn noop() -> Arc<dyn ScrapingService> {
Arc::new(NoopScraper)
}
#[test]
fn register_and_get() {
let r = ServiceRegistry::new();
r.register("svc".to_string(), noop());
assert!(r.get("svc").is_some());
assert!(r.get("missing").is_none());
}
#[test]
fn deregister() {
let r = ServiceRegistry::new();
r.register("svc".to_string(), noop());
assert!(r.deregister("svc"));
assert!(!r.deregister("svc")); assert!(r.get("svc").is_none());
}
#[test]
fn names_lists_all() {
let r = ServiceRegistry::builder()
.register("a", noop())
.register("b", noop())
.build();
let mut names = r.names();
names.sort();
assert_eq!(names, vec!["a", "b"]);
}
#[test]
fn builder_pattern() {
let r = ServiceRegistry::builder()
.register("one", noop())
.register("two", noop())
.build();
assert_eq!(r.names().len(), 2);
}
#[test]
fn status_unknown_after_register() {
let r = ServiceRegistry::new();
r.register("svc".to_string(), noop());
assert_eq!(r.status("svc"), Some(ServiceStatus::Unknown));
}
#[test]
fn update_status() {
let r = ServiceRegistry::new();
r.register("svc".to_string(), noop());
r.update_status("svc", ServiceStatus::Healthy);
assert_eq!(r.status("svc"), Some(ServiceStatus::Healthy));
}
#[test]
fn service_status_is_available() {
assert!(ServiceStatus::Healthy.is_available());
assert!(ServiceStatus::Degraded("x".into()).is_available());
assert!(!ServiceStatus::Unavailable("x".into()).is_available());
assert!(!ServiceStatus::Unknown.is_available());
}
#[tokio::test]
async fn health_check_all_marks_healthy() {
let r = ServiceRegistry::builder().register("noop", noop()).build();
let results = r.health_check_all().await;
assert_eq!(results.get("noop"), Some(&ServiceStatus::Healthy));
assert_eq!(r.status("noop"), Some(ServiceStatus::Healthy));
}
#[test]
fn global_registry_singleton_is_same_ref() {
use std::ptr;
let a = global_registry();
let b = global_registry();
assert!(ptr::addr_eq(a, b));
}
}