1use std::{
2 collections::{BTreeMap, BTreeSet, HashMap},
3 fmt,
4 future::Future,
5 pin::Pin,
6 sync::{Arc, Mutex},
7 time::Duration,
8};
9
10use tokio::sync::broadcast;
11
12const EVENT_BUFFER_SIZE: usize = 128;
13const MAX_ENDPOINT_WEIGHT: u32 = 1_000;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct DiscoveryReconnectBackoff {
21 initial: Duration,
22 max: Duration,
23 jitter: Duration,
24}
25
26impl Default for DiscoveryReconnectBackoff {
27 fn default() -> Self {
28 Self {
29 initial: Duration::from_millis(200),
30 max: Duration::from_secs(10),
31 jitter: Duration::from_millis(200),
32 }
33 }
34}
35
36impl DiscoveryReconnectBackoff {
37 pub fn new(initial: Duration, max: Duration, jitter: Duration) -> Self {
38 assert!(
39 !initial.is_zero(),
40 "discovery reconnect delay must be positive"
41 );
42 assert!(
43 max >= initial,
44 "discovery reconnect maximum must not be less than its initial delay"
45 );
46 Self {
47 initial,
48 max,
49 jitter,
50 }
51 }
52
53 pub fn initial(self) -> Duration {
54 self.initial
55 }
56
57 pub fn max(self) -> Duration {
58 self.max
59 }
60
61 pub fn jitter(self) -> Duration {
62 self.jitter
63 }
64
65 pub fn delay(self, attempt: u32, jitter_sample: u64) -> Duration {
68 let multiplier = 1_u32.checked_shl(attempt.min(31)).unwrap_or(u32::MAX);
69 let base = self.initial.saturating_mul(multiplier).min(self.max);
70 let jitter_nanos = self.jitter.as_nanos();
71 if jitter_nanos == 0 {
72 return base;
73 }
74 let sampled = u128::from(jitter_sample) % (jitter_nanos + 1);
75 base.saturating_add(Duration::from_nanos(
76 u64::try_from(sampled).unwrap_or(u64::MAX),
77 ))
78 }
79}
80
81#[cfg(test)]
82mod reconnect_tests {
83 use super::*;
84
85 #[test]
86 fn reconnect_backoff_is_exponential_capped_and_deterministic() {
87 let policy = DiscoveryReconnectBackoff::new(
88 Duration::from_millis(10),
89 Duration::from_millis(40),
90 Duration::from_millis(5),
91 );
92 assert_eq!(policy.delay(0, 0), Duration::from_millis(10));
93 assert_eq!(policy.delay(1, 1_000_000), Duration::from_millis(21));
94 assert_eq!(policy.delay(2, 2_000_000), Duration::from_millis(42));
95 assert_eq!(policy.delay(20, 5_000_001), Duration::from_millis(40));
96 assert_eq!(policy.delay(2, 123), policy.delay(2, 123));
97 }
98
99 #[test]
100 #[should_panic(expected = "discovery reconnect delay must be positive")]
101 fn reconnect_backoff_rejects_zero_initial_delay() {
102 DiscoveryReconnectBackoff::new(Duration::ZERO, Duration::from_secs(1), Duration::ZERO);
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
112pub struct DiscoveredEndpoint {
113 uri: String,
114 weight: u32,
115 metadata: BTreeMap<String, String>,
116}
117
118impl DiscoveredEndpoint {
119 pub fn new(uri: impl Into<String>) -> Result<Self, DiscoveryError> {
120 Self::weighted(uri, 1)
121 }
122
123 pub fn weighted(uri: impl Into<String>, weight: u32) -> Result<Self, DiscoveryError> {
124 Ok(Self {
125 uri: validate_endpoint(uri.into())?,
126 weight: validate_weight(weight)?,
127 metadata: BTreeMap::new(),
128 })
129 }
130
131 pub fn with_metadata(mut self, metadata: impl IntoIterator<Item = (String, String)>) -> Self {
132 self.metadata = metadata.into_iter().collect();
133 self
134 }
135
136 pub fn uri(&self) -> &str {
137 &self.uri
138 }
139
140 pub fn weight(&self) -> u32 {
141 self.weight
142 }
143
144 pub fn metadata(&self) -> &BTreeMap<String, String> {
145 &self.metadata
146 }
147}
148
149pub type EndpointChangeFuture<'a, E> =
151 Pin<Box<dyn Future<Output = Result<Vec<String>, E>> + Send + 'a>>;
152
153pub trait EndpointSubscription: Send + 'static {
158 type Error: Send + 'static;
159
160 fn endpoints(&self) -> Vec<String>;
162
163 fn discovered_endpoints(&self) -> Vec<DiscoveredEndpoint> {
167 self.endpoints()
168 .into_iter()
169 .filter_map(|uri| DiscoveredEndpoint::new(uri).ok())
170 .collect()
171 }
172
173 fn changed(&mut self) -> EndpointChangeFuture<'_, Self::Error>;
175}
176
177#[derive(Clone)]
179pub struct ServiceRegistry {
180 state: Arc<RegistryState>,
181}
182
183impl Default for ServiceRegistry {
184 fn default() -> Self {
185 let (changes, _) = broadcast::channel(EVENT_BUFFER_SIZE);
186 Self {
187 state: Arc::new(RegistryState {
188 services: Mutex::new(HashMap::new()),
189 changes,
190 }),
191 }
192 }
193}
194
195impl ServiceRegistry {
196 pub fn new() -> Self {
197 Self::default()
198 }
199
200 pub fn publish(
202 &self,
203 service: impl Into<String>,
204 endpoint: impl Into<String>,
205 ) -> Result<ServiceLease, DiscoveryError> {
206 self.publish_endpoint(service, DiscoveredEndpoint::new(endpoint)?)
207 }
208
209 pub fn publish_weighted(
211 &self,
212 service: impl Into<String>,
213 endpoint: impl Into<String>,
214 weight: u32,
215 ) -> Result<ServiceLease, DiscoveryError> {
216 self.publish_endpoint(service, DiscoveredEndpoint::weighted(endpoint, weight)?)
217 }
218
219 pub fn publish_endpoint(
221 &self,
222 service: impl Into<String>,
223 endpoint: DiscoveredEndpoint,
224 ) -> Result<ServiceLease, DiscoveryError> {
225 let service = validate_service(service.into())?;
226 let uri = endpoint.uri.clone();
227 let added = {
228 let mut services = self
229 .state
230 .services
231 .lock()
232 .expect("service registry mutex poisoned");
233 let endpoints = services.entry(service.clone()).or_default();
234 match endpoints.get_mut(&uri) {
235 Some(entry) if entry.endpoint != endpoint => {
236 return Err(DiscoveryError::ConflictingEndpointMetadata(uri));
237 }
238 Some(entry) => {
239 entry.references += 1;
240 false
241 }
242 None => {
243 endpoints.insert(
244 uri.clone(),
245 RegistryEndpoint {
246 endpoint,
247 references: 1,
248 },
249 );
250 true
251 }
252 }
253 };
254
255 if added {
256 let _ = self.state.changes.send(ServiceEvent::Added {
257 service: service.clone(),
258 endpoint: uri.clone(),
259 });
260 }
261
262 Ok(ServiceLease {
263 state: Arc::clone(&self.state),
264 service,
265 endpoint: uri,
266 active: true,
267 })
268 }
269
270 pub fn endpoints(&self, service: &str) -> Result<Vec<String>, DiscoveryError> {
272 let service = validate_service(service.to_owned())?;
273 let services = self
274 .state
275 .services
276 .lock()
277 .expect("service registry mutex poisoned");
278 Ok(services
279 .get(&service)
280 .into_iter()
281 .flat_map(|endpoints| endpoints.keys())
282 .cloned()
283 .collect())
284 }
285
286 pub fn discovered_endpoints(
288 &self,
289 service: &str,
290 ) -> Result<Vec<DiscoveredEndpoint>, DiscoveryError> {
291 let service = validate_service(service.to_owned())?;
292 let services = self
293 .state
294 .services
295 .lock()
296 .expect("service registry mutex poisoned");
297 Ok(services
298 .get(&service)
299 .into_iter()
300 .flat_map(|endpoints| endpoints.values())
301 .map(|entry| entry.endpoint.clone())
302 .collect())
303 }
304
305 pub fn subscribe(
307 &self,
308 service: impl Into<String>,
309 ) -> Result<ServiceSubscription, DiscoveryError> {
310 let service = validate_service(service.into())?;
311 let receiver = self.state.changes.subscribe();
312 let endpoints = self
313 .state
314 .services
315 .lock()
316 .expect("service registry mutex poisoned")
317 .get(&service)
318 .into_iter()
319 .flat_map(|endpoints| endpoints.keys())
320 .cloned()
321 .collect();
322
323 Ok(ServiceSubscription {
324 service,
325 endpoints,
326 receiver,
327 state: Arc::clone(&self.state),
328 })
329 }
330}
331
332struct RegistryEndpoint {
333 endpoint: DiscoveredEndpoint,
334 references: usize,
335}
336
337struct RegistryState {
338 services: Mutex<HashMap<String, BTreeMap<String, RegistryEndpoint>>>,
339 changes: broadcast::Sender<ServiceEvent>,
340}
341
342pub struct ServiceLease {
344 state: Arc<RegistryState>,
345 service: String,
346 endpoint: String,
347 active: bool,
348}
349
350impl fmt::Debug for ServiceLease {
351 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
352 formatter
353 .debug_struct("ServiceLease")
354 .field("service", &self.service)
355 .field("endpoint", &self.endpoint)
356 .field("active", &self.active)
357 .finish_non_exhaustive()
358 }
359}
360
361impl ServiceLease {
362 pub fn release(&mut self) {
364 if !self.active {
365 return;
366 }
367 self.active = false;
368
369 let removed = {
370 let mut services = self
371 .state
372 .services
373 .lock()
374 .expect("service registry mutex poisoned");
375 let Some(endpoints) = services.get_mut(&self.service) else {
376 return;
377 };
378 let Some(entry) = endpoints.get_mut(&self.endpoint) else {
379 return;
380 };
381
382 entry.references -= 1;
383 if entry.references > 0 {
384 false
385 } else {
386 endpoints.remove(&self.endpoint);
387 if endpoints.is_empty() {
388 services.remove(&self.service);
389 }
390 true
391 }
392 };
393
394 if removed {
395 let _ = self.state.changes.send(ServiceEvent::Removed {
396 service: self.service.clone(),
397 endpoint: self.endpoint.clone(),
398 });
399 }
400 }
401}
402
403impl Drop for ServiceLease {
404 fn drop(&mut self) {
405 self.release();
406 }
407}
408
409pub struct ServiceSubscription {
411 service: String,
412 endpoints: BTreeSet<String>,
413 receiver: broadcast::Receiver<ServiceEvent>,
414 state: Arc<RegistryState>,
415}
416
417impl ServiceSubscription {
418 pub fn endpoints(&self) -> Vec<String> {
420 self.endpoints.iter().cloned().collect()
421 }
422
423 pub fn discovered_endpoints(&self) -> Vec<DiscoveredEndpoint> {
424 self.state
425 .services
426 .lock()
427 .expect("service registry mutex poisoned")
428 .get(&self.service)
429 .into_iter()
430 .flat_map(|endpoints| endpoints.values())
431 .map(|entry| entry.endpoint.clone())
432 .collect()
433 }
434
435 pub fn resync(&mut self) {
437 self.endpoints = self
438 .state
439 .services
440 .lock()
441 .expect("service registry mutex poisoned")
442 .get(&self.service)
443 .into_iter()
444 .flat_map(|endpoints| endpoints.keys())
445 .cloned()
446 .collect();
447 }
448
449 pub async fn recv(&mut self) -> Result<ServiceEvent, DiscoveryError> {
451 loop {
452 match self.receiver.recv().await {
453 Ok(event) if event.service() == self.service => {
454 let changed = match &event {
455 ServiceEvent::Added { endpoint, .. } => {
456 self.endpoints.insert(endpoint.clone())
457 }
458 ServiceEvent::Removed { endpoint, .. } => self.endpoints.remove(endpoint),
459 };
460 if changed {
461 return Ok(event);
462 }
463 }
464 Ok(_) => {}
465 Err(broadcast::error::RecvError::Lagged(skipped)) => {
466 return Err(DiscoveryError::SubscriptionLagged(skipped));
467 }
468 Err(broadcast::error::RecvError::Closed) => {
469 return Err(DiscoveryError::RegistryClosed);
470 }
471 }
472 }
473 }
474}
475
476impl EndpointSubscription for ServiceSubscription {
477 type Error = DiscoveryError;
478
479 fn endpoints(&self) -> Vec<String> {
480 ServiceSubscription::endpoints(self)
481 }
482
483 fn discovered_endpoints(&self) -> Vec<DiscoveredEndpoint> {
484 ServiceSubscription::discovered_endpoints(self)
485 }
486
487 fn changed(&mut self) -> EndpointChangeFuture<'_, Self::Error> {
488 Box::pin(async move {
489 match self.recv().await {
490 Ok(_) => Ok(self.endpoints()),
491 Err(DiscoveryError::SubscriptionLagged(_)) => {
492 self.resync();
493 Ok(self.endpoints())
494 }
495 Err(error) => Err(error),
496 }
497 })
498 }
499}
500
501#[derive(Debug, Clone, PartialEq, Eq)]
503pub enum ServiceEvent {
504 Added { service: String, endpoint: String },
505 Removed { service: String, endpoint: String },
506}
507
508impl ServiceEvent {
509 fn service(&self) -> &str {
510 match self {
511 Self::Added { service, .. } | Self::Removed { service, .. } => service,
512 }
513 }
514}
515
516#[derive(Debug, Clone, PartialEq, Eq)]
518pub enum DiscoveryError {
519 EmptyService,
520 EmptyEndpoint,
521 InvalidEndpointWeight(u32),
522 ConflictingEndpointMetadata(String),
523 SubscriptionLagged(u64),
524 RegistryClosed,
525}
526
527impl fmt::Display for DiscoveryError {
528 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
529 match self {
530 Self::EmptyService => formatter.write_str("service name cannot be empty"),
531 Self::EmptyEndpoint => formatter.write_str("service endpoint cannot be empty"),
532 Self::InvalidEndpointWeight(weight) => write!(
533 formatter,
534 "service endpoint weight must be between 1 and {MAX_ENDPOINT_WEIGHT}, got {weight}"
535 ),
536 Self::ConflictingEndpointMetadata(endpoint) => write!(
537 formatter,
538 "service endpoint {endpoint} is already published with different metadata"
539 ),
540 Self::SubscriptionLagged(skipped) => {
541 write!(formatter, "service subscription lagged by {skipped} events")
542 }
543 Self::RegistryClosed => formatter.write_str("service registry has closed"),
544 }
545 }
546}
547
548impl std::error::Error for DiscoveryError {}
549
550fn validate_service(service: String) -> Result<String, DiscoveryError> {
551 if service.trim().is_empty() {
552 Err(DiscoveryError::EmptyService)
553 } else {
554 Ok(service)
555 }
556}
557
558fn validate_endpoint(endpoint: String) -> Result<String, DiscoveryError> {
559 if endpoint.trim().is_empty() {
560 Err(DiscoveryError::EmptyEndpoint)
561 } else {
562 Ok(endpoint)
563 }
564}
565
566fn validate_weight(weight: u32) -> Result<u32, DiscoveryError> {
567 if (1..=MAX_ENDPOINT_WEIGHT).contains(&weight) {
568 Ok(weight)
569 } else {
570 Err(DiscoveryError::InvalidEndpointWeight(weight))
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::{
577 DiscoveredEndpoint, DiscoveryError, EndpointSubscription, ServiceEvent, ServiceRegistry,
578 };
579
580 #[tokio::test]
581 async fn publishes_and_withdraws_endpoints() {
582 let registry = ServiceRegistry::new();
583 let mut subscription = registry.subscribe("users").unwrap();
584 let mut first = registry.publish("users", "http://users-a:8080").unwrap();
585 let second = registry.publish("users", "http://users-b:8080").unwrap();
586
587 assert_eq!(
588 subscription.recv().await.unwrap(),
589 ServiceEvent::Added {
590 service: "users".to_owned(),
591 endpoint: "http://users-a:8080".to_owned(),
592 }
593 );
594 assert_eq!(
595 subscription.recv().await.unwrap(),
596 ServiceEvent::Added {
597 service: "users".to_owned(),
598 endpoint: "http://users-b:8080".to_owned(),
599 }
600 );
601 assert_eq!(
602 subscription.endpoints(),
603 vec![
604 "http://users-a:8080".to_owned(),
605 "http://users-b:8080".to_owned()
606 ]
607 );
608
609 first.release();
610 assert_eq!(
611 subscription.recv().await.unwrap(),
612 ServiceEvent::Removed {
613 service: "users".to_owned(),
614 endpoint: "http://users-a:8080".to_owned(),
615 }
616 );
617 assert_eq!(
618 registry.endpoints("users").unwrap(),
619 vec!["http://users-b:8080"]
620 );
621
622 drop(second);
623 }
624
625 #[tokio::test]
626 async fn keeps_endpoint_published_until_its_last_lease_is_released() {
627 let registry = ServiceRegistry::new();
628 let mut subscription = registry.subscribe("users").unwrap();
629 let first = registry.publish("users", "http://users-a:8080").unwrap();
630 let second = registry.publish("users", "http://users-a:8080").unwrap();
631
632 assert!(matches!(
633 subscription.recv().await,
634 Ok(ServiceEvent::Added { .. })
635 ));
636 drop(first);
637 assert_eq!(
638 registry.endpoints("users").unwrap(),
639 vec!["http://users-a:8080"]
640 );
641
642 drop(second);
643 assert!(matches!(
644 subscription.recv().await,
645 Ok(ServiceEvent::Removed { .. })
646 ));
647 }
648
649 #[test]
650 fn rejects_empty_service_names_and_endpoints() {
651 let registry = ServiceRegistry::new();
652
653 assert_eq!(
654 registry.publish("", "http://users:8080").unwrap_err(),
655 DiscoveryError::EmptyService
656 );
657 assert_eq!(
658 registry.publish("users", " ").unwrap_err(),
659 DiscoveryError::EmptyEndpoint
660 );
661 assert_eq!(
662 registry
663 .publish_weighted("users", "http://users:8080", 0)
664 .unwrap_err(),
665 DiscoveryError::InvalidEndpointWeight(0)
666 );
667 }
668
669 #[test]
670 fn preserves_weighted_endpoint_metadata_in_subscriptions() {
671 let registry = ServiceRegistry::new();
672 let endpoint = DiscoveredEndpoint::weighted("http://users:8080", 3)
673 .unwrap()
674 .with_metadata([("zone".to_owned(), "east".to_owned())]);
675 let _lease = registry
676 .publish_endpoint("users", endpoint.clone())
677 .unwrap();
678 let subscription = registry.subscribe("users").unwrap();
679
680 assert_eq!(subscription.discovered_endpoints(), vec![endpoint.clone()]);
681 assert_eq!(
682 EndpointSubscription::discovered_endpoints(&subscription),
683 vec![endpoint]
684 );
685 }
686
687 #[test]
688 fn rejects_conflicting_metadata_for_the_same_live_endpoint() {
689 let registry = ServiceRegistry::new();
690 let _lease = registry
691 .publish_weighted("users", "http://users:8080", 2)
692 .unwrap();
693
694 assert_eq!(
695 registry
696 .publish_weighted("users", "http://users:8080", 3)
697 .unwrap_err(),
698 DiscoveryError::ConflictingEndpointMetadata("http://users:8080".to_owned())
699 );
700 }
701}