Skip to content

Commit

Permalink
feat(grpc): add support for reflection (#1647)
Browse files Browse the repository at this point in the history
Co-authored-by: Tushar Mathur <[email protected]>
Co-authored-by: Shashi Kant <[email protected]>
Co-authored-by: meskill <[email protected]>
  • Loading branch information
4 people authored May 1, 2024
1 parent 9e92a83 commit c7a25cb
Show file tree
Hide file tree
Showing 113 changed files with 921 additions and 379 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ tailcall-macros = { path = "tailcall-macros" }
tonic-types = "0.11.0"
datatest-stable = "0.2.9"
tokio-test = "0.4.4"
base64 = "0.22.0"


[dev-dependencies]
Expand Down
36 changes: 36 additions & 0 deletions examples/grpc-reflection.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# for test upstream server see [repo](https://github.com/tailcallhq/tailcall/tree/main/tailcall-upstream-grpc)
schema
@server(port: 8000, graphiql: true)
@upstream(baseURL: "http://localhost:50051", httpCache: true, batch: {delay: 10})
@link(src: "http://localhost:50051", type: Grpc) {
query: Query
}

type Query {
news: NewsData! @grpc(method: "news.NewsService.GetAllNews")
newsById(news: NewsInput!): News! @grpc(method: "news.NewsService.GetNews", body: "{{args.news}}")
newsByIdBatch(news: NewsInput!): News!
@grpc(method: "news.NewsService.GetMultipleNews", body: "{{args.news}}", batchKey: ["news", "id"])
}

type News {
id: Int
title: String
body: String
postImage: String
status: Status
}

enum Status {
PUBLISHED
DRAFT
DELETED
}

input NewsInput {
id: Int
}

type NewsData {
news: [News]!
}
3 changes: 2 additions & 1 deletion examples/lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ check_files() {
local depth=1
local -a extensions=("-name" "*.json" -o "-name" "*.yml" -o "-name" "*.yaml" -o "-name" "*.graphql" -o "-name" "*.gql")
local command="./target/debug/tailcall check"
local -a ignore=("!" "-name" "grpc-reflection.graphql")

# Execute find command with constructed options and extensions
find "$path" -maxdepth "$depth" \( "${extensions[@]}" \) -exec sh -c '
find "$path" -maxdepth "$depth" \( "${extensions[@]}" \) "${ignore[@]}" -exec sh -c '
for file; do
echo "Checking file: $file"
'"$command"' "$file" || exit 255
Expand Down
1 change: 1 addition & 0 deletions generated/.tailcallrc.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,7 @@ enum LinkType {
Operation
Htpasswd
Jwks
Grpc
}
enum Method {
GET
Expand Down
3 changes: 2 additions & 1 deletion generated/.tailcallrc.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,8 @@
"Key",
"Operation",
"Htpasswd",
"Jwks"
"Jwks",
"Grpc"
]
},
"Method": {
Expand Down
1 change: 1 addition & 0 deletions src/config/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub enum LinkType {
Operation,
Htpasswd,
Jwks,
Grpc,
}

/// The @link directive allows you to import external resources, such as
Expand Down
42 changes: 31 additions & 11 deletions src/config/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ impl ConfigReader {
Self {
runtime: runtime.clone(),
resource_reader: resource_reader.clone(),
proto_reader: ProtoReader::init(resource_reader),
proto_reader: ProtoReader::init(resource_reader, runtime),
}
}

Expand Down Expand Up @@ -59,12 +59,11 @@ impl ConfigReader {
for link in links.iter() {
let path = Self::resolve_path(&link.src, parent_dir);

let source = self.resource_reader.read_file(&path).await?;

let content = source.content;

match link.type_of {
LinkType::Config => {
let source = self.resource_reader.read_file(&path).await?;
let content = source.content;

let config = Config::from_source(Source::detect(&source.path)?, &content)?;

config_module = config_module.merge_right(ConfigModule::from(config.clone()));
Expand All @@ -80,40 +79,60 @@ impl ConfigReader {
}
}
LinkType::Protobuf => {
let path = Self::resolve_path(&link.src, parent_dir);
let meta = self.proto_reader.read(path).await?;
config_module.extensions.add_proto(meta);
}
LinkType::Script => {
let source = self.resource_reader.read_file(&path).await?;
let content = source.content;
config_module.extensions.script = Some(content);
}
LinkType::Cert => {
let source = self.resource_reader.read_file(&path).await?;
let content = source.content;
config_module
.extensions
.cert
.extend(self.load_cert(content.clone()).await?);
.extend(self.load_cert(content).await?);
}
LinkType::Key => {
config_module.extensions.keys =
Arc::new(self.load_private_key(content.clone()).await?)
let source = self.resource_reader.read_file(&path).await?;
let content = source.content;
config_module.extensions.keys = Arc::new(self.load_private_key(content).await?)
}
LinkType::Operation => {
let source = self.resource_reader.read_file(&path).await?;
let content = source.content;

config_module.extensions.endpoint_set = EndpointSet::try_new(&content)?;
}
LinkType::Htpasswd => {
let source = self.resource_reader.read_file(&path).await?;
let content = source.content;

config_module
.extensions
.htpasswd
.push(Content { id: link.id.clone(), content: content.clone() });
.push(Content { id: link.id.clone(), content });
}
LinkType::Jwks => {
let source = self.resource_reader.read_file(&path).await?;
let content = source.content;

let de = &mut serde_json::Deserializer::from_str(&content);

config_module.extensions.jwks.push(Content {
id: link.id.clone(),
content: serde_path_to_error::deserialize(de)?,
})
}
LinkType::Grpc => {
let meta = self.proto_reader.fetch(link.src.as_str()).await?;

for m in meta {
config_module.extensions.add_proto(m);
}
}
}
}

Expand Down Expand Up @@ -197,12 +216,13 @@ impl ConfigReader {

let server = &mut config_module.config.server;
let reader_ctx = ConfigReaderContext {
env: self.runtime.env.clone(),
runtime: &self.runtime,
vars: &server
.vars
.iter()
.map(|vars| (vars.key.clone(), vars.value.clone()))
.collect(),
headers: Default::default(),
};

config_module
Expand Down
31 changes: 23 additions & 8 deletions src/config/reader_context.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::sync::Arc;

use headers::HeaderMap;

use crate::has_headers::HasHeaders;
use crate::path::PathString;
use crate::EnvIO;
use crate::runtime::TargetRuntime;

pub struct ConfigReaderContext<'a> {
pub env: Arc<dyn EnvIO>,
pub runtime: &'a TargetRuntime,
pub vars: &'a BTreeMap<String, String>,
pub headers: HeaderMap,
}

impl<'a> PathString for ConfigReaderContext<'a> {
Expand All @@ -19,25 +22,37 @@ impl<'a> PathString for ConfigReaderContext<'a> {
path.split_first()
.and_then(|(head, tail)| match head.as_ref() {
"vars" => self.vars.get(tail[0].as_ref()).map(|v| v.into()),
"env" => self.env.get(tail[0].as_ref()),
"env" => self.runtime.env.get(tail[0].as_ref()),
_ => None,
})
}
}

impl HasHeaders for ConfigReaderContext<'_> {
fn headers(&self) -> &HeaderMap {
&self.headers
}
}

#[cfg(test)]
mod tests {
use std::sync::Arc;

use super::*;
use crate::tests::TestEnvIO;

#[test]
fn path_string() {
let mut runtime = crate::runtime::test::init(None);
runtime.env = Arc::new(TestEnvIO::from_iter([(
"ENV_1".to_owned(),
"ENV_VAL".to_owned(),
)]));

let reader_context = ConfigReaderContext {
env: Arc::new(TestEnvIO::from_iter([(
"ENV_1".to_owned(),
"ENV_VAL".to_owned(),
)])),
runtime: &runtime,
vars: &BTreeMap::from_iter([("VAR_1".to_owned(), "VAR_VAL".to_owned())]),
headers: Default::default(),
};

assert_eq!(
Expand Down
3 changes: 1 addition & 2 deletions src/generator/from_proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,7 @@ mod test {
use crate::generator::from_proto::from_proto;

fn get_proto_file_descriptor(name: &str) -> anyhow::Result<FileDescriptorProto> {
let path =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("src/generator/proto/{}", name));
let path = PathBuf::from(tailcall_fixtures::generator::proto::SELF).join(name);
Ok(protox_parse::parse(
name,
std::fs::read_to_string(path)?.as_str(),
Expand Down
4 changes: 2 additions & 2 deletions src/generator/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub struct Generator {
impl Generator {
pub fn init(runtime: TargetRuntime) -> Self {
Self {
proto_reader: ProtoReader::init(ResourceReader::cached(runtime)),
proto_reader: ProtoReader::init(ResourceReader::cached(runtime.clone()), runtime),
}
}

Expand Down Expand Up @@ -56,7 +56,7 @@ mod test {
async fn test_read_all() {
let server = start_mock_server();
let runtime = crate::runtime::test::init(None);
let test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/generator/proto");
let test_dir = PathBuf::from(tailcall_fixtures::generator::proto::SELF);

let news_content = runtime
.file
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ fn run_blocking() -> anyhow::Result<()> {
.on_thread_stop(|| {
TRACING_GUARD.take();
})
.enable_all()
.build()?;
rt.block_on(async { tailcall::cli::run().await })
}
Expand Down
Loading

1 comment on commit c7a25cb

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running 30s test @ http://localhost:8000/graphql

4 threads and 100 connections

Thread Stats Avg Stdev Max +/- Stdev
Latency 7.74ms 3.51ms 87.24ms 72.29%
Req/Sec 3.27k 278.01 4.64k 87.25%

390950 requests in 30.03s, 1.96GB read

Requests/sec: 13020.57

Transfer/sec: 66.83MB

Please sign in to comment.