Skip to content

Commit

Permalink
feat: add enableFederation flag to server (#2919)
Browse files Browse the repository at this point in the history
Co-authored-by: Tushar Mathur <[email protected]>
  • Loading branch information
meskill and tusharmath authored Oct 1, 2024
1 parent aab7e07 commit cec5896
Show file tree
Hide file tree
Showing 26 changed files with 563 additions and 52 deletions.
5 changes: 5 additions & 0 deletions generated/.tailcallrc.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,11 @@ directive @server(
as nonce-based APIs.
"""
dedupe: Boolean
"""
`enableFederation` enables functionality to Tailcall server to act as a federation
subgraph.
"""
enableFederation: Boolean
enableJIT: Boolean
"""
`globalResponseTimeout` sets the maximum query duration before termination, acting
Expand Down
7 changes: 7 additions & 0 deletions generated/.tailcallrc.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,13 @@
"null"
]
},
"enableFederation": {
"description": "`enableFederation` enables functionality to Tailcall server to act as a federation subgraph.",
"type": [
"boolean",
"null"
]
},
"enableJIT": {
"type": [
"boolean",
Expand Down
9 changes: 9 additions & 0 deletions src/core/config/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ pub struct Server {
/// and operations. @default `true`.
pub introspection: Option<bool>,

/// `enableFederation` enables functionality to Tailcall server to act
/// as a federation subgraph.
#[serde(default, skip_serializing_if = "is_default")]
pub enable_federation: Option<bool>,

#[serde(default, skip_serializing_if = "is_default")]
/// `pipelineFlush` allows to control flushing behavior of the server
/// pipeline.
Expand Down Expand Up @@ -277,6 +282,10 @@ impl Server {
pub fn get_routes(&self) -> Routes {
self.routes.clone().unwrap_or_default()
}

pub fn get_enable_federation(&self) -> bool {
self.enable_federation.unwrap_or(false)
}
}

#[cfg(test)]
Expand Down
95 changes: 50 additions & 45 deletions src/core/config/transformer/subgraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ impl Transform for Subgraph {
type Error = String;

fn transform(&self, mut config: Self::Value) -> Valid<Self::Value, Self::Error> {
if !config.server.get_enable_federation() {
// if federation is disabled don't process the config
return Valid::succeed(config);
}

let mut resolver_by_type = BTreeMap::new();

let valid = Valid::from_iter(config.types.iter_mut(), |(type_name, ty)| {
Expand All @@ -55,26 +60,6 @@ impl Transform for Subgraph {
return valid.map_to(config);
}

if resolver_by_type.is_empty() {
return Valid::succeed(config);
}

let entity_union = Union {
types: resolver_by_type.keys().cloned().collect(),
..Default::default()
};

let entity_resolver = config::EntityResolver { resolver_by_type };

// union that wraps any possible types for entities
config
.unions
.insert(UNION_ENTITIES_NAME.to_owned(), entity_union);
// any scalar for argument `representations`
config
.types
.insert(ENTITIES_TYPE_NAME.to_owned(), config::Type::default());

let service_field = Field { type_of: "String".to_string().into(), ..Default::default() };

let service_type = config::Type {
Expand All @@ -87,38 +72,15 @@ impl Transform for Subgraph {
.types
.insert(SERVICE_TYPE_NAME.to_owned(), service_type);

let query_type = match config.schema.query.as_ref() {
let query_type_name = match config.schema.query.as_ref() {
Some(name) => name,
None => {
config.schema.query = Some("Query".to_string());
"Query"
}
};

let query_type = config.types.entry(query_type.to_owned()).or_default();

let arg = Arg {
type_of: Type::from(ENTITIES_TYPE_NAME.to_string())
.into_required()
.into_list()
.into_required(),
..Default::default()
};

query_type.fields.insert(
ENTITIES_FIELD_NAME.to_string(),
Field {
type_of: Type::from(UNION_ENTITIES_NAME.to_owned())
.into_list()
.into_required(),
args: [(ENTITIES_ARG_NAME.to_owned(), arg)].into_iter().collect(),
doc: Some("Apollo federation Query._entities resolver".to_string()),
resolver: Some(Resolver::ApolloFederation(
ApolloFederation::EntityResolver(entity_resolver),
)),
..Default::default()
},
);
let query_type = config.types.entry(query_type_name.to_owned()).or_default();

query_type.fields.insert(
SERVICE_FIELD_NAME.to_string(),
Expand All @@ -130,6 +92,49 @@ impl Transform for Subgraph {
},
);

if !resolver_by_type.is_empty() {
let entity_union = Union {
types: resolver_by_type.keys().cloned().collect(),
..Default::default()
};

let entity_resolver = config::EntityResolver { resolver_by_type };

// union that wraps any possible types for entities
config
.unions
.insert(UNION_ENTITIES_NAME.to_owned(), entity_union);
// any scalar for argument `representations`
config
.types
.insert(ENTITIES_TYPE_NAME.to_owned(), config::Type::default());

let query_type = config.types.entry(query_type_name.to_owned()).or_default();

let arg = Arg {
type_of: Type::from(ENTITIES_TYPE_NAME.to_string())
.into_required()
.into_list()
.into_required(),
..Default::default()
};

query_type.fields.insert(
ENTITIES_FIELD_NAME.to_string(),
Field {
type_of: Type::from(UNION_ENTITIES_NAME.to_owned())
.into_list()
.into_required(),
args: [(ENTITIES_ARG_NAME.to_owned(), arg)].into_iter().collect(),
doc: Some("Apollo federation Query._entities resolver".to_string()),
resolver: Some(Resolver::ApolloFederation(
ApolloFederation::EntityResolver(entity_resolver),
)),
..Default::default()
},
);
}

Valid::succeed(config)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ expression: response
"body": {
"data": {
"_service": {
"sdl": "schema @server(port: 8000) @upstream(baseURL: \"http://jsonplaceholder.typicode.com\", batch: {delay: 100, headers: []}, httpCache: 42) {\n query: Query\n}\n\nscalar _Any\n\nunion _Entity = Post | User\n\ntype Post @graphQL(args: [{key: \"id\", value: \"{{.value.id}}\"}], baseURL: \"http://upstream/graphql\", batch: true, name: \"post\") @key(fields: \"id\") {\n id: Int!\n title: String!\n}\n\ntype Query {\n \"\"\"\n Apollo federation Query._entities resolver\n \"\"\"\n _entities(representations: [_Any!]!): [_Entity]!\n \"\"\"\n Apollo federation Query._service resolver\n \"\"\"\n _service: _Service!\n user(id: Int!): User @http(path: \"/users/{{.args.id}}\")\n}\n\ntype User @http(batchKey: [\"id\"], path: \"/users\", query: [{key: \"id\", value: \"{{.value.id}}\"}]) @key(fields: \"id\") {\n id: Int!\n name: String!\n}\n\ntype _Service {\n sdl: String\n}\nextend schema @link(\n\turl: \"https://specs.apollo.dev/federation/v2.3\",\n\timport: [\"@key\", \"@tag\", \"@shareable\", \"@inaccessible\", \"@override\", \"@external\", \"@provides\", \"@requires\", \"@composeDirective\", \"@interfaceObject\"]\n)\n"
"sdl": "schema @server(enableFederation: true, port: 8000) @upstream(baseURL: \"http://jsonplaceholder.typicode.com\", batch: {delay: 100, headers: []}, httpCache: 42) {\n query: Query\n}\n\nscalar _Any\n\nunion _Entity = Post | User\n\ntype Post @graphQL(args: [{key: \"id\", value: \"{{.value.id}}\"}], baseURL: \"http://upstream/graphql\", batch: true, name: \"post\") @key(fields: \"id\") {\n id: Int!\n title: String!\n}\n\ntype Query {\n \"\"\"\n Apollo federation Query._entities resolver\n \"\"\"\n _entities(representations: [_Any!]!): [_Entity]!\n \"\"\"\n Apollo federation Query._service resolver\n \"\"\"\n _service: _Service!\n user(id: Int!): User @http(path: \"/users/{{.args.id}}\")\n}\n\ntype User @http(batchKey: [\"id\"], path: \"/users\", query: [{key: \"id\", value: \"{{.value.id}}\"}]) @key(fields: \"id\") {\n id: Int!\n name: String!\n}\n\ntype _Service {\n sdl: String\n}\nextend schema @link(\n\turl: \"https://specs.apollo.dev/federation/v2.3\",\n\timport: [\"@key\", \"@tag\", \"@shareable\", \"@inaccessible\", \"@override\", \"@external\", \"@provides\", \"@requires\", \"@composeDirective\", \"@interfaceObject\"]\n)\n"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ source: tests/core/spec.rs
expression: formatter
---
schema
@server(port: 8000)
@server(enableFederation: true, port: 8000)
@upstream(baseURL: "http://jsonplaceholder.typicode.com", batch: {delay: 100, headers: []}, httpCache: 42) {
query: Query
}
Expand Down
2 changes: 1 addition & 1 deletion tests/core/snapshots/apollo-federation-entities.md_1.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ expression: response
"body": {
"data": {
"_service": {
"sdl": "schema @server(port: 8000) @upstream(baseURL: \"http://jsonplaceholder.typicode.com\", batch: {delay: 100, headers: []}, httpCache: 42) {\n query: Query\n}\n\nscalar _Any\n\nunion _Entity = Post | User\n\ntype Post @expr(body: {id: \"{{.value.id}}\", title: \"post-title-{{.value.id}}\"}) @key(fields: \"id\") {\n id: Int!\n title: String!\n}\n\ntype Query {\n \"\"\"\n Apollo federation Query._entities resolver\n \"\"\"\n _entities(representations: [_Any!]!): [_Entity]!\n \"\"\"\n Apollo federation Query._service resolver\n \"\"\"\n _service: _Service!\n user(id: Int!): User @http(path: \"/users/{{.args.id}}\")\n}\n\ntype User @call(steps: [{query: \"user\", args: {id: \"{{.value.id}}\"}}]) @key(fields: \"id\") {\n id: Int!\n name: String!\n}\n\ntype _Service {\n sdl: String\n}\nextend schema @link(\n\turl: \"https://specs.apollo.dev/federation/v2.3\",\n\timport: [\"@key\", \"@tag\", \"@shareable\", \"@inaccessible\", \"@override\", \"@external\", \"@provides\", \"@requires\", \"@composeDirective\", \"@interfaceObject\"]\n)\n"
"sdl": "schema @server(enableFederation: true, port: 8000) @upstream(baseURL: \"http://jsonplaceholder.typicode.com\", batch: {delay: 100, headers: []}, httpCache: 42) {\n query: Query\n}\n\nscalar _Any\n\nunion _Entity = Post | User\n\ntype Post @expr(body: {id: \"{{.value.id}}\", title: \"post-title-{{.value.id}}\"}) @key(fields: \"id\") {\n id: Int!\n title: String!\n}\n\ntype Query {\n \"\"\"\n Apollo federation Query._entities resolver\n \"\"\"\n _entities(representations: [_Any!]!): [_Entity]!\n \"\"\"\n Apollo federation Query._service resolver\n \"\"\"\n _service: _Service!\n user(id: Int!): User @http(path: \"/users/{{.args.id}}\")\n}\n\ntype User @call(steps: [{query: \"user\", args: {id: \"{{.value.id}}\"}}]) @key(fields: \"id\") {\n id: Int!\n name: String!\n}\n\ntype _Service {\n sdl: String\n}\nextend schema @link(\n\turl: \"https://specs.apollo.dev/federation/v2.3\",\n\timport: [\"@key\", \"@tag\", \"@shareable\", \"@inaccessible\", \"@override\", \"@external\", \"@provides\", \"@requires\", \"@composeDirective\", \"@interfaceObject\"]\n)\n"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ source: tests/core/spec.rs
expression: formatter
---
schema
@server(port: 8000)
@server(enableFederation: true, port: 8000)
@upstream(baseURL: "http://jsonplaceholder.typicode.com", batch: {delay: 100, headers: []}, httpCache: 42)
@link(src: "./posts.graphql", type: Config) {
query: Query
Expand Down
13 changes: 13 additions & 0 deletions tests/core/snapshots/federation-subgraph-force-disabled.md_0.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: tests/core/spec.rs
expression: response
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"data": {}
}
}
13 changes: 13 additions & 0 deletions tests/core/snapshots/federation-subgraph-force-disabled.md_1.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: tests/core/spec.rs
expression: response
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"data": {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
source: tests/core/spec.rs
expression: formatted
---
scalar Bytes

scalar Date

scalar DateTime

scalar Email

scalar Empty

scalar Int128

scalar Int16

scalar Int32

scalar Int64

scalar Int8

scalar JSON

scalar PhoneNumber

type Query {
user(id: Int!): User
}

scalar UInt128

scalar UInt16

scalar UInt32

scalar UInt64

scalar UInt8

scalar Url

type User {
id: Int!
name: String!
}

schema {
query: Query
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
source: tests/core/spec.rs
expression: formatter
---
schema
@server(enableFederation: false, port: 8000)
@upstream(baseURL: "http://jsonplaceholder.typicode.com", batch: {delay: 100, headers: []}, httpCache: 42) {
query: Query
}

type Post @expr(body: {id: "{{.value.id}}", title: "post-title-{{.value.id}}"}) {
id: Int!
title: String!
}

type Query {
user(id: Int!): User @http(path: "/users/{{.args.id}}")
}

type User @call(steps: [{query: "user", args: {id: "{{.value.id}}"}}]) {
id: Int!
name: String!
}
13 changes: 13 additions & 0 deletions tests/core/snapshots/federation-subgraph-force-enabled.md_0.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: tests/core/spec.rs
expression: response
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"data": {}
}
}
17 changes: 17 additions & 0 deletions tests/core/snapshots/federation-subgraph-force-enabled.md_1.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
source: tests/core/spec.rs
expression: response
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"data": {
"_service": {
"sdl": "schema @server(enableFederation: true, port: 8000) @upstream(baseURL: \"http://jsonplaceholder.typicode.com\", batch: {delay: 100, headers: []}, httpCache: 42) {\n query: Query\n}\n\ntype Post {\n id: Int!\n title: String!\n}\n\ntype Query {\n \"\"\"\n Apollo federation Query._service resolver\n \"\"\"\n _service: _Service!\n user(id: Int!): User @http(path: \"/users/{{.args.id}}\")\n}\n\ntype User {\n id: Int!\n name: String!\n}\n\ntype _Service {\n sdl: String\n}\nextend schema @link(\n\turl: \"https://specs.apollo.dev/federation/v2.3\",\n\timport: [\"@key\", \"@tag\", \"@shareable\", \"@inaccessible\", \"@override\", \"@external\", \"@provides\", \"@requires\", \"@composeDirective\", \"@interfaceObject\"]\n)\n"
}
}
}
}
Loading

1 comment on commit cec5896

@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 11.54ms 4.48ms 176.02ms 89.81%
Req/Sec 2.20k 231.85 2.74k 88.67%

263062 requests in 30.03s, 1.32GB read

Requests/sec: 8760.42

Transfer/sec: 44.96MB

Please sign in to comment.