#![allow(clippy::result_large_err, clippy::const_is_empty)]
use async_graphql::{Name, Value as GqlValue};
use grpc_graphql_gateway::{
CacheConfig, Gateway, GatewayBuilder, GrpcClient, PersistedQueryConfig,
RequestCollapsingConfig, Result as GatewayResult,
};
use std::net::{SocketAddr, ToSocketAddrs};
use std::sync::Arc;
use std::time::Duration;
use tonic::{transport::Server, Request, Response, Status};
use tracing_subscriber::prelude::*;
type ServiceResult<T> = std::result::Result<T, Status>;
const DESCRIPTOR_SET: &[u8] =
include_bytes!("../../src/generated/live_query_example_descriptor.bin");
#[allow(dead_code)]
const DEFAULT_GRPC_ADDR: &str = "0.0.0.0:50051";
fn describe(list: &[&str]) -> String {
if list.is_empty() {
"none".to_string()
} else {
list.join(", ")
}
}
fn describe_resolvers(list: &[&str]) -> String {
if list.is_empty() {
"none".to_string()
} else {
list.join(", ")
}
}
#[allow(dead_code)]
fn listen_addr(endpoint: &str, fallback: &str) -> GatewayResult<SocketAddr> {
let mut addr = endpoint.trim();
if let Some(stripped) = addr
.strip_prefix("http://")
.or_else(|| addr.strip_prefix("https://"))
{
addr = stripped;
}
if let Some((host, _rest)) = addr.split_once('/') {
addr = host;
}
if let Ok(sock) = addr.parse() {
return Ok(sock);
}
if let Ok(mut iter) = addr.to_socket_addrs() {
if let Some(sock) = iter.next() {
return Ok(sock);
}
}
fallback
.parse()
.map_err(|e| grpc_graphql_gateway::Error::Other(anyhow::Error::new(e)))
}
fn describe_key_sets(keys: &[&[&str]]) -> String {
if keys.is_empty() {
"none".to_string()
} else {
keys.iter()
.map(|set| set.join(" "))
.collect::<Vec<_>>()
.join(" | ")
}
}
fn describe_entities() -> String {
if ENTITY_CONFIGS.is_empty() {
"none".to_string()
} else {
ENTITY_CONFIGS
.iter()
.map(|e| format!("{} (keys: {})", e.type_name, describe_key_sets(e.keys)))
.collect::<Vec<_>>()
.join(", ")
}
}
const QUERIES: &[&str] = &["user", "userStatus", "users"];
const MUTATIONS: &[&str] = &["createUser", "deleteUser", "updateUser"];
const SUBSCRIPTIONS: &[&str] = &[];
const RESOLVERS: &[&str] = &["_entities"];
#[allow(dead_code)]
const FEDERATION_ENABLED: bool = true;
pub struct EntityConfigInfo {
pub type_name: &'static str,
pub keys: &'static [&'static [&'static str]],
pub extend: bool,
pub resolvable: bool,
}
pub const ENTITY_CONFIGS: &[EntityConfigInfo] = &[EntityConfigInfo {
type_name: "live_example_User",
keys: &[&["id"]],
extend: false,
resolvable: true,
}];
#[derive(Debug, Clone)]
pub struct LiveQueryConfigInfo {
pub operation_name: &'static str,
pub throttle_ms: u32,
pub triggers: &'static [&'static str],
pub max_connections: u32,
pub ttl_seconds: u32,
pub strategy: &'static str,
pub poll_interval_ms: u32,
pub depends_on: &'static [&'static str],
}
pub const LIVE_QUERY_CONFIGS: &[LiveQueryConfigInfo] = &[
LiveQueryConfigInfo {
operation_name: "user",
throttle_ms: 100,
triggers: &["User.update", "User.delete"],
max_connections: 10,
ttl_seconds: 3600,
strategy: "INVALIDATION",
poll_interval_ms: 0,
depends_on: &["User"],
},
LiveQueryConfigInfo {
operation_name: "users",
throttle_ms: 500,
triggers: &["User.create", "User.update", "User.delete"],
max_connections: 10,
ttl_seconds: 0,
strategy: "POLLING",
poll_interval_ms: 5000,
depends_on: &[],
},
LiveQueryConfigInfo {
operation_name: "userStatus",
throttle_ms: 50,
triggers: &["User.status_change"],
max_connections: 10,
ttl_seconds: 0,
strategy: "HASH_DIFF",
poll_interval_ms: 0,
depends_on: &[],
},
];
#[allow(dead_code)]
const LIVE_QUERIES_ENABLED: bool = true;
#[allow(dead_code)]
fn describe_live_queries() -> String {
if LIVE_QUERY_CONFIGS.is_empty() {
"none".to_string()
} else {
LIVE_QUERY_CONFIGS
.iter()
.map(|lq| {
format!(
"{}@live ({}ms, {})",
lq.operation_name, lq.throttle_ms, lq.strategy
)
})
.collect::<Vec<_>>()
.join(", ")
}
}
pub mod live_example {
include!("../../src/generated/live_example.rs");
}
pub struct ServiceConfig {
pub name: &'static str,
pub endpoint: &'static str,
pub insecure: bool,
pub queries: &'static [&'static str],
pub mutations: &'static [&'static str],
pub subscriptions: &'static [&'static str],
pub resolvers: &'static [&'static str],
}
pub mod services {
use super::ServiceConfig;
pub const LIVE_EXAMPLE_USERSERVICE: ServiceConfig = ServiceConfig {
name: "live_example.UserService",
endpoint: "http://localhost:50052",
insecure: true,
queries: &["user", "users", "userStatus", "post"],
mutations: &["createUser", "updateUser", "deleteUser", "createPost"],
subscriptions: &[],
resolvers: &["_entities"],
};
pub const ALL: &[ServiceConfig] = &[LIVE_EXAMPLE_USERSERVICE];
}
#[derive(Clone, Default)]
pub struct LiveExampleEntityResolver;
#[async_trait::async_trait]
impl grpc_graphql_gateway::EntityResolver for LiveExampleEntityResolver {
async fn resolve_entity(
&self,
_entity_config: &grpc_graphql_gateway::federation::EntityConfig,
representation: &async_graphql::indexmap::IndexMap<Name, GqlValue>,
) -> grpc_graphql_gateway::Result<GqlValue> {
let mut obj = representation.clone();
obj.shift_remove(&Name::new("__typename"));
Ok(GqlValue::Object(obj))
}
async fn batch_resolve_entities(
&self,
entity_config: &grpc_graphql_gateway::federation::EntityConfig,
representations: Vec<async_graphql::indexmap::IndexMap<Name, GqlValue>>,
) -> grpc_graphql_gateway::Result<Vec<GqlValue>> {
let mut results = Vec::with_capacity(representations.len());
for repr in representations {
results.push(self.resolve_entity(entity_config, &repr).await?);
}
Ok(results)
}
}
fn default_entity_resolver() -> Arc<dyn grpc_graphql_gateway::EntityResolver> {
Arc::new(LiveExampleEntityResolver)
}
use grpc_graphql_gateway::{InvalidationEvent, SharedLiveQueryStore};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
lazy_static::lazy_static! {
static ref USER_STORE: RwLock<HashMap<String, live_example::User>> = {
let mut store = HashMap::new();
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
store.insert("1".to_string(), live_example::User {
id: "1".to_string(),
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
status: Some(live_example::UserStatus {
user_id: "1".to_string(),
is_online: true,
last_active: "2024-01-01T12:00:00Z".to_string(),
current_activity: "Browsing".to_string(),
}),
created_at: now,
updated_at: now,
posts: vec![],
});
store.insert("2".to_string(), live_example::User {
id: "2".to_string(),
name: "Bob".to_string(),
email: "bob@example.com".to_string(),
status: Some(live_example::UserStatus {
user_id: "2".to_string(),
is_online: false,
last_active: "2024-01-01T10:00:00Z".to_string(),
current_activity: "Offline".to_string(),
}),
created_at: now,
updated_at: now,
posts: vec![],
});
store.insert("3".to_string(), live_example::User {
id: "3".to_string(),
name: "Charlie".to_string(),
email: "charlie@example.com".to_string(),
status: Some(live_example::UserStatus {
user_id: "3".to_string(),
is_online: true,
last_active: "2024-01-01T11:30:00Z".to_string(),
current_activity: "Coding".to_string(),
}),
created_at: now,
updated_at: now,
posts: vec![],
});
RwLock::new(store)
};
static ref POST_STORE: RwLock<HashMap<String, live_example::Post>> = RwLock::new(HashMap::new());
static ref NEXT_USER_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(4);
}
pub fn get_live_query_store() -> SharedLiveQueryStore {
grpc_graphql_gateway::global_live_query_store()
}
#[derive(Clone)]
pub struct ServiceImpl {
live_query_store: SharedLiveQueryStore,
}
impl Default for ServiceImpl {
fn default() -> Self {
Self {
live_query_store: grpc_graphql_gateway::global_live_query_store(),
}
}
}
impl ServiceImpl {
pub fn new(live_query_store: SharedLiveQueryStore) -> Self {
Self { live_query_store }
}
fn invalidate(&self, type_name: &str, action: &str) {
let event = InvalidationEvent::new(type_name, action);
let affected = self.live_query_store.invalidate(event);
if affected > 0 {
tracing::info!(
type_name = %type_name,
action = %action,
affected_count = affected,
"Triggered live query invalidation"
);
}
}
}
#[tonic::async_trait]
impl live_example::user_service_server::UserService for ServiceImpl {
async fn get_user(
&self,
request: Request<live_example::GetUserRequest>,
) -> ServiceResult<Response<live_example::User>> {
let req = request.into_inner();
tracing::debug!(user_id = %req.id, "GetUser request");
let store = USER_STORE.read();
match store.get(&req.id) {
Some(user) => {
let mut user = user.clone();
let post_store = POST_STORE.read();
user.posts = post_store
.values()
.filter(|p| p.author_id == req.id)
.cloned()
.collect();
Ok(Response::new(user))
}
None => Err(Status::not_found(format!("User {} not found", req.id))),
}
}
async fn list_users(
&self,
request: Request<live_example::ListUsersRequest>,
) -> ServiceResult<Response<live_example::ListUsersResponse>> {
let req = request.into_inner();
tracing::debug!(limit = req.limit, offset = req.offset, "ListUsers request");
let store = USER_STORE.read();
let post_store = POST_STORE.read();
let mut users: Vec<live_example::User> = store
.values()
.map(|u| {
let mut user = u.clone();
user.posts = post_store
.values()
.filter(|p| p.author_id == u.id)
.cloned()
.collect();
user
})
.collect();
if !req.filter.is_empty() {
let filter_lower = req.filter.to_lowercase();
users.retain(|u| {
u.name.to_lowercase().contains(&filter_lower)
|| u.email.to_lowercase().contains(&filter_lower)
});
}
users.sort_by(|a, b| a.id.cmp(&b.id));
let total_count = users.len() as i32;
let offset = req.offset.max(0) as usize;
let limit = if req.limit > 0 {
req.limit as usize
} else {
users.len()
};
let users: Vec<live_example::User> = users.into_iter().skip(offset).take(limit).collect();
Ok(Response::new(live_example::ListUsersResponse {
users,
total_count,
}))
}
async fn get_user_status(
&self,
request: Request<live_example::GetUserStatusRequest>,
) -> ServiceResult<Response<live_example::UserStatus>> {
let req = request.into_inner();
tracing::debug!(user_id = %req.user_id, "GetUserStatus request");
let store = USER_STORE.read();
match store.get(&req.user_id) {
Some(user) => match &user.status {
Some(status) => Ok(Response::new(status.clone())),
None => Err(Status::not_found(format!(
"Status for user {} not found",
req.user_id
))),
},
None => Err(Status::not_found(format!("User {} not found", req.user_id))),
}
}
async fn create_user(
&self,
request: Request<live_example::CreateUserRequest>,
) -> ServiceResult<Response<live_example::User>> {
let req = request.into_inner();
tracing::info!(name = %req.name, email = %req.email, "CreateUser request");
let id = NEXT_USER_ID
.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
.to_string();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let user = live_example::User {
id: id.clone(),
name: req.name,
email: req.email,
status: Some(live_example::UserStatus {
user_id: id.clone(),
is_online: true,
last_active: chrono::Utc::now().to_rfc3339(),
current_activity: "Just joined".to_string(),
}),
created_at: now,
updated_at: now,
posts: vec![],
};
{
let mut store = USER_STORE.write();
store.insert(id.clone(), user.clone());
}
self.invalidate("User", "create");
tracing::info!(user_id = %id, "Created new user");
Ok(Response::new(user))
}
async fn update_user(
&self,
request: Request<live_example::UpdateUserRequest>,
) -> ServiceResult<Response<live_example::User>> {
let req = request.into_inner();
tracing::info!(user_id = %req.id, "UpdateUser request");
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let user = {
let mut store = USER_STORE.write();
match store.get_mut(&req.id) {
Some(user) => {
if !req.name.is_empty() {
user.name = req.name;
}
if !req.email.is_empty() {
user.email = req.email;
}
user.updated_at = now;
user.clone()
}
None => return Err(Status::not_found(format!("User {} not found", req.id))),
}
};
self.invalidate("User", "update");
tracing::info!(user_id = %req.id, "Updated user");
Ok(Response::new(user))
}
async fn delete_user(
&self,
request: Request<live_example::DeleteUserRequest>,
) -> ServiceResult<Response<live_example::DeleteUserResponse>> {
let req = request.into_inner();
tracing::info!(user_id = %req.id, "DeleteUser request");
let removed = {
let mut store = USER_STORE.write();
store.remove(&req.id)
};
match removed {
Some(_) => {
self.invalidate("User", "delete");
tracing::info!(user_id = %req.id, "Deleted user");
Ok(Response::new(live_example::DeleteUserResponse {
success: true,
message: format!("User {} deleted successfully", req.id),
}))
}
None => Err(Status::not_found(format!("User {} not found", req.id))),
}
}
async fn get_post(
&self,
request: Request<live_example::GetPostRequest>,
) -> ServiceResult<Response<live_example::Post>> {
let req = request.into_inner();
tracing::debug!(post_id = %req.id, "GetPost request");
let store = POST_STORE.read();
match store.get(&req.id) {
Some(post) => Ok(Response::new(post.clone())),
None => Err(Status::not_found(format!("Post {} not found", req.id))),
}
}
async fn create_post(
&self,
request: Request<live_example::CreatePostRequest>,
) -> ServiceResult<Response<live_example::Post>> {
let req = request.into_inner();
tracing::info!(title = %req.title, author_id = %req.author_id, "CreatePost request");
let id = uuid::Uuid::new_v4().to_string();
let post = live_example::Post {
id: id.clone(),
title: req.title,
content: req.content,
author_id: req.author_id.clone(),
};
{
let mut store = POST_STORE.write();
store.insert(id.clone(), post.clone());
}
self.invalidate("Post", "create");
self.invalidate("User", "update");
tracing::info!(post_id = %id, "Created new post");
Ok(Response::new(post))
}
}
pub async fn run_services() -> GatewayResult<()> {
let addr: SocketAddr = "127.0.0.1:50052"
.parse()
.map_err(|e| grpc_graphql_gateway::Error::Other(anyhow::Error::new(e)))?;
let service = ServiceImpl::default();
tracing::info!(
"gRPC service live_example.UserService listening on {}",
addr
);
Server::builder()
.add_service(live_example::user_service_server::UserServiceServer::new(
service,
))
.serve(addr)
.await
.map_err(|e| {
tracing::error!(error = %e, "gRPC server failed to start");
grpc_graphql_gateway::Error::Other(anyhow::Error::new(e))
})?;
Ok(())
}
pub fn gateway_builder() -> GatewayResult<GatewayBuilder> {
let mut builder = Gateway::builder().with_descriptor_set_bytes(DESCRIPTOR_SET);
if FEDERATION_ENABLED {
tracing::info!(
"Federation enabled (entities: {entities})",
entities = describe_entities()
);
builder = builder
.enable_federation()
.with_entity_resolver(default_entity_resolver());
}
for svc in services::ALL {
tracing::info!(
"{svc} -> {endpoint} (queries: {queries}; mutations: {mutations}; subscriptions: {subscriptions}; resolvers: {resolvers})",
svc = svc.name,
endpoint = svc.endpoint,
queries = describe(svc.queries),
mutations = describe(svc.mutations),
subscriptions = describe(svc.subscriptions),
resolvers = describe_resolvers(svc.resolvers),
);
let client = GrpcClient::builder(svc.endpoint)
.insecure(svc.insecure)
.lazy(true)
.connect_lazy()?;
builder = builder.add_grpc_client(svc.name, client);
}
Ok(builder)
}
pub fn gateway_builder_for_service(svc: &ServiceConfig) -> GatewayResult<GatewayBuilder> {
let mut builder = Gateway::builder()
.with_descriptor_set_bytes(DESCRIPTOR_SET)
.with_persisted_queries(PersistedQueryConfig::default())
.with_response_cache(CacheConfig {
max_size: 1000,
default_ttl: Duration::from_secs(60),
..Default::default()
})
.with_request_collapsing(RequestCollapsingConfig::default());
tracing::info!(
"{svc} -> {endpoint} (queries: {queries}; mutations: {mutations}; subscriptions: {subscriptions}; resolvers: {resolvers})",
svc = svc.name,
endpoint = svc.endpoint,
queries = describe(svc.queries),
mutations = describe(svc.mutations),
subscriptions = describe(svc.subscriptions),
resolvers = describe_resolvers(svc.resolvers),
);
if FEDERATION_ENABLED {
builder = builder
.enable_federation()
.with_entity_resolver(default_entity_resolver())
.with_services([svc.name]);
} else {
builder = builder.with_services([svc.name]);
}
let client = GrpcClient::builder(svc.endpoint)
.insecure(svc.insecure)
.lazy(true)
.connect_lazy()?;
builder = builder.add_grpc_client(svc.name, client);
Ok(builder)
}
pub fn gateway_builder_for(name: &str) -> GatewayResult<Option<GatewayBuilder>> {
for svc in services::ALL {
if svc.name == name {
return gateway_builder_for_service(svc).map(Some);
}
}
Ok(None)
}
pub async fn run_live_example_userservice_gateway() -> GatewayResult<()> {
gateway_builder_for_service(&services::LIVE_EXAMPLE_USERSERVICE)?
.serve("0.0.0.0:9000")
.await
}
#[tokio::main]
async fn main() -> GatewayResult<()> {
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.init();
tracing::info!(
"GraphQL operations -> queries: {queries}; mutations: {mutations}; subscriptions: {subscriptions}; resolvers: {resolvers}",
queries = describe(QUERIES),
mutations = describe(MUTATIONS),
subscriptions = describe(SUBSCRIPTIONS),
resolvers = describe_resolvers(RESOLVERS),
);
if FEDERATION_ENABLED {
tracing::info!(
"Federation entities -> {entities}",
entities = describe_entities()
);
}
tokio::spawn(async {
if let Err(e) = run_services().await {
tracing::error!(error = %e, "gRPC services exited with error");
}
});
tokio::spawn(async {
if let Err(e) = run_live_example_userservice_gateway().await {
tracing::error!(error = %e, "GraphQL gateway for live_example.UserService exited with error");
}
});
tokio::signal::ctrl_c()
.await
.map_err(|e| grpc_graphql_gateway::Error::Other(anyhow::Error::new(e)))?;
Ok(())
}