omni_dev/daemon/
registry.rs1use std::sync::Arc;
4use std::time::Duration;
5
6use anyhow::{anyhow, Result};
7use serde_json::Value;
8
9use crate::github_rate_limit::{RateLimitCache, RateLimitSnapshot};
10
11use super::service::{DaemonService, ServiceStatus, ServiceStream};
12
13#[derive(Clone, Default)]
16pub struct ServiceRegistry {
17 services: Vec<Arc<dyn DaemonService>>,
18 github_rate_limit: Option<Arc<RateLimitCache>>,
24}
25
26impl ServiceRegistry {
27 pub fn new() -> Self {
29 Self::default()
30 }
31
32 pub fn register(&mut self, service: Arc<dyn DaemonService>) {
37 let name = service.name();
38 if self.services.iter().any(|s| s.name() == name) {
39 tracing::warn!("ignoring duplicate registration of daemon service `{name}`");
40 return;
41 }
42 self.services.push(service);
43 }
44
45 pub fn get(&self, name: &str) -> Option<&Arc<dyn DaemonService>> {
47 self.services.iter().find(|s| s.name() == name)
48 }
49
50 pub fn services(&self) -> &[Arc<dyn DaemonService>] {
52 &self.services
53 }
54
55 pub fn set_github_rate_limit(&mut self, cache: Arc<RateLimitCache>) {
59 self.github_rate_limit = Some(cache);
60 }
61
62 #[must_use]
65 pub fn github_rate_limit(&self) -> Option<RateLimitSnapshot> {
66 self.github_rate_limit.as_ref().and_then(|c| c.get())
67 }
68
69 pub async fn dispatch(&self, service: &str, op: &str, payload: Value) -> Result<Value> {
72 let svc = self
73 .get(service)
74 .ok_or_else(|| anyhow!("unknown service: {service}"))?;
75 svc.handle(op, payload).await
76 }
77
78 pub fn subscribe(
83 &self,
84 service: &str,
85 op: &str,
86 payload: &Value,
87 ) -> Option<Box<dyn ServiceStream>> {
88 self.get(service)?.subscribe(op, payload)
89 }
90
91 pub async fn statuses(&self) -> Vec<ServiceStatus> {
93 let mut out = Vec::with_capacity(self.services.len());
94 for svc in &self.services {
95 out.push(svc.status().await);
96 }
97 out
98 }
99
100 pub fn credit_accept_outage(&self, outage: Duration) {
103 for svc in &self.services {
104 svc.credit_accept_outage(outage);
105 }
106 }
107
108 pub async fn shutdown_all(&self) {
110 for svc in &self.services {
111 svc.shutdown().await;
112 }
113 }
114}
115
116#[cfg(test)]
117#[allow(clippy::unwrap_used, clippy::expect_used)]
118mod tests {
119 use super::*;
120 use crate::daemon::services::echo::EchoService;
121 use serde_json::json;
122
123 #[tokio::test]
124 async fn routes_known_service_and_rejects_unknown() {
125 let mut registry = ServiceRegistry::new();
126 assert!(registry.services().is_empty());
127 registry.register(Arc::new(EchoService));
128
129 assert!(registry.get("echo").is_some());
130 assert!(registry.get("missing").is_none());
131
132 assert_eq!(
134 registry
135 .dispatch("echo", "echo", json!({ "x": 1 }))
136 .await
137 .unwrap(),
138 json!({ "x": 1 })
139 );
140 let err = registry
141 .dispatch("missing", "echo", Value::Null)
142 .await
143 .unwrap_err();
144 assert!(err.to_string().contains("unknown service"));
145
146 assert_eq!(registry.statuses().await.len(), 1);
148 registry.shutdown_all().await;
149 }
150
151 #[test]
152 fn credit_accept_outage_reaches_every_service() {
153 use crate::daemon::testutil::StubService;
154
155 let first = Arc::new(StubService::new("first"));
156 let second = Arc::new(StubService::new("second"));
157 let mut registry = ServiceRegistry::new();
158 registry.register(first.clone());
159 registry.register(second.clone());
160
161 registry.credit_accept_outage(Duration::from_secs(7));
162
163 assert_eq!(first.credited(), vec![Duration::from_secs(7)]);
164 assert_eq!(second.credited(), vec![Duration::from_secs(7)]);
165 }
166
167 #[test]
168 fn credit_accept_outage_is_a_no_op_for_a_service_that_ignores_it() {
169 let mut registry = ServiceRegistry::new();
171 registry.register(Arc::new(EchoService));
172 registry.credit_accept_outage(Duration::from_secs(7));
173 }
174
175 #[test]
176 fn duplicate_registration_is_ignored() {
177 let mut registry = ServiceRegistry::new();
178 registry.register(Arc::new(EchoService));
179 registry.register(Arc::new(EchoService));
180 assert_eq!(registry.services().len(), 1);
183 }
184}