1use std::{
7 collections::{BTreeMap, BTreeSet},
8 error::Error as StdError,
9 fmt,
10 future::Future,
11 net::SocketAddr,
12 time::Duration,
13};
14
15use rust_zero_core::{
16 DiscoveredEndpoint, DiscoveryError, EndpointSubscription, HealthRegistry, ServiceRegistry,
17};
18use serde::{Deserialize, Serialize};
19use tonic::transport::{Channel, Endpoint, Server};
20use tower::discover::Change;
21
22pub mod auth;
23pub mod metrics;
24pub mod resilience;
25pub mod stack;
26pub mod trace;
27
28pub mod echo {
29 tonic::include_proto!("rust_zero.echo");
30}
31
32pub use auth::{BearerToken, RpcBearerAuth, RpcJwtAuth, RpcRequestSignatureAuth, RpcRequestSigner};
33pub use metrics::{RpcMetricMode, RpcMetrics, RpcMetricsLayer};
34pub use resilience::{acceptable_status, circuit_outcome, RpcCircuitBreaker, RpcLoadShedder};
35pub use rust_zero_core::{AuthFailure, JwtClaimProjection, RequestSignatureVerifier};
36pub use stack::{
37 RpcClientStack, RpcClientStackBuilder, RpcClientStackService, RpcServerStack,
38 RpcServerStackBuilder,
39};
40pub use tonic_health::server::{health_reporter, HealthReporter};
41pub use trace::RpcTrace;
42#[cfg(feature = "telemetry")]
43pub use trace::{RpcTelemetryLayer, RpcTelemetryMode};
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(default)]
48pub struct RpcServerConfig {
49 address: SocketAddr,
50 #[serde(rename = "request_timeout_ms", with = "optional_duration_millis")]
51 request_timeout: Option<Duration>,
52 concurrency_limit: Option<usize>,
53 max_concurrent_streams: Option<u32>,
54 #[serde(rename = "shutdown_timeout_ms", with = "duration_millis")]
55 shutdown_timeout: Duration,
56}
57
58impl Default for RpcServerConfig {
59 fn default() -> Self {
60 Self::new(
61 "0.0.0.0:50051"
62 .parse()
63 .expect("default RPC address is valid"),
64 )
65 }
66}
67
68impl RpcServerConfig {
69 pub fn new(address: SocketAddr) -> Self {
70 Self {
71 address,
72 request_timeout: None,
73 concurrency_limit: None,
74 max_concurrent_streams: None,
75 shutdown_timeout: Duration::from_secs(30),
76 }
77 }
78
79 pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
80 assert!(
81 !timeout.is_zero(),
82 "request timeout must be greater than zero"
83 );
84 self.request_timeout = Some(timeout);
85 self
86 }
87
88 pub fn with_concurrency_limit(mut self, limit: usize) -> Self {
89 assert!(limit > 0, "concurrency limit must be greater than zero");
90 self.concurrency_limit = Some(limit);
91 self
92 }
93
94 pub fn with_max_concurrent_streams(mut self, limit: u32) -> Self {
95 assert!(
96 limit > 0,
97 "maximum concurrent streams must be greater than zero"
98 );
99 self.max_concurrent_streams = Some(limit);
100 self
101 }
102
103 pub fn with_shutdown_timeout(mut self, timeout: Duration) -> Self {
104 assert!(
105 !timeout.is_zero(),
106 "shutdown timeout must be greater than zero"
107 );
108 self.shutdown_timeout = timeout;
109 self
110 }
111
112 pub fn address(&self) -> SocketAddr {
113 self.address
114 }
115
116 pub fn shutdown_timeout(&self) -> Duration {
117 self.shutdown_timeout
118 }
119
120 pub fn validate(&self) -> Result<(), RpcConfigError> {
121 if self
122 .request_timeout
123 .is_some_and(|duration| duration.is_zero())
124 {
125 return Err(RpcConfigError::Invalid(
126 "request timeout must be greater than zero",
127 ));
128 }
129 if self.concurrency_limit == Some(0) {
130 return Err(RpcConfigError::Invalid(
131 "concurrency limit must be greater than zero",
132 ));
133 }
134 if self.max_concurrent_streams == Some(0) {
135 return Err(RpcConfigError::Invalid(
136 "maximum concurrent streams must be greater than zero",
137 ));
138 }
139 if self.shutdown_timeout.is_zero() {
140 return Err(RpcConfigError::Invalid(
141 "shutdown timeout must be greater than zero",
142 ));
143 }
144 Ok(())
145 }
146}
147
148#[derive(Debug, Clone)]
150pub struct RpcServer {
151 config: RpcServerConfig,
152}
153
154impl RpcServer {
155 pub fn new(config: RpcServerConfig) -> Self {
156 Self { config }
157 }
158
159 pub fn try_new(config: RpcServerConfig) -> Result<Self, RpcConfigError> {
160 config.validate()?;
161 Ok(Self { config })
162 }
163
164 pub fn config(&self) -> &RpcServerConfig {
165 &self.config
166 }
167
168 pub fn router(&self) -> Server {
170 let mut server = Server::builder();
171
172 if let Some(timeout) = self.config.request_timeout {
173 server = server.timeout(timeout);
174 }
175 if let Some(limit) = self.config.concurrency_limit {
176 server = server.concurrency_limit_per_connection(limit);
177 }
178 if let Some(limit) = self.config.max_concurrent_streams {
179 server = server.max_concurrent_streams(Some(limit));
180 }
181
182 server
183 }
184
185 pub async fn serve_with_shutdown<F>(
187 &self,
188 router: tonic::transport::server::Router,
189 signal: F,
190 ) -> Result<(), RpcServerError>
191 where
192 F: Future<Output = ()>,
193 {
194 self.config
195 .validate()
196 .map_err(RpcServerError::Configuration)?;
197 let (stop, stopped) = tokio::sync::oneshot::channel::<()>();
198 let serving = router.serve_with_shutdown(self.config.address, async move {
199 let _ = stopped.await;
200 });
201 tokio::pin!(serving);
202 tokio::pin!(signal);
203
204 tokio::select! {
205 result = &mut serving => result.map_err(RpcServerError::Transport),
206 _ = &mut signal => {
207 let _ = stop.send(());
208 tokio::time::timeout(self.config.shutdown_timeout, serving)
209 .await
210 .map_err(|_| RpcServerError::ShutdownTimeout)?
211 .map_err(RpcServerError::Transport)
212 }
213 }
214 }
215}
216
217#[derive(Debug)]
218pub enum RpcServerError {
219 Configuration(RpcConfigError),
220 Transport(tonic::transport::Error),
221 ShutdownTimeout,
222}
223
224impl fmt::Display for RpcServerError {
225 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
226 match self {
227 Self::Configuration(error) => write!(formatter, "invalid gRPC configuration: {error}"),
228 Self::Transport(error) => write!(formatter, "gRPC server transport error: {error}"),
229 Self::ShutdownTimeout => formatter.write_str("gRPC graceful shutdown timed out"),
230 }
231 }
232}
233
234impl StdError for RpcServerError {
235 fn source(&self) -> Option<&(dyn StdError + 'static)> {
236 match self {
237 Self::Configuration(error) => Some(error),
238 Self::Transport(error) => Some(error),
239 Self::ShutdownTimeout => None,
240 }
241 }
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
246#[serde(default)]
247pub struct RpcClientConfig {
248 uri: String,
249 #[serde(rename = "request_timeout_ms", with = "optional_duration_millis")]
250 request_timeout: Option<Duration>,
251 #[serde(rename = "connect_timeout_ms", with = "optional_duration_millis")]
252 connect_timeout: Option<Duration>,
253 concurrency_limit: Option<usize>,
254 #[serde(rename = "tcp_keepalive_ms", with = "optional_duration_millis")]
255 tcp_keepalive: Option<Duration>,
256 #[serde(
257 rename = "http2_keepalive_interval_ms",
258 with = "optional_duration_millis"
259 )]
260 http2_keepalive_interval: Option<Duration>,
261 #[serde(rename = "keepalive_timeout_ms", with = "optional_duration_millis")]
262 keepalive_timeout: Option<Duration>,
263 keepalive_while_idle: bool,
264 #[serde(
265 rename = "discovery_health_interval_ms",
266 with = "optional_duration_millis"
267 )]
268 discovery_health_interval: Option<Duration>,
269 #[serde(
270 rename = "discovery_health_timeout_ms",
271 with = "optional_duration_millis"
272 )]
273 discovery_health_timeout: Option<Duration>,
274}
275
276impl Default for RpcClientConfig {
277 fn default() -> Self {
278 Self::new(String::new())
279 }
280}
281
282impl RpcClientConfig {
283 pub fn new(uri: impl Into<String>) -> Self {
284 Self {
285 uri: uri.into(),
286 request_timeout: None,
287 connect_timeout: None,
288 concurrency_limit: None,
289 tcp_keepalive: None,
290 http2_keepalive_interval: None,
291 keepalive_timeout: None,
292 keepalive_while_idle: false,
293 discovery_health_interval: None,
294 discovery_health_timeout: None,
295 }
296 }
297
298 pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
299 assert!(
300 !timeout.is_zero(),
301 "request timeout must be greater than zero"
302 );
303 self.request_timeout = Some(timeout);
304 self
305 }
306
307 pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
308 assert!(
309 !timeout.is_zero(),
310 "connect timeout must be greater than zero"
311 );
312 self.connect_timeout = Some(timeout);
313 self
314 }
315
316 pub fn with_concurrency_limit(mut self, limit: usize) -> Self {
317 assert!(limit > 0, "concurrency limit must be greater than zero");
318 self.concurrency_limit = Some(limit);
319 self
320 }
321
322 pub fn with_tcp_keepalive(mut self, interval: Duration) -> Self {
323 assert!(
324 !interval.is_zero(),
325 "TCP keepalive interval must be greater than zero"
326 );
327 self.tcp_keepalive = Some(interval);
328 self
329 }
330
331 pub fn with_http2_keepalive(mut self, interval: Duration, timeout: Duration) -> Self {
332 assert!(
333 !interval.is_zero(),
334 "HTTP/2 keepalive interval must be greater than zero"
335 );
336 assert!(
337 !timeout.is_zero(),
338 "HTTP/2 keepalive timeout must be greater than zero"
339 );
340 self.http2_keepalive_interval = Some(interval);
341 self.keepalive_timeout = Some(timeout);
342 self
343 }
344
345 pub fn keepalive_while_idle(mut self, enabled: bool) -> Self {
346 self.keepalive_while_idle = enabled;
347 self
348 }
349
350 pub fn with_discovery_health_check(mut self, interval: Duration, timeout: Duration) -> Self {
352 assert!(
353 !interval.is_zero(),
354 "discovery health interval must be positive"
355 );
356 assert!(
357 !timeout.is_zero(),
358 "discovery health timeout must be positive"
359 );
360 self.discovery_health_interval = Some(interval);
361 self.discovery_health_timeout = Some(timeout);
362 self
363 }
364
365 pub fn validate(&self) -> Result<(), RpcConfigError> {
366 if self.uri.trim().is_empty() {
367 return Err(RpcConfigError::Invalid("client URI must not be empty"));
368 }
369 for (name, duration) in [
370 ("request timeout", self.request_timeout),
371 ("connect timeout", self.connect_timeout),
372 ("TCP keepalive interval", self.tcp_keepalive),
373 ("HTTP/2 keepalive interval", self.http2_keepalive_interval),
374 ("HTTP/2 keepalive timeout", self.keepalive_timeout),
375 ("discovery health interval", self.discovery_health_interval),
376 ("discovery health timeout", self.discovery_health_timeout),
377 ] {
378 if duration.is_some_and(|duration| duration.is_zero()) {
379 return Err(RpcConfigError::Invalid(match name {
380 "request timeout" => "request timeout must be greater than zero",
381 "connect timeout" => "connect timeout must be greater than zero",
382 "TCP keepalive interval" => "TCP keepalive interval must be greater than zero",
383 "HTTP/2 keepalive interval" => {
384 "HTTP/2 keepalive interval must be greater than zero"
385 }
386 "discovery health interval" => {
387 "discovery health interval must be greater than zero"
388 }
389 "discovery health timeout" => {
390 "discovery health timeout must be greater than zero"
391 }
392 _ => "HTTP/2 keepalive timeout must be greater than zero",
393 }));
394 }
395 }
396 if self.concurrency_limit == Some(0) {
397 return Err(RpcConfigError::Invalid(
398 "concurrency limit must be greater than zero",
399 ));
400 }
401 if self.http2_keepalive_interval.is_some() != self.keepalive_timeout.is_some() {
402 return Err(RpcConfigError::Invalid(
403 "HTTP/2 keepalive interval and timeout must be configured together",
404 ));
405 }
406 if self.discovery_health_interval.is_some() != self.discovery_health_timeout.is_some() {
407 return Err(RpcConfigError::Invalid(
408 "discovery health interval and timeout must be configured together",
409 ));
410 }
411 Ok(())
412 }
413}
414
415#[derive(Debug, Clone, Copy, PartialEq, Eq)]
417pub enum DiscoveryReadiness {
418 Empty,
419 Ready,
420 Degraded,
421}
422
423#[derive(Debug, Clone, Copy, PartialEq, Eq)]
424pub struct DiscoveryStatusSnapshot {
425 pub readiness: DiscoveryReadiness,
426 pub discovered: usize,
427 pub available: usize,
428 pub rejected: usize,
429}
430
431impl DiscoveryStatusSnapshot {
432 pub fn is_ready(self) -> bool {
433 self.readiness == DiscoveryReadiness::Ready
434 }
435}
436
437#[derive(Debug, Clone)]
439pub struct DiscoveryStatus {
440 receiver: tokio::sync::watch::Receiver<DiscoveryStatusSnapshot>,
441}
442
443impl DiscoveryStatus {
444 pub fn snapshot(&self) -> DiscoveryStatusSnapshot {
445 *self.receiver.borrow()
446 }
447
448 pub async fn changed(
449 &mut self,
450 ) -> Result<DiscoveryStatusSnapshot, tokio::sync::watch::error::RecvError> {
451 self.receiver.changed().await?;
452 Ok(self.snapshot())
453 }
454
455 pub fn project_to_health(
457 mut self,
458 registry: HealthRegistry,
459 dependency: impl Into<String>,
460 ) -> tokio::task::JoinHandle<()> {
461 let dependency = dependency.into();
462 tokio::spawn(async move {
463 registry.set(&dependency, self.snapshot().is_ready());
464 while self.receiver.changed().await.is_ok() {
465 registry.set(&dependency, self.snapshot().is_ready());
466 }
467 registry.set(dependency, false);
468 })
469 }
470
471 pub fn project_to_grpc_health(
473 mut self,
474 mut reporter: HealthReporter,
475 service_name: impl Into<String>,
476 ) -> tokio::task::JoinHandle<()> {
477 let service_name = service_name.into();
478 tokio::spawn(async move {
479 loop {
480 let serving = if self.snapshot().is_ready() {
481 tonic_health::ServingStatus::Serving
482 } else {
483 tonic_health::ServingStatus::NotServing
484 };
485 reporter.set_service_status(&service_name, serving).await;
486 if self.receiver.changed().await.is_err() {
487 reporter
488 .set_service_status(&service_name, tonic_health::ServingStatus::NotServing)
489 .await;
490 return;
491 }
492 }
493 })
494 }
495}
496
497#[derive(Debug, Clone)]
499pub struct RpcClient {
500 config: RpcClientConfig,
501}
502
503impl RpcClient {
504 pub fn new(config: RpcClientConfig) -> Self {
505 Self { config }
506 }
507
508 pub fn try_new(config: RpcClientConfig) -> Result<Self, RpcConfigError> {
509 config.validate()?;
510 Ok(Self { config })
511 }
512
513 pub fn config(&self) -> &RpcClientConfig {
514 &self.config
515 }
516
517 pub async fn connect(&self) -> Result<Channel, RpcClientError> {
518 self.config
519 .validate()
520 .map_err(RpcClientError::Configuration)?;
521 let endpoint = self.endpoint(self.config.uri.clone())?;
522 endpoint.connect().await.map_err(RpcClientError::Transport)
523 }
524
525 pub fn connect_service(
531 &self,
532 registry: &ServiceRegistry,
533 service: impl Into<String>,
534 ) -> Result<Channel, RpcClientError> {
535 let subscription = registry
536 .subscribe(service)
537 .map_err(RpcClientError::Discovery)?;
538 Ok(self.connect_discovered(subscription))
539 }
540
541 pub fn connect_discovered<S>(&self, subscription: S) -> Channel
547 where
548 S: EndpointSubscription,
549 {
550 self.connect_discovered_with_status(subscription).0
551 }
552
553 pub fn connect_discovered_with_status<S>(
558 &self,
559 mut subscription: S,
560 ) -> (Channel, DiscoveryStatus)
561 where
562 S: EndpointSubscription,
563 {
564 let initial = subscription.discovered_endpoints();
565 let (configured, rejected) = self.configure_discovered(initial);
566
567 let capacity = configured
568 .values()
569 .map(|(_, endpoint)| endpoint.weight() as usize)
570 .sum::<usize>()
571 .max(128);
572 let (channel, changes) = Channel::balance_channel(capacity);
573 let mut installed = BTreeSet::new();
574 for (uri, (endpoint, discovered)) in &configured {
575 for slot in 0..discovered.weight() {
576 let key = weighted_key(uri, slot);
577 installed.insert(key.clone());
578 changes
579 .try_send(Change::Insert(key, endpoint.clone()))
580 .expect(
581 "discovery channel is sized for its weighted initial endpoint snapshot",
582 );
583 }
584 }
585 let initial_status = discovery_status(configured.len(), configured.len(), rejected);
586 let (status_updates, status_receiver) = tokio::sync::watch::channel(initial_status);
587
588 let client = self.clone();
589 tokio::spawn(async move {
590 let mut configured = configured;
591 let mut available: BTreeSet<String> = configured.keys().cloned().collect();
592 let mut rejected = rejected;
593 let mut health_ticks = client.discovery_health_ticks();
594 loop {
595 tokio::select! {
596 _ = changes.closed() => return,
597 snapshot = subscription.changed() => {
598 if snapshot.is_err() {
599 let mut closed = discovery_status(
600 configured.len(),
601 0,
602 rejected,
603 );
604 closed.readiness = DiscoveryReadiness::Degraded;
605 status_updates.send_replace(closed);
606 return;
607 }
608 let (next, next_rejected) =
609 client.configure_discovered(subscription.discovered_endpoints());
610 configured = next;
611 rejected = next_rejected;
612 available.retain(|uri| configured.contains_key(uri));
613 available.extend(configured.keys().cloned());
614 }
615 _ = health_ticks.tick(), if client.config.discovery_health_interval.is_some() => {
616 available = client.probe_discovered(&configured).await;
617 }
618 }
619
620 let desired = weighted_keys(&configured, &available);
621 for key in installed.difference(&desired).cloned().collect::<Vec<_>>() {
622 if changes.send(Change::Remove(key.clone())).await.is_err() {
623 return;
624 }
625 installed.remove(&key);
626 }
627 for key in desired.difference(&installed).cloned().collect::<Vec<_>>() {
628 let Some((uri, _)) = key.rsplit_once('\0') else {
629 continue;
630 };
631 let Some((endpoint, _)) = configured.get(uri) else {
632 continue;
633 };
634 if changes
635 .send(Change::Insert(key.clone(), endpoint.clone()))
636 .await
637 .is_err()
638 {
639 return;
640 }
641 installed.insert(key);
642 }
643 status_updates.send_replace(discovery_status(
644 configured.len(),
645 available.len(),
646 rejected,
647 ));
648 }
649 });
650
651 (
652 channel,
653 DiscoveryStatus {
654 receiver: status_receiver,
655 },
656 )
657 }
658
659 fn configure_discovered(
660 &self,
661 endpoints: Vec<DiscoveredEndpoint>,
662 ) -> (BTreeMap<String, (Endpoint, DiscoveredEndpoint)>, usize) {
663 let discovered = endpoints.len();
664 let configured: BTreeMap<_, _> = endpoints
665 .into_iter()
666 .filter_map(|discovered| {
667 self.endpoint(discovered.uri().to_owned())
668 .ok()
669 .map(|endpoint| (discovered.uri().to_owned(), (endpoint, discovered)))
670 })
671 .collect();
672 let rejected = discovered.saturating_sub(configured.len());
673 (configured, rejected)
674 }
675
676 fn discovery_health_ticks(&self) -> tokio::time::Interval {
677 let interval = self
678 .config
679 .discovery_health_interval
680 .unwrap_or(Duration::from_secs(86_400));
681 let mut ticks = tokio::time::interval(interval);
682 ticks.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
683 ticks
684 }
685
686 async fn probe_discovered(
687 &self,
688 configured: &BTreeMap<String, (Endpoint, DiscoveredEndpoint)>,
689 ) -> BTreeSet<String> {
690 let timeout = self
691 .config
692 .discovery_health_timeout
693 .expect("probe timeout configured");
694 let probes = configured.iter().map(|(uri, (endpoint, _))| {
695 let uri = uri.clone();
696 let endpoint = endpoint.clone();
697 async move {
698 let healthy = tokio::time::timeout(timeout, endpoint.connect())
699 .await
700 .is_ok_and(|result| result.is_ok());
701 (uri, healthy)
702 }
703 });
704 futures::future::join_all(probes)
705 .await
706 .into_iter()
707 .filter_map(|(uri, healthy)| healthy.then_some(uri))
708 .collect()
709 }
710
711 fn endpoint(&self, uri: String) -> Result<Endpoint, RpcClientError> {
712 let mut endpoint = Endpoint::from_shared(uri).map_err(RpcClientError::Transport)?;
713
714 if let Some(timeout) = self.config.request_timeout {
715 endpoint = endpoint.timeout(timeout);
716 }
717 if let Some(timeout) = self.config.connect_timeout {
718 endpoint = endpoint.connect_timeout(timeout);
719 }
720 if let Some(limit) = self.config.concurrency_limit {
721 endpoint = endpoint.concurrency_limit(limit);
722 }
723 if let Some(interval) = self.config.tcp_keepalive {
724 endpoint = endpoint.tcp_keepalive(Some(interval));
725 }
726 if let Some(interval) = self.config.http2_keepalive_interval {
727 endpoint = endpoint.http2_keep_alive_interval(interval);
728 }
729 if let Some(timeout) = self.config.keepalive_timeout {
730 endpoint = endpoint.keep_alive_timeout(timeout);
731 }
732 endpoint = endpoint.keep_alive_while_idle(self.config.keepalive_while_idle);
733
734 Ok(endpoint)
735 }
736}
737
738fn weighted_key(uri: &str, slot: u32) -> String {
739 format!("{uri}\0{slot}")
740}
741
742fn weighted_keys(
743 configured: &BTreeMap<String, (Endpoint, DiscoveredEndpoint)>,
744 available: &BTreeSet<String>,
745) -> BTreeSet<String> {
746 configured
747 .iter()
748 .filter(|(uri, _)| available.contains(*uri))
749 .flat_map(|(uri, (_, endpoint))| {
750 (0..endpoint.weight()).map(move |slot| weighted_key(uri, slot))
751 })
752 .collect()
753}
754
755fn discovery_status(
756 discovered: usize,
757 available: usize,
758 rejected: usize,
759) -> DiscoveryStatusSnapshot {
760 let total = discovered + rejected;
761 let readiness = if total == 0 {
762 DiscoveryReadiness::Empty
763 } else if available == discovered && rejected == 0 {
764 DiscoveryReadiness::Ready
765 } else {
766 DiscoveryReadiness::Degraded
767 };
768 DiscoveryStatusSnapshot {
769 readiness,
770 discovered: total,
771 available,
772 rejected,
773 }
774}
775
776#[derive(Debug)]
777pub enum RpcClientError {
778 Configuration(RpcConfigError),
779 Transport(tonic::transport::Error),
780 Discovery(DiscoveryError),
781}
782
783impl fmt::Display for RpcClientError {
784 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
785 match self {
786 Self::Configuration(error) => write!(formatter, "invalid gRPC configuration: {error}"),
787 Self::Transport(error) => write!(formatter, "gRPC transport error: {error}"),
788 Self::Discovery(error) => write!(formatter, "gRPC service discovery error: {error}"),
789 }
790 }
791}
792
793impl StdError for RpcClientError {
794 fn source(&self) -> Option<&(dyn StdError + 'static)> {
795 match self {
796 Self::Configuration(error) => Some(error),
797 Self::Transport(error) => Some(error),
798 Self::Discovery(error) => Some(error),
799 }
800 }
801}
802
803#[derive(Debug, Clone, PartialEq, Eq)]
804pub enum RpcConfigError {
805 Invalid(&'static str),
806}
807
808impl fmt::Display for RpcConfigError {
809 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
810 match self {
811 Self::Invalid(message) => formatter.write_str(message),
812 }
813 }
814}
815
816impl StdError for RpcConfigError {}
817
818mod duration_millis {
819 use serde::{Deserialize, Deserializer, Serializer};
820 use std::time::Duration;
821
822 pub fn serialize<S>(value: &Duration, serializer: S) -> Result<S::Ok, S::Error>
823 where
824 S: Serializer,
825 {
826 serializer.serialize_u64(value.as_millis().try_into().unwrap_or(u64::MAX))
827 }
828
829 pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
830 where
831 D: Deserializer<'de>,
832 {
833 u64::deserialize(deserializer).map(Duration::from_millis)
834 }
835}
836
837mod optional_duration_millis {
838 use serde::{Deserialize, Deserializer, Serialize, Serializer};
839 use std::time::Duration;
840
841 pub fn serialize<S>(value: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
842 where
843 S: Serializer,
844 {
845 value
846 .map(|duration| duration.as_millis().try_into().unwrap_or(u64::MAX))
847 .serialize(serializer)
848 }
849
850 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
851 where
852 D: Deserializer<'de>,
853 {
854 Option::<u64>::deserialize(deserializer).map(|value| value.map(Duration::from_millis))
855 }
856}
857
858#[cfg(test)]
859mod tests {
860 use super::*;
861 use crate::echo::{
862 echo_client::EchoClient,
863 echo_server::{Echo, EchoServer},
864 EchoRequest, EchoResponse,
865 };
866 use futures::{Stream, StreamExt};
867 use std::{pin::Pin, sync::Arc};
868 use tokio::{
869 net::TcpListener,
870 sync::{oneshot, watch, Notify},
871 };
872 use tokio_stream::wrappers::TcpListenerStream;
873 use tonic::{Request, Response, Status};
874
875 #[test]
876 fn transport_configs_deserialize_millisecond_durations() {
877 let server: RpcServerConfig = rust_zero_core::parse_config(
878 "address = \"127.0.0.1:50052\"\nrequest_timeout_ms = 750\nshutdown_timeout_ms = 5000",
879 rust_zero_core::ConfigFormat::Toml,
880 )
881 .unwrap();
882 assert_eq!(server.request_timeout, Some(Duration::from_millis(750)));
883 assert_eq!(server.shutdown_timeout(), Duration::from_secs(5));
884 server.validate().unwrap();
885
886 let client: RpcClientConfig = rust_zero_core::parse_config(
887 "uri = \"http://127.0.0.1:50052\"\nconnect_timeout_ms = 250",
888 rust_zero_core::ConfigFormat::Toml,
889 )
890 .unwrap();
891 assert_eq!(client.connect_timeout, Some(Duration::from_millis(250)));
892 client.validate().unwrap();
893 }
894
895 #[test]
896 fn transport_configs_reject_incomplete_keepalive_settings() {
897 let client: RpcClientConfig = rust_zero_core::parse_config(
898 r#"{"uri":"http://localhost:50051","http2_keepalive_interval_ms":1000}"#,
899 rust_zero_core::ConfigFormat::Json,
900 )
901 .unwrap();
902 assert!(client.validate().is_err());
903 }
904
905 #[derive(Default)]
906 struct EchoService;
907
908 type EchoStream = Pin<Box<dyn Stream<Item = Result<EchoResponse, Status>> + Send>>;
909
910 #[tonic::async_trait]
911 impl Echo for EchoService {
912 type ServerStreamStream = EchoStream;
913 type BidirectionalStreamStream = EchoStream;
914
915 async fn echo(
916 &self,
917 request: Request<EchoRequest>,
918 ) -> Result<Response<EchoResponse>, Status> {
919 Ok(Response::new(EchoResponse {
920 message: request.into_inner().message,
921 }))
922 }
923
924 async fn server_stream(
925 &self,
926 request: Request<EchoRequest>,
927 ) -> Result<Response<Self::ServerStreamStream>, Status> {
928 Ok(Response::new(Box::pin(tokio_stream::iter([Ok(
929 EchoResponse {
930 message: request.into_inner().message,
931 },
932 )]))))
933 }
934
935 async fn client_stream(
936 &self,
937 request: Request<tonic::Streaming<EchoRequest>>,
938 ) -> Result<Response<EchoResponse>, Status> {
939 let mut input = request.into_inner();
940 let mut messages = Vec::new();
941 while let Some(message) = input.message().await? {
942 messages.push(message.message);
943 }
944 Ok(Response::new(EchoResponse {
945 message: messages.join(","),
946 }))
947 }
948
949 async fn bidirectional_stream(
950 &self,
951 request: Request<tonic::Streaming<EchoRequest>>,
952 ) -> Result<Response<Self::BidirectionalStreamStream>, Status> {
953 let replies = futures::stream::unfold(request.into_inner(), |mut input| async move {
954 match input.message().await {
955 Ok(Some(message)) => Some((
956 Ok(EchoResponse {
957 message: message.message,
958 }),
959 input,
960 )),
961 Err(status) => Some((Err(status), input)),
962 Ok(None) => None,
963 }
964 });
965 Ok(Response::new(Box::pin(replies)))
966 }
967 }
968
969 struct NamedEchoService(&'static str);
970
971 #[tonic::async_trait]
972 impl Echo for NamedEchoService {
973 type ServerStreamStream = EchoStream;
974 type BidirectionalStreamStream = EchoStream;
975
976 async fn echo(&self, _: Request<EchoRequest>) -> Result<Response<EchoResponse>, Status> {
977 Ok(Response::new(EchoResponse {
978 message: self.0.to_owned(),
979 }))
980 }
981
982 async fn server_stream(
983 &self,
984 _: Request<EchoRequest>,
985 ) -> Result<Response<Self::ServerStreamStream>, Status> {
986 Ok(Response::new(Box::pin(tokio_stream::iter([Ok(
987 EchoResponse {
988 message: self.0.to_owned(),
989 },
990 )]))))
991 }
992
993 async fn client_stream(
994 &self,
995 _: Request<tonic::Streaming<EchoRequest>>,
996 ) -> Result<Response<EchoResponse>, Status> {
997 Ok(Response::new(EchoResponse {
998 message: self.0.to_owned(),
999 }))
1000 }
1001
1002 async fn bidirectional_stream(
1003 &self,
1004 _: Request<tonic::Streaming<EchoRequest>>,
1005 ) -> Result<Response<Self::BidirectionalStreamStream>, Status> {
1006 Ok(Response::new(Box::pin(tokio_stream::iter([Ok(
1007 EchoResponse {
1008 message: self.0.to_owned(),
1009 },
1010 )]))))
1011 }
1012 }
1013
1014 struct StackedEchoService;
1015
1016 #[tonic::async_trait]
1017 impl Echo for StackedEchoService {
1018 type ServerStreamStream = EchoStream;
1019 type BidirectionalStreamStream = EchoStream;
1020
1021 async fn echo(
1022 &self,
1023 request: Request<EchoRequest>,
1024 ) -> Result<Response<EchoResponse>, Status> {
1025 assert_eq!(
1026 request.extensions().get::<String>().map(String::as_str),
1027 Some("caller")
1028 );
1029 assert!(request
1030 .extensions()
1031 .get::<rust_zero_core::TraceContext>()
1032 .is_some());
1033 if request.get_ref().message == "panic" {
1034 panic!("intentional handler panic");
1035 }
1036 Ok(Response::new(EchoResponse {
1037 message: request.into_inner().message,
1038 }))
1039 }
1040
1041 async fn server_stream(
1042 &self,
1043 request: Request<EchoRequest>,
1044 ) -> Result<Response<Self::ServerStreamStream>, Status> {
1045 if let Some(error) = stack_extension_error(&request) {
1046 return Err(error);
1047 }
1048 let message = request.into_inner().message;
1049 let stream: EchoStream = match message.as_str() {
1050 "status" => Box::pin(tokio_stream::iter([
1051 Ok(EchoResponse {
1052 message: "first".to_owned(),
1053 }),
1054 Err(Status::unavailable("stream failed")),
1055 ])),
1056 "cancel" => Box::pin(
1057 tokio_stream::once(Ok(EchoResponse {
1058 message: "first".to_owned(),
1059 }))
1060 .chain(futures::stream::pending()),
1061 ),
1062 _ => Box::pin(tokio_stream::iter([Ok(EchoResponse { message })])),
1063 };
1064 Ok(Response::new(stream))
1065 }
1066
1067 async fn client_stream(
1068 &self,
1069 request: Request<tonic::Streaming<EchoRequest>>,
1070 ) -> Result<Response<EchoResponse>, Status> {
1071 if let Some(error) = stack_extension_error(&request) {
1072 return Err(error);
1073 }
1074 EchoService.client_stream(request).await
1075 }
1076
1077 async fn bidirectional_stream(
1078 &self,
1079 request: Request<tonic::Streaming<EchoRequest>>,
1080 ) -> Result<Response<Self::BidirectionalStreamStream>, Status> {
1081 if let Some(error) = stack_extension_error(&request) {
1082 return Err(error);
1083 }
1084 EchoService.bidirectional_stream(request).await
1085 }
1086 }
1087
1088 fn stack_extension_error<T>(request: &Request<T>) -> Option<Status> {
1089 if request.extensions().get::<String>().map(String::as_str) != Some("caller") {
1090 return Some(Status::internal(
1091 "authentication did not run before handler",
1092 ));
1093 }
1094 if request
1095 .extensions()
1096 .get::<rust_zero_core::TraceContext>()
1097 .is_none()
1098 {
1099 return Some(Status::internal("tracing did not run before handler"));
1100 }
1101 None
1102 }
1103
1104 struct DrainEchoService {
1105 entered: Arc<Notify>,
1106 release: Arc<Notify>,
1107 }
1108
1109 #[tonic::async_trait]
1110 impl Echo for DrainEchoService {
1111 type ServerStreamStream = EchoStream;
1112 type BidirectionalStreamStream = EchoStream;
1113
1114 async fn echo(
1115 &self,
1116 request: Request<EchoRequest>,
1117 ) -> Result<Response<EchoResponse>, Status> {
1118 self.entered.notify_one();
1119 self.release.notified().await;
1120 Ok(Response::new(EchoResponse {
1121 message: request.into_inner().message,
1122 }))
1123 }
1124
1125 async fn server_stream(
1126 &self,
1127 _: Request<EchoRequest>,
1128 ) -> Result<Response<Self::ServerStreamStream>, Status> {
1129 Err(Status::unimplemented("not used by drain tests"))
1130 }
1131
1132 async fn client_stream(
1133 &self,
1134 _: Request<tonic::Streaming<EchoRequest>>,
1135 ) -> Result<Response<EchoResponse>, Status> {
1136 Err(Status::unimplemented("not used by drain tests"))
1137 }
1138
1139 async fn bidirectional_stream(
1140 &self,
1141 _: Request<tonic::Streaming<EchoRequest>>,
1142 ) -> Result<Response<Self::BidirectionalStreamStream>, Status> {
1143 Err(Status::unimplemented("not used by drain tests"))
1144 }
1145 }
1146
1147 struct TestSubscription {
1148 receiver: watch::Receiver<Vec<String>>,
1149 dropped: Option<oneshot::Sender<()>>,
1150 }
1151
1152 impl EndpointSubscription for TestSubscription {
1153 type Error = watch::error::RecvError;
1154
1155 fn endpoints(&self) -> Vec<String> {
1156 self.receiver.borrow().clone()
1157 }
1158
1159 fn changed(&mut self) -> rust_zero_core::EndpointChangeFuture<'_, Self::Error> {
1160 Box::pin(async move {
1161 self.receiver.changed().await?;
1162 Ok(self.receiver.borrow().clone())
1163 })
1164 }
1165 }
1166
1167 impl Drop for TestSubscription {
1168 fn drop(&mut self) {
1169 if let Some(dropped) = self.dropped.take() {
1170 let _ = dropped.send(());
1171 }
1172 }
1173 }
1174
1175 #[test]
1176 fn server_configuration_preserves_address_and_limits() {
1177 let address = "127.0.0.1:50051".parse().unwrap();
1178 let config = RpcServerConfig::new(address)
1179 .with_request_timeout(Duration::from_secs(2))
1180 .with_concurrency_limit(32)
1181 .with_max_concurrent_streams(16);
1182
1183 assert_eq!(config.address(), address);
1184 assert_eq!(config.request_timeout, Some(Duration::from_secs(2)));
1185 assert_eq!(config.concurrency_limit, Some(32));
1186 assert_eq!(config.max_concurrent_streams, Some(16));
1187 }
1188
1189 #[tokio::test]
1190 async fn invalid_client_uri_is_reported() {
1191 let client = RpcClient::new(RpcClientConfig::new("not a URI"));
1192 let error = client.connect().await.unwrap_err();
1193
1194 assert!(matches!(error, RpcClientError::Transport(_)));
1195 }
1196
1197 #[tokio::test]
1198 async fn client_and_server_complete_unary_call() {
1199 use std::sync::Arc;
1200 use tower::Layer;
1201
1202 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1203 let address = listener.local_addr().unwrap();
1204 let metrics = Arc::new(rust_zero_core::Metrics::new());
1205 let server_metrics = RpcMetrics::new(
1206 metrics.as_ref(),
1207 "echo",
1208 RpcMetricMode::Server,
1209 ["/rust_zero.echo.Echo/Echo"],
1210 )
1211 .unwrap();
1212 let server = RpcServer::new(
1213 RpcServerConfig::new(address)
1214 .with_request_timeout(Duration::from_secs(1))
1215 .with_concurrency_limit(8),
1216 );
1217 let server_task = tokio::spawn(async move {
1218 server
1219 .router()
1220 .layer(RpcMetricsLayer::new(server_metrics))
1221 .add_service(EchoServer::new(EchoService))
1222 .serve_with_incoming(TcpListenerStream::new(listener))
1223 .await
1224 .unwrap();
1225 });
1226
1227 let channel = RpcClient::new(
1228 RpcClientConfig::new(format!("http://{address}"))
1229 .with_connect_timeout(Duration::from_secs(1))
1230 .with_request_timeout(Duration::from_secs(1)),
1231 )
1232 .connect()
1233 .await
1234 .unwrap();
1235 let client_metrics = RpcMetrics::new(
1236 metrics.as_ref(),
1237 "echo",
1238 RpcMetricMode::Client,
1239 ["/rust_zero.echo.Echo/Echo"],
1240 )
1241 .unwrap();
1242 let client_stack = RpcClientStackBuilder::new(client_metrics)
1243 .with_default_timeout(Duration::from_secs(1))
1244 .with_circuit_breaker(rust_zero_core::CircuitBreakerConfig::new(
1245 3,
1246 Duration::from_secs(30),
1247 ))
1248 .build();
1249 let response = EchoClient::new(client_stack.layer(channel))
1250 .echo(Request::new(EchoRequest {
1251 message: "hello".to_owned(),
1252 }))
1253 .await
1254 .unwrap();
1255
1256 assert_eq!(response.into_inner().message, "hello");
1257 let rendered = metrics.render();
1258 assert!(rendered.contains(
1259 "echo_rpc_server_requests_total{method=\"/rust_zero.echo.Echo/Echo\",code=\"0\"} 1"
1260 ));
1261 assert!(rendered.contains(
1262 "echo_rpc_client_requests_total{method=\"/rust_zero.echo.Echo/Echo\",code=\"0\"} 1"
1263 ));
1264 server_task.abort();
1265 }
1266
1267 async fn connect_eventually(address: SocketAddr) -> Channel {
1268 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
1269 loop {
1270 if let Ok(channel) = RpcClient::new(
1271 RpcClientConfig::new(format!("http://{address}"))
1272 .with_connect_timeout(Duration::from_millis(100)),
1273 )
1274 .connect()
1275 .await
1276 {
1277 return channel;
1278 }
1279 assert!(
1280 tokio::time::Instant::now() < deadline,
1281 "gRPC server did not start listening"
1282 );
1283 tokio::time::sleep(Duration::from_millis(10)).await;
1284 }
1285 }
1286
1287 #[tokio::test]
1288 async fn configured_server_drains_in_flight_calls_and_bounds_shutdown() {
1289 let reservation = TcpListener::bind("127.0.0.1:0").await.unwrap();
1290 let address = reservation.local_addr().unwrap();
1291 drop(reservation);
1292
1293 let entered = Arc::new(Notify::new());
1294 let release = Arc::new(Notify::new());
1295 let server = RpcServer::new(
1296 RpcServerConfig::new(address).with_shutdown_timeout(Duration::from_secs(1)),
1297 );
1298 let router = server
1299 .router()
1300 .add_service(EchoServer::new(DrainEchoService {
1301 entered: Arc::clone(&entered),
1302 release: Arc::clone(&release),
1303 }));
1304 let (shutdown, shutdown_signal) = oneshot::channel();
1305 let server_task = tokio::spawn(async move {
1306 server
1307 .serve_with_shutdown(router, async {
1308 let _ = shutdown_signal.await;
1309 })
1310 .await
1311 });
1312
1313 let channel = connect_eventually(address).await;
1314 let call = tokio::spawn(async move {
1315 EchoClient::new(channel)
1316 .echo(EchoRequest {
1317 message: "drained".to_owned(),
1318 })
1319 .await
1320 });
1321 entered.notified().await;
1322 shutdown.send(()).unwrap();
1323 tokio::task::yield_now().await;
1324 assert!(
1325 !server_task.is_finished(),
1326 "server must wait for an in-flight call"
1327 );
1328 release.notify_one();
1329 assert_eq!(call.await.unwrap().unwrap().into_inner().message, "drained");
1330 tokio::time::timeout(Duration::from_secs(1), server_task)
1331 .await
1332 .expect("server should finish after its in-flight call")
1333 .unwrap()
1334 .unwrap();
1335
1336 let reservation = TcpListener::bind("127.0.0.1:0").await.unwrap();
1337 let address = reservation.local_addr().unwrap();
1338 drop(reservation);
1339 let entered = Arc::new(Notify::new());
1340 let release = Arc::new(Notify::new());
1341 let server = RpcServer::new(
1342 RpcServerConfig::new(address).with_shutdown_timeout(Duration::from_millis(50)),
1343 );
1344 let router = server
1345 .router()
1346 .add_service(EchoServer::new(DrainEchoService {
1347 entered: Arc::clone(&entered),
1348 release: Arc::clone(&release),
1349 }));
1350 let (shutdown, shutdown_signal) = oneshot::channel();
1351 let server_task = tokio::spawn(async move {
1352 server
1353 .serve_with_shutdown(router, async {
1354 let _ = shutdown_signal.await;
1355 })
1356 .await
1357 });
1358 let channel = connect_eventually(address).await;
1359 let call = tokio::spawn(async move {
1360 EchoClient::new(channel)
1361 .echo(EchoRequest {
1362 message: "too-slow".to_owned(),
1363 })
1364 .await
1365 });
1366 entered.notified().await;
1367 shutdown.send(()).unwrap();
1368 assert!(matches!(
1369 server_task.await.unwrap(),
1370 Err(RpcServerError::ShutdownTimeout)
1371 ));
1372 release.notify_one();
1373 let _ = call.await;
1374 }
1375
1376 #[tokio::test]
1377 async fn standard_server_stack_composes_auth_trace_metrics_recovery_and_health() {
1378 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1379 let address = listener.local_addr().unwrap();
1380 let registry = Arc::new(rust_zero_core::Metrics::new());
1381 let metrics = RpcMetrics::new(
1382 registry.as_ref(),
1383 "stacked",
1384 RpcMetricMode::Server,
1385 [
1386 "/rust_zero.echo.Echo/Echo",
1387 "/rust_zero.echo.Echo/ServerStream",
1388 "/rust_zero.echo.Echo/ClientStream",
1389 "/rust_zero.echo.Echo/BidirectionalStream",
1390 ],
1391 )
1392 .unwrap();
1393 let stack = RpcServerStackBuilder::new(metrics)
1394 .with_bearer_auth(|token| (token == "secret").then(|| "caller".to_owned()))
1395 .with_load_shedder(rust_zero_core::LoadShedderConfig::new(
1396 8,
1397 Duration::from_secs(1),
1398 ))
1399 .build();
1400 let (mut reporter, health) = health_reporter();
1401 reporter
1402 .set_serving::<EchoServer<StackedEchoService>>()
1403 .await;
1404 let server = tokio::spawn(async move {
1405 Server::builder()
1406 .layer(stack)
1407 .add_service(health)
1408 .add_service(EchoServer::new(StackedEchoService))
1409 .serve_with_incoming(TcpListenerStream::new(listener))
1410 .await
1411 .unwrap();
1412 });
1413
1414 let channel = RpcClient::new(RpcClientConfig::new(format!("http://{address}")))
1415 .connect()
1416 .await
1417 .unwrap();
1418 let error = EchoClient::new(channel.clone())
1419 .echo(EchoRequest {
1420 message: "denied".into(),
1421 })
1422 .await
1423 .unwrap_err();
1424 assert_eq!(error.code(), tonic::Code::Unauthenticated);
1425
1426 let mut client =
1427 EchoClient::with_interceptor(channel.clone(), BearerToken::new("secret").unwrap());
1428 let response = client
1429 .echo(EchoRequest {
1430 message: "accepted".into(),
1431 })
1432 .await
1433 .unwrap();
1434 assert_eq!(response.into_inner().message, "accepted");
1435 let error = client
1436 .echo(EchoRequest {
1437 message: "panic".into(),
1438 })
1439 .await
1440 .unwrap_err();
1441 assert_eq!(error.code(), tonic::Code::Internal);
1442 let response = client
1443 .echo(EchoRequest {
1444 message: "still-serving".into(),
1445 })
1446 .await
1447 .unwrap();
1448 assert_eq!(response.into_inner().message, "still-serving");
1449
1450 let mut health_client = tonic_health::pb::health_client::HealthClient::with_interceptor(
1451 channel.clone(),
1452 BearerToken::new("secret").unwrap(),
1453 );
1454 let health_request = || tonic_health::pb::HealthCheckRequest {
1455 service: "rust_zero.echo.Echo".to_owned(),
1456 };
1457 assert_eq!(
1458 health_client
1459 .check(health_request())
1460 .await
1461 .unwrap()
1462 .into_inner()
1463 .status,
1464 tonic_health::pb::health_check_response::ServingStatus::Serving as i32
1465 );
1466 reporter
1467 .set_not_serving::<EchoServer<StackedEchoService>>()
1468 .await;
1469 assert_eq!(
1470 health_client
1471 .check(health_request())
1472 .await
1473 .unwrap()
1474 .into_inner()
1475 .status,
1476 tonic_health::pb::health_check_response::ServingStatus::NotServing as i32
1477 );
1478
1479 let client_metrics = RpcMetrics::new(
1480 registry.as_ref(),
1481 "stacked",
1482 RpcMetricMode::Client,
1483 [
1484 "/rust_zero.echo.Echo/Echo",
1485 "/rust_zero.echo.Echo/ServerStream",
1486 "/rust_zero.echo.Echo/ClientStream",
1487 "/rust_zero.echo.Echo/BidirectionalStream",
1488 ],
1489 )
1490 .unwrap();
1491 let client_stack = RpcClientStackBuilder::new(client_metrics)
1492 .with_bearer_token(BearerToken::new("secret").unwrap())
1493 .with_default_timeout(Duration::from_secs(2))
1494 .with_circuit_breaker(rust_zero_core::CircuitBreakerConfig::new(
1495 1,
1496 Duration::from_secs(30),
1497 ))
1498 .build();
1499 let mut client = EchoClient::new(tower::Layer::layer(&client_stack, channel));
1500
1501 let client_stream =
1502 tokio_stream::iter(["one", "two", "three"].map(|message| EchoRequest {
1503 message: message.to_owned(),
1504 }));
1505 assert_eq!(
1506 client
1507 .client_stream(client_stream)
1508 .await
1509 .unwrap()
1510 .into_inner()
1511 .message,
1512 "one,two,three"
1513 );
1514
1515 let bidi_input = tokio_stream::iter(["left", "right"].map(|message| EchoRequest {
1516 message: message.to_owned(),
1517 }));
1518 let mut bidi = client
1519 .bidirectional_stream(bidi_input)
1520 .await
1521 .unwrap()
1522 .into_inner();
1523 assert_eq!(bidi.message().await.unwrap().unwrap().message, "left");
1524 assert_eq!(bidi.message().await.unwrap().unwrap().message, "right");
1525 assert!(bidi.message().await.unwrap().is_none());
1526
1527 let mut cancelled = client
1528 .server_stream(EchoRequest {
1529 message: "cancel".to_owned(),
1530 })
1531 .await
1532 .unwrap()
1533 .into_inner();
1534 assert_eq!(cancelled.message().await.unwrap().unwrap().message, "first");
1535 drop(cancelled);
1536 tokio::time::timeout(Duration::from_secs(1), async {
1537 loop {
1538 let metrics = registry.render();
1539 if metrics.contains(
1540 "stacked_rpc_client_requests_total{method=\"/rust_zero.echo.Echo/ServerStream\",code=\"cancelled\"} 1",
1541 ) && metrics.contains(
1542 "stacked_rpc_server_requests_total{method=\"/rust_zero.echo.Echo/ServerStream\",code=\"cancelled\"} 1",
1543 ) {
1544 break;
1545 }
1546 tokio::task::yield_now().await;
1547 }
1548 })
1549 .await
1550 .expect("dropping a response stream should record client cancellation");
1551
1552 let mut failed = client
1553 .server_stream(EchoRequest {
1554 message: "status".to_owned(),
1555 })
1556 .await
1557 .unwrap()
1558 .into_inner();
1559 assert_eq!(failed.message().await.unwrap().unwrap().message, "first");
1560 assert_eq!(
1561 failed.message().await.unwrap_err().code(),
1562 tonic::Code::Unavailable
1563 );
1564 let rejected = client
1565 .echo(EchoRequest {
1566 message: "circuit-open".to_owned(),
1567 })
1568 .await
1569 .unwrap_err();
1570 assert_eq!(rejected.code(), tonic::Code::Unavailable);
1571
1572 assert!(registry.render().contains(
1573 "stacked_rpc_server_requests_total{method=\"/rust_zero.echo.Echo/Echo\",code=\"0\"} 2"
1574 ));
1575 assert!(registry.render().contains(
1576 "stacked_rpc_client_requests_total{method=\"/rust_zero.echo.Echo/ServerStream\",code=\"14\"} 1"
1577 ));
1578 assert!(registry.render().contains(
1579 "stacked_rpc_server_requests_total{method=\"/rust_zero.echo.Echo/ServerStream\",code=\"14\"} 1"
1580 ));
1581 server.abort();
1582 }
1583
1584 #[tokio::test]
1585 async fn discovered_client_tracks_published_rpc_endpoints() {
1586 let first_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1587 let first_address = first_listener.local_addr().unwrap();
1588 let first_server = tokio::spawn(async move {
1589 Server::builder()
1590 .add_service(EchoServer::new(EchoService))
1591 .serve_with_incoming(TcpListenerStream::new(first_listener))
1592 .await
1593 .unwrap();
1594 });
1595
1596 let registry = ServiceRegistry::new();
1597 let channel = RpcClient::new(RpcClientConfig::new("http://unused"))
1598 .connect_service(®istry, "echo")
1599 .unwrap();
1600 let first_lease = registry
1601 .publish("echo", format!("http://{first_address}"))
1602 .unwrap();
1603 let response = EchoClient::new(channel.clone())
1604 .echo(Request::new(EchoRequest {
1605 message: "discovered".to_owned(),
1606 }))
1607 .await
1608 .unwrap();
1609 assert_eq!(response.into_inner().message, "discovered");
1610
1611 drop(first_lease);
1612 drop(channel);
1613 first_server.abort();
1614 }
1615
1616 #[tokio::test]
1617 async fn generic_discovery_recovers_from_empty_and_malformed_snapshots() {
1618 let first_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1619 let first_address = first_listener.local_addr().unwrap();
1620 let first_server = tokio::spawn(async move {
1621 Server::builder()
1622 .add_service(EchoServer::new(NamedEchoService("first")))
1623 .serve_with_incoming(TcpListenerStream::new(first_listener))
1624 .await
1625 .unwrap();
1626 });
1627 let second_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1628 let second_address = second_listener.local_addr().unwrap();
1629 let second_server = tokio::spawn(async move {
1630 Server::builder()
1631 .add_service(EchoServer::new(NamedEchoService("second")))
1632 .serve_with_incoming(TcpListenerStream::new(second_listener))
1633 .await
1634 .unwrap();
1635 });
1636
1637 let (updates, receiver) = watch::channel(Vec::new());
1638 let channel = RpcClient::new(RpcClientConfig::new("http://unused")).connect_discovered(
1639 TestSubscription {
1640 receiver,
1641 dropped: None,
1642 },
1643 );
1644 updates.send_replace(vec![
1645 "not a URI".to_owned(),
1646 format!("http://{first_address}"),
1647 ]);
1648 let response = EchoClient::new(channel.clone())
1649 .echo(Request::new(EchoRequest::default()))
1650 .await
1651 .unwrap();
1652 assert_eq!(response.into_inner().message, "first");
1653
1654 updates.send_replace(vec![format!("http://{second_address}")]);
1655 let message = tokio::time::timeout(Duration::from_secs(2), async {
1656 loop {
1657 let result = EchoClient::new(channel.clone())
1658 .echo(Request::new(EchoRequest::default()))
1659 .await;
1660 if let Ok(response) = result {
1661 if response.get_ref().message == "second" {
1662 break response.into_inner().message;
1663 }
1664 }
1665 tokio::task::yield_now().await;
1666 }
1667 })
1668 .await
1669 .unwrap();
1670 assert_eq!(message, "second");
1671
1672 drop(channel);
1673 first_server.abort();
1674 second_server.abort();
1675 }
1676
1677 #[tokio::test]
1678 async fn discovery_watcher_stops_when_channel_is_dropped() {
1679 let (_updates, receiver) = watch::channel(Vec::new());
1680 let (dropped, stopped) = oneshot::channel();
1681 let channel = RpcClient::new(RpcClientConfig::new("http://unused")).connect_discovered(
1682 TestSubscription {
1683 receiver,
1684 dropped: Some(dropped),
1685 },
1686 );
1687
1688 drop(channel);
1689 tokio::time::timeout(Duration::from_secs(1), stopped)
1690 .await
1691 .expect("discovery watcher should stop")
1692 .expect("drop notification should be delivered");
1693 }
1694
1695 #[tokio::test]
1696 async fn active_discovery_health_marks_failed_endpoints_degraded() {
1697 let healthy_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1698 let healthy_address = healthy_listener.local_addr().unwrap();
1699 let healthy_server = tokio::spawn(async move {
1700 Server::builder()
1701 .add_service(EchoServer::new(EchoService))
1702 .serve_with_incoming(TcpListenerStream::new(healthy_listener))
1703 .await
1704 .unwrap();
1705 });
1706 let unavailable_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1707 let unavailable_address = unavailable_listener.local_addr().unwrap();
1708 drop(unavailable_listener);
1709
1710 let (_updates, receiver) = watch::channel(vec![
1711 format!("http://{healthy_address}"),
1712 format!("http://{unavailable_address}"),
1713 ]);
1714 let config = RpcClientConfig::new("http://unused")
1715 .with_discovery_health_check(Duration::from_millis(20), Duration::from_millis(100));
1716 let (channel, mut status) =
1717 RpcClient::new(config).connect_discovered_with_status(TestSubscription {
1718 receiver,
1719 dropped: None,
1720 });
1721
1722 let snapshot = tokio::time::timeout(Duration::from_secs(2), async {
1723 loop {
1724 let snapshot = status.changed().await.unwrap();
1725 if snapshot.readiness == DiscoveryReadiness::Degraded {
1726 break snapshot;
1727 }
1728 }
1729 })
1730 .await
1731 .unwrap();
1732 assert_eq!(snapshot.discovered, 2);
1733 assert_eq!(snapshot.available, 1);
1734
1735 let recovered_listener = TcpListener::bind(unavailable_address).await.unwrap();
1736 let recovered_server = tokio::spawn(async move {
1737 Server::builder()
1738 .add_service(EchoServer::new(EchoService))
1739 .serve_with_incoming(TcpListenerStream::new(recovered_listener))
1740 .await
1741 .unwrap();
1742 });
1743 let recovered = tokio::time::timeout(Duration::from_secs(2), async {
1744 loop {
1745 let snapshot = status.changed().await.unwrap();
1746 if snapshot.readiness == DiscoveryReadiness::Ready {
1747 break snapshot;
1748 }
1749 }
1750 })
1751 .await
1752 .unwrap();
1753 assert_eq!(recovered.available, 2);
1754
1755 drop(channel);
1756 healthy_server.abort();
1757 recovered_server.abort();
1758 }
1759
1760 #[tokio::test]
1761 async fn discovery_status_projects_into_shared_health() {
1762 let (updates, receiver) = watch::channel(DiscoveryStatusSnapshot {
1763 readiness: DiscoveryReadiness::Empty,
1764 discovered: 0,
1765 available: 0,
1766 rejected: 0,
1767 });
1768 let registry = HealthRegistry::new();
1769 let mut health_updates = registry.subscribe();
1770 let task = DiscoveryStatus { receiver }.project_to_health(registry.clone(), "users-rpc");
1771 health_updates.changed().await.unwrap();
1772 assert_eq!(registry.snapshot().unhealthy(), vec!["users-rpc"]);
1773
1774 updates
1775 .send(DiscoveryStatusSnapshot {
1776 readiness: DiscoveryReadiness::Ready,
1777 discovered: 1,
1778 available: 1,
1779 rejected: 0,
1780 })
1781 .unwrap();
1782 health_updates.changed().await.unwrap();
1783 assert!(registry.snapshot().is_ready());
1784 task.abort();
1785 }
1786
1787 #[test]
1788 fn discovered_client_rejects_invalid_service_names() {
1789 let registry = ServiceRegistry::new();
1790 let client = RpcClient::new(RpcClientConfig::new("http://unused"));
1791
1792 assert!(matches!(
1793 client.connect_service(®istry, ""),
1794 Err(RpcClientError::Discovery(DiscoveryError::EmptyService))
1795 ));
1796 }
1797
1798 #[test]
1799 fn weighted_discovery_keys_preserve_relative_capacity() {
1800 let client = RpcClient::new(RpcClientConfig::new("http://unused"));
1801 let (configured, rejected) = client.configure_discovered(vec![
1802 DiscoveredEndpoint::weighted("http://one:8080", 3).unwrap(),
1803 DiscoveredEndpoint::weighted("http://two:8080", 1).unwrap(),
1804 DiscoveredEndpoint::new("not a URI").unwrap(),
1805 ]);
1806 let available = configured.keys().cloned().collect();
1807 let keys = weighted_keys(&configured, &available);
1808
1809 assert_eq!(keys.len(), 4);
1810 assert_eq!(rejected, 1);
1811 assert_eq!(
1812 discovery_status(configured.len(), configured.len(), rejected),
1813 DiscoveryStatusSnapshot {
1814 readiness: DiscoveryReadiness::Degraded,
1815 discovered: 3,
1816 available: 2,
1817 rejected: 1,
1818 }
1819 );
1820 }
1821
1822 #[test]
1823 fn discovery_status_distinguishes_empty_ready_and_degraded() {
1824 assert_eq!(
1825 discovery_status(0, 0, 0).readiness,
1826 DiscoveryReadiness::Empty
1827 );
1828 assert_eq!(
1829 discovery_status(2, 2, 0).readiness,
1830 DiscoveryReadiness::Ready
1831 );
1832 assert_eq!(
1833 discovery_status(2, 1, 0).readiness,
1834 DiscoveryReadiness::Degraded
1835 );
1836 }
1837}