made_client/
made_client.rs1use std::cmp::min;
2
3use made_proto::v1::made_service_client::MadeServiceClient;
4use prost::Message;
5use sha2::{Digest, Sha256};
6use tokio::time::sleep;
7use tonic::metadata::MetadataValue;
8use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity};
9use tonic::Request;
10
11use crate::{ClientConfig, MadeClientError, RequestContext};
12
13#[derive(Clone, Debug)]
15pub struct MadeClient {
16 channel: Channel,
17 request_context: Option<RequestContext>,
18}
19
20impl MadeClient {
21 pub async fn connect(endpoint: impl Into<String>) -> Result<Self, MadeClientError> {
22 Self::connect_with_config(ClientConfig::new(endpoint)).await
23 }
24
25 pub async fn connect_with_config(config: ClientConfig) -> Result<Self, MadeClientError> {
26 let mut endpoint = Endpoint::from_shared(config.endpoint().to_owned())
27 .map_err(|error| MadeClientError::InvalidEndpoint(error.to_string()))?;
28 if config.uses_tls() {
29 let mut tls = ClientTlsConfig::new().with_native_roots();
30 if let Some(pem) = config.ca_certificate_pem() {
31 tls = tls.ca_certificate(Certificate::from_pem(pem));
32 }
33 if let Some((certificate, key)) = config.client_identity_pem() {
34 tls = tls.identity(Identity::from_pem(certificate, key));
35 }
36 if let Some(domain_name) = config.tls_domain_name() {
37 tls = tls.domain_name(domain_name);
38 }
39 endpoint = endpoint
40 .tls_config(tls)
41 .map_err(|error| MadeClientError::TlsConfiguration(error.to_string()))?;
42 }
43 let mut delay = config.initial_backoff();
44 let mut last_error = None;
45
46 for attempt in 0..config.connect_attempts() {
47 match endpoint.clone().connect().await {
48 Ok(channel) => {
49 return Ok(Self {
50 channel,
51 request_context: config.request_context().cloned(),
52 });
53 }
54 Err(error) => last_error = Some(error.to_string()),
55 }
56 if attempt + 1 < config.connect_attempts() {
57 sleep(delay).await;
58 delay = min(delay.saturating_mul(2), config.maximum_backoff());
59 }
60 }
61
62 Err(MadeClientError::ConnectionExhausted(
63 last_error.unwrap_or_else(|| "no connection attempt was made".to_owned()),
64 ))
65 }
66
67 pub(crate) fn rpc(&self) -> MadeServiceClient<Channel> {
68 MadeServiceClient::new(self.channel.clone())
69 }
70
71 pub(crate) fn context(&self) -> RequestContext {
72 self.request_context.clone().unwrap_or_default()
73 }
74
75 pub(crate) fn request<T>(
76 context: &RequestContext,
77 method: &'static str,
78 payload: T,
79 ) -> Request<T>
80 where
81 T: Message,
82 {
83 let payload_bytes = payload.encode_to_vec();
84 let request_id = derived_request_id(context.id(), method, &payload_bytes);
85 let mut request = Request::new(payload);
86 request
87 .metadata_mut()
88 .insert("x-made-request-id", request_id);
89 request
90 }
91
92 #[must_use]
93 pub fn request_context(&self) -> RequestContext {
94 self.context()
95 }
96}
97
98fn derived_request_id(
99 invocation_id: &str,
100 method: &'static str,
101 payload: &[u8],
102) -> MetadataValue<tonic::metadata::Ascii> {
103 let mut digest = Sha256::new();
104 digest.update(b"underpass.made.client-request.v1\0");
105 hash_field(&mut digest, invocation_id.as_bytes());
106 hash_field(&mut digest, method.as_bytes());
107 hash_field(&mut digest, payload);
108 let value = format!("made-{:x}", digest.finalize());
109 MetadataValue::try_from(value).expect("SHA-256 request ids are valid ASCII metadata")
110}
111
112fn hash_field(digest: &mut Sha256, value: &[u8]) {
113 digest.update((value.len() as u64).to_be_bytes());
114 digest.update(value);
115}