mod channel;
mod context;
mod memory;
mod metrics;
mod status;
use std::any::{Any, TypeId};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::convert::Infallible;
use std::sync::{Arc, Mutex, PoisonError, RwLock};
use autumn_web::actuator::{MetricFamily, MetricsSource};
use autumn_web::app::AppBuilder;
use autumn_web::{AppState, AutumnError};
use axum::extract::FromRequestParts;
use axum::response::IntoResponse;
use tonic::body::Body as TonicBody;
use tonic::server::NamedService;
use tonic::service::Routes;
use tonic::transport::Endpoint;
use tower::Service;
pub use channel::GrpcChannel;
pub use status::{GrpcResultExt, http_status, status_to_error};
use crate::config::{ClientConfig, GrpcConfig};
use crate::error::GrpcError;
use crate::plugin::GrpcPlugin;
use channel::ClientShared;
use context::CallContext;
use metrics::ClientMetrics;
const METRICS_SOURCE: &str = "autumn-plugin-grpc-clients";
#[derive(Clone)]
struct Factory {
type_id: TypeId,
type_name: &'static str,
build: Arc<dyn Any + Send + Sync>,
}
type Build<T> = Arc<dyn Fn(GrpcChannel) -> T + Send + Sync>;
impl Factory {
fn new<T, F>(build: F) -> Self
where
T: Send + 'static,
F: Fn(GrpcChannel) -> T + Send + Sync + 'static,
{
let build: Build<T> = Arc::new(build);
Self {
type_id: TypeId::of::<T>(),
type_name: std::any::type_name::<T>(),
build: Arc::new(build),
}
}
fn build<T: 'static>(&self, channel: GrpcChannel) -> Option<T> {
self.build
.downcast_ref::<Build<T>>()
.map(|build| build(channel))
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ClientError {
#[error("no gRPC client is registered; add `GrpcPlugin::client(name, ..)`")]
NotInstalled,
#[error("no gRPC client named `{0}` is registered; add `GrpcPlugin::client(\"{0}\", ..)`")]
NotRegistered(String),
#[non_exhaustive]
#[error(
"gRPC client `{name}` cannot give a `{wanted}`; add `GrpcPlugin::client(\"{name}\", ..)` for that type"
)]
WrongType {
name: String,
wanted: &'static str,
},
#[error("no gRPC client of type `{0}` is registered; add `GrpcPlugin::client(name, ..)`")]
NoClientOfType(&'static str),
#[non_exhaustive]
#[error(
"gRPC clients {} all have type `{wanted}`; use `GrpcClients::get::<T>(name)` to choose one",
names.join(", ")
)]
Ambiguous {
wanted: &'static str,
names: Vec<String>,
},
}
struct Entry {
channel: GrpcChannel,
metrics: Arc<ClientMetrics>,
factories: Vec<Factory>,
}
#[derive(Default)]
struct Claims {
names: BTreeSet<String>,
types: HashMap<TypeId, (&'static str, BTreeSet<String>)>,
}
#[derive(Default)]
struct Registry {
entries: RwLock<BTreeMap<String, Entry>>,
reserved: Mutex<Claims>,
}
#[derive(Clone, Default)]
pub struct GrpcClients {
registry: Arc<Registry>,
context: Option<Arc<CallContext>>,
}
impl std::fmt::Debug for GrpcClients {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GrpcClients")
.field("names", &self.names())
.field("context", &self.context)
.finish_non_exhaustive()
}
}
impl GrpcClients {
pub fn get<T: 'static>(&self, name: &str) -> Result<T, ClientError> {
let entries = self
.registry
.entries
.read()
.unwrap_or_else(PoisonError::into_inner);
let entry = entries
.get(name)
.ok_or_else(|| ClientError::NotRegistered(name.to_owned()))?;
let factory = entry
.factories
.iter()
.find(|factory| factory.type_id == TypeId::of::<T>())
.cloned();
let channel = entry.channel.with_context(self.context.clone());
drop(entries);
factory
.and_then(|factory| factory.build::<T>(channel))
.ok_or_else(|| ClientError::WrongType {
name: name.to_owned(),
wanted: std::any::type_name::<T>(),
})
}
pub(crate) fn only<T: 'static>(&self) -> Result<T, ClientError> {
let wanted = std::any::type_name::<T>();
let names: Vec<String> = self
.registry
.entries
.read()
.unwrap_or_else(PoisonError::into_inner)
.iter()
.filter(|(_, entry)| {
entry
.factories
.iter()
.any(|factory| factory.type_id == TypeId::of::<T>())
})
.map(|(name, _)| name.clone())
.collect();
match names.as_slice() {
[] => Err(ClientError::NoClientOfType(wanted)),
[name] => self.get(name),
_ => Err(ClientError::Ambiguous { wanted, names }),
}
}
#[must_use]
pub fn names(&self) -> Vec<String> {
self.registry
.entries
.read()
.unwrap_or_else(PoisonError::into_inner)
.keys()
.cloned()
.collect()
}
#[must_use]
pub fn metric_families(&self) -> Vec<MetricFamily> {
let entries = self
.registry
.entries
.read()
.unwrap_or_else(PoisonError::into_inner);
metrics::families(
entries
.iter()
.map(|(name, entry)| (name.as_str(), &*entry.metrics)),
)
}
fn reserve(&self, pending: &[Pending]) -> Result<Vec<String>, GrpcError> {
let mut claims = self
.registry
.reserved
.lock()
.unwrap_or_else(PoisonError::into_inner);
if let Some(taken) = pending.iter().find(|p| claims.names.contains(&p.name)) {
return Err(GrpcError::DuplicateClient(taken.name.clone()));
}
let mut touched = BTreeSet::new();
for client in pending {
claims.names.insert(client.name.clone());
for factory in &client.factories {
claims
.types
.entry(factory.type_id)
.or_insert_with(|| (factory.type_name, BTreeSet::new()))
.1
.insert(client.name.clone());
touched.insert(factory.type_id);
}
}
let mut warnings: Vec<String> = touched
.iter()
.filter_map(|type_id| claims.types.get(type_id))
.filter(|(_, names)| names.len() > 1)
.map(|(type_name, names)| {
format!(
"gRPC clients {} have one type, `{type_name}`; `GrpcClient<T>` cannot choose, use `GrpcClients::get`",
names.iter().cloned().collect::<Vec<_>>().join(", ")
)
})
.collect();
drop(claims);
warnings.sort();
Ok(warnings)
}
fn insert(&self, name: String, entry: Entry) {
self.registry
.entries
.write()
.unwrap_or_else(PoisonError::into_inner)
.insert(name, entry);
}
fn from_state(state: &AppState) -> Result<Arc<Self>, ClientError> {
state.extension::<Self>().ok_or(ClientError::NotInstalled)
}
}
fn reject(error: ClientError) -> AutumnError {
tracing::error!(%error, "gRPC client lookup failed");
AutumnError::from(error)
}
impl GrpcClients {
fn extract(parts: &http::request::Parts, state: &AppState) -> Result<Self, AutumnError> {
let clients = Self::from_state(state).map_err(reject)?;
Ok(Self {
registry: clients.registry.clone(),
context: Some(Arc::new(CallContext::from_request(parts, state))),
})
}
}
impl FromRequestParts<AppState> for GrpcClients {
type Rejection = AutumnError;
fn from_request_parts(
parts: &mut http::request::Parts,
state: &AppState,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
std::future::ready(Self::extract(parts, state))
}
}
#[derive(Debug, Clone)]
pub struct GrpcClient<T>(pub T);
impl<T: Send + 'static> FromRequestParts<AppState> for GrpcClient<T> {
type Rejection = AutumnError;
fn from_request_parts(
parts: &mut http::request::Parts,
state: &AppState,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
let client = GrpcClients::extract(parts, state)
.and_then(|clients| clients.only::<T>().map(GrpcClient).map_err(reject));
std::future::ready(client)
}
}
#[derive(Default)]
pub struct Registrations {
factories: Vec<(String, Factory)>,
doubles: BTreeMap<String, Routes>,
}
impl Registrations {
pub fn names(&self) -> Vec<&str> {
let names: BTreeSet<&str> = self.factories.iter().map(|(n, _)| n.as_str()).collect();
names.into_iter().collect()
}
}
impl GrpcPlugin {
#[must_use]
pub fn client<T, F>(mut self, name: impl Into<String>, build: F) -> Self
where
T: Send + 'static,
F: Fn(GrpcChannel) -> T + Send + Sync + 'static,
{
self.clients
.factories
.push((name.into(), Factory::new(build)));
self.reset();
self
}
#[must_use]
pub fn client_double<S>(mut self, name: impl Into<String>, service: S) -> Self
where
S: Service<http::Request<TonicBody>, Error = Infallible>
+ NamedService
+ Clone
+ Send
+ Sync
+ 'static,
S::Response: IntoResponse,
S::Future: Send + 'static,
{
let routes = self.clients.doubles.entry(name.into()).or_default();
*routes = std::mem::take(routes).add_service(service);
self
}
}
struct Pending {
name: String,
config: ClientConfig,
double: Option<Routes>,
factories: Vec<Factory>,
}
pub struct Prepared {
clients: GrpcClients,
first: bool,
pending: Vec<Pending>,
metrics: bool,
max_series: usize,
}
pub fn prepare(
app: &AppBuilder,
registrations: Registrations,
config: &GrpcConfig,
section: &str,
) -> Result<Option<Prepared>, GrpcError> {
let Registrations {
factories,
mut doubles,
} = registrations;
for warning in unregistered(&factories, config, section) {
tracing::warn!("{warning}");
}
if factories.is_empty() {
if let Some(name) = doubles.keys().next() {
return Err(GrpcError::DoubleWithoutClient(name.clone()));
}
return Ok(None);
}
let mut by_name: BTreeMap<String, Vec<Factory>> = BTreeMap::new();
for (name, factory) in factories {
crate::config::validate_client_name(&name)?;
let list = by_name.entry(name.clone()).or_default();
if list.iter().any(|f| f.type_id == factory.type_id) {
return Err(GrpcError::DuplicateClient(name));
}
list.push(factory);
}
if let Some(name) = doubles.keys().find(|name| !by_name.contains_key(*name)) {
return Err(GrpcError::DoubleWithoutClient(name.clone()));
}
let mut pending = Vec::with_capacity(by_name.len());
for (name, factories) in by_name {
let double = doubles.remove(&name);
let client = config.clients.get(&name).cloned().unwrap_or_default();
if double.is_none() && client.endpoint.trim().is_empty() {
return Err(GrpcError::ClientWithoutEndpoint {
name,
section: section.to_owned(),
});
}
pending.push(Pending {
name,
config: client,
double,
factories,
});
}
let existing = app.extension::<GrpcClients>().cloned();
let first = existing.is_none();
let clients = existing.unwrap_or_default();
for warning in clients.reserve(&pending)? {
tracing::warn!("{warning}");
}
Ok(Some(Prepared {
clients,
first,
pending,
metrics: config.metrics,
max_series: config.max_metric_series,
}))
}
fn unregistered(
factories: &[(String, Factory)],
config: &GrpcConfig,
section: &str,
) -> Vec<String> {
let registered: BTreeSet<&str> = factories.iter().map(|(n, _)| n.as_str()).collect();
config
.clients
.keys()
.filter(|name| !registered.contains(name.as_str()))
.map(|name| {
format!(
"`[{section}.clients.{name}]` has no registration; add `GrpcPlugin::client(\"{name}\", ..)`"
)
})
.collect()
}
impl Prepared {
pub fn install(self, app: AppBuilder) -> AppBuilder {
let Self {
clients,
first,
pending,
metrics,
max_series,
} = self;
let app = if first {
app.with_extension(clients.clone())
.metrics_source(METRICS_SOURCE, Arc::new(ClientsMetrics(clients.clone())))
} else {
app
};
let pending = Arc::new(Mutex::new(Some(pending)));
app.on_startup(move |state| {
let pending = pending
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take();
let clients = clients.clone();
async move {
for client in pending.unwrap_or_default() {
let name = client.name.clone();
let entry = connect(client, metrics, max_series)
.map_err(|error| crate::plugin::startup_error(&error))?;
clients.insert(name, entry);
}
state.insert_extension(clients);
Ok(())
}
})
}
}
fn connect(client: Pending, metrics: bool, max_series: usize) -> Result<Entry, GrpcError> {
let Pending {
name,
config,
double,
factories,
} = client;
let channel = match double {
Some(routes) => memory::connect(&name, routes),
None => endpoint(&name, &config)?.connect_lazy(),
};
let metrics = Arc::new(ClientMetrics::new(metrics, max_series));
let shared = Arc::new(ClientShared {
name,
timeout: config.timeout(),
metrics: metrics.clone(),
});
Ok(Entry {
channel: GrpcChannel::new(channel, shared),
metrics,
factories,
})
}
fn endpoint(name: &str, config: &ClientConfig) -> Result<Endpoint, GrpcError> {
let setup = |message: String| GrpcError::ClientSetup {
name: name.to_owned(),
message,
};
let endpoint = Endpoint::from_shared(config.endpoint.trim().to_owned())
.map_err(|e| setup(format!("bad endpoint: {e}")))?
.connect_timeout(config.connect_timeout())
.tcp_nodelay(true);
#[cfg(feature = "tls")]
if config.is_https() {
return endpoint
.tls_config(crate::tls::client_config(&config.tls).map_err(setup)?)
.map_err(|e| setup(format!("bad TLS setup: {e}")));
}
Ok(endpoint)
}
struct ClientsMetrics(GrpcClients);
impl MetricsSource for ClientsMetrics {
fn collect(&self) -> Vec<MetricFamily> {
self.0.metric_families()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn pending(name: &str, factories: Vec<Factory>) -> Pending {
Pending {
name: name.to_owned(),
config: ClientConfig::default(),
double: None,
factories,
}
}
#[test]
fn config_with_no_registration_is_named_with_its_section() {
let mut config = GrpcConfig::default();
config
.clients
.insert("orphan".to_owned(), ClientConfig::default());
config
.clients
.insert("a".to_owned(), ClientConfig::default());
let factories = vec![("a".to_owned(), Factory::new(|_: GrpcChannel| 1_u8))];
let found = unregistered(&factories, &config, "grpc_admin");
assert_eq!(found.len(), 1, "{found:?}");
assert!(
found[0].contains("[grpc_admin.clients.orphan]"),
"{found:?}"
);
}
#[test]
fn one_type_on_two_names_warns_also_across_plugins() {
let clients = GrpcClients::default();
let u8s = || Factory::new(|_: GrpcChannel| 1_u8);
let first = clients
.reserve(&[
pending("a", vec![u8s()]),
pending("c", vec![Factory::new(|_: GrpcChannel| "x")]),
])
.unwrap();
assert!(first.is_empty(), "{first:?}");
let second = clients.reserve(&[pending("b", vec![u8s()])]).unwrap();
assert_eq!(second.len(), 1, "{second:?}");
assert!(second[0].contains("a, b") && second[0].contains("u8"));
let error = clients.reserve(&[pending("a", vec![])]).unwrap_err();
assert!(matches!(error, GrpcError::DuplicateClient(name) if name == "a"));
}
#[test]
fn the_ambiguity_error_lists_the_names() {
let error = ClientError::Ambiguous {
wanted: "T",
names: vec!["a".to_owned(), "b".to_owned()],
};
assert!(error.to_string().contains("a, b"), "{error}");
}
}