Skip to content

Commit

Permalink
cache-control example (#2759)
Browse files Browse the repository at this point in the history
uses cache-control headers from subgraphs to determine an overally
cache-control policy

partially addresses #326
  • Loading branch information
lennyburdette authored and garypen committed Apr 6, 2023
1 parent cc615c8 commit 6e71900
Show file tree
Hide file tree
Showing 11 changed files with 306 additions and 24 deletions.
5 changes: 5 additions & 0 deletions .changesets/docs_cache_control_rhai_example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Add cache-control response header management example in Rhai

This recreates some of the behavior of Apollo Gateway's cache-control header behavior and partially addresses #326.

By [@lennyburdette](https://github.com/lennyburdette) in https://github.com/apollographql/router/pull/2759
12 changes: 12 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,18 @@ dependencies = [
"either",
]

[[package]]
name = "cache-control"
version = "0.1.0"
dependencies = [
"anyhow",
"apollo-router",
"http",
"serde_json",
"tokio",
"tower",
]

[[package]]
name = "cargo-scaffold"
version = "0.8.8"
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"apollo-router-scaffold",
"examples/add-timestamp-header/rhai",
"examples/async-auth/rust",
"examples/cache-control/rhai",
"examples/context/rust",
"examples/cookies-to-headers/rhai",
"examples/data-response-mutate/rhai",
Expand Down Expand Up @@ -34,7 +35,7 @@ strip = "debuginfo"
incremental = false

# If building a dhat feature, you must use this profile
# e.g. heap allocation tracing: cargo build --profile release-dhat --features dhat-heap
# e.g. heap allocation tracing: cargo build --profile release-dhat --features dhat-heap
# e.g. heap and ad-hoc allocation tracing: cargo build --profile release-dhat --features dhat-heap,dhat-ad-hoc
[profile.release-dhat]
inherits = "release"
Expand Down
8 changes: 7 additions & 1 deletion apollo-router/src/services/subgraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ impl Response {
extensions: Object,
status_code: Option<StatusCode>,
context: Context,
headers: Option<http::HeaderMap<http::HeaderValue>>,
) -> Response {
// Build a response
let res = graphql::Response::builder()
Expand All @@ -151,11 +152,13 @@ impl Response {
.build();

// Build an http Response
let response = http::Response::builder()
let mut response = http::Response::builder()
.status(status_code.unwrap_or(StatusCode::OK))
.body(res)
.expect("Response is serializable; qed");

*response.headers_mut() = headers.unwrap_or_default();

Self { response, context }
}

Expand All @@ -174,6 +177,7 @@ impl Response {
extensions: JsonMap<ByteString, Value>,
status_code: Option<StatusCode>,
context: Option<Context>,
headers: Option<http::HeaderMap<http::HeaderValue>>,
) -> Response {
Response::new(
label,
Expand All @@ -183,6 +187,7 @@ impl Response {
extensions,
status_code,
context.unwrap_or_default(),
headers,
)
}

Expand All @@ -203,6 +208,7 @@ impl Response {
Default::default(),
status_code,
context,
Default::default(),
))
}
}
26 changes: 14 additions & 12 deletions apollo-router/src/services/supergraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,29 +133,31 @@ impl Request {
/// Create a request with an example query, for tests
#[builder(visibility = "pub")]
fn canned_new(
query: Option<String>,
operation_name: Option<String>,
// Skip the `Object` type alias in order to use buildstructor’s map special-casing
extensions: JsonMap<ByteString, Value>,
context: Option<Context>,
headers: MultiMap<TryIntoHeaderName, TryIntoHeaderValue>,
) -> Result<Request, BoxError> {
let query = "
query TopProducts($first: Int) {
topProducts(first: $first) {
upc
name
reviews {
id
product { name }
author { id name }
}
}
let default_query = "
query TopProducts($first: Int) {
topProducts(first: $first) {
upc
name
reviews {
id
product { name }
author { id name }
}
}
}
";
let query = query.unwrap_or(default_query.to_string());
let mut variables = JsonMap::new();
variables.insert("first", 2_usize.into());
Self::fake_new(
Some(query.to_owned()),
Some(query),
operation_name,
variables,
extensions,
Expand Down
20 changes: 10 additions & 10 deletions apollo-router/tests/fixtures/request_response_test.rhai
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,16 @@ fn process_supergraph_request(request) {
}
let expected_query = `

query TopProducts($first: Int) {
topProducts(first: $first) {
upc
name
reviews {
id
product { name }
author { id name }
}
}
query TopProducts($first: Int) {
topProducts(first: $first) {
upc
name
reviews {
id
product { name }
author { id name }
}
}
}
`;
if request.body.query != expected_query {
Expand Down
13 changes: 13 additions & 0 deletions examples/cache-control/rhai/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "cache-control"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1"
apollo-router = { path = "../../../apollo-router" }
http = "0.2"
serde_json = "1"
tokio = { version = "1", features = ["full"] }
tower = { version = "0.4", features = ["full"] }
9 changes: 9 additions & 0 deletions examples/cache-control/rhai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Rhai script

Demonstrates header and context manipulation via Rhai script.

Usage:

```bash
cargo run -- -s ../../graphql/supergraph.graphql -c ./router.yaml
```
3 changes: 3 additions & 0 deletions examples/cache-control/rhai/router.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
rhai:
scripts: src
main: cache_control.rhai
80 changes: 80 additions & 0 deletions examples/cache-control/rhai/src/cache_control.rhai
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
fn subgraph_service(service, subgraph) {
// collect the max-age and scope values from cache-control headers and store
// on the context for use in supergraph_service
service.map_response(|response| {
let cache_control = response.headers.values("cache-control").get(0);

// if a subgraph response is uncacheable, the whole response is uncacheable
if cache_control == () {
response.context.cache_control_uncacheable = true;
return;
}

let max_age = get_max_age(cache_control);

// use the smallest max age
response.context.upsert("cache_control_max_age", |current| {
if current == () {
max_age
} else if max_age < current {
max_age
} else {
current
}
});

let scope = if cache_control.contains("public") {
"public"
} else {
"private"
};

// if the scope is ever private, it cannot become public
response.context.upsert("cache_control_scope", |current| {
if current == "private" || scope == "private" {
"private"
} else {
scope
}
});
});
}

fn supergraph_service(service) {
// attach the cache-control header if enough data is available
service.map_response(|response| {
let uncacheable = response.context.cache_control_uncacheable;
let max_age = response.context.cache_control_max_age;
let scope = response.context.cache_control_scope;

if uncacheable != true && max_age != () && scope != () {
response.headers["cache-control"] = `max-age=${max_age}, ${scope}`;
}
});
}

// find the the max-age= part and parse the value into an integer
fn get_max_age(str) {
let max_age = 0;

for part in str.split(",") {
part.remove(" ");

if part.starts_with("max-age=") {
let num = part.split("=").get(1);

if num == () || num == "" {
break;
}

try {
max_age = num.parse_int();
} catch (err) {
log_error(`error parsing max-age from "${str}": ${err}`);
}
break;
}
}

max_age
}
Loading

0 comments on commit 6e71900

Please sign in to comment.