Skip to content

Commit e875939

Browse files
cache-control example (#2759)
uses cache-control headers from subgraphs to determine an overally cache-control policy partially addresses #326
1 parent 2062efc commit e875939

File tree

11 files changed

+306
-24
lines changed

11 files changed

+306
-24
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### Add cache-control response header management example in Rhai
2+
3+
This recreates some of the behavior of Apollo Gateway's cache-control header behavior and partially addresses #326.
4+
5+
By [@lennyburdette](https://github.com/lennyburdette) in https://github.com/apollographql/router/pull/2759

Cargo.lock

+12
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,18 @@ dependencies = [
899899
"either",
900900
]
901901

902+
[[package]]
903+
name = "cache-control"
904+
version = "0.1.0"
905+
dependencies = [
906+
"anyhow",
907+
"apollo-router",
908+
"http",
909+
"serde_json",
910+
"tokio",
911+
"tower",
912+
]
913+
902914
[[package]]
903915
name = "cargo-scaffold"
904916
version = "0.8.8"

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ members = [
66
"apollo-router-scaffold",
77
"examples/add-timestamp-header/rhai",
88
"examples/async-auth/rust",
9+
"examples/cache-control/rhai",
910
"examples/context/rust",
1011
"examples/cookies-to-headers/rhai",
1112
"examples/data-response-mutate/rhai",
@@ -34,7 +35,7 @@ strip = "debuginfo"
3435
incremental = false
3536

3637
# If building a dhat feature, you must use this profile
37-
# e.g. heap allocation tracing: cargo build --profile release-dhat --features dhat-heap
38+
# e.g. heap allocation tracing: cargo build --profile release-dhat --features dhat-heap
3839
# e.g. heap and ad-hoc allocation tracing: cargo build --profile release-dhat --features dhat-heap,dhat-ad-hoc
3940
[profile.release-dhat]
4041
inherits = "release"

apollo-router/src/services/subgraph.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ impl Response {
140140
extensions: Object,
141141
status_code: Option<StatusCode>,
142142
context: Context,
143+
headers: Option<http::HeaderMap<http::HeaderValue>>,
143144
) -> Response {
144145
// Build a response
145146
let res = graphql::Response::builder()
@@ -151,11 +152,13 @@ impl Response {
151152
.build();
152153

153154
// Build an http Response
154-
let response = http::Response::builder()
155+
let mut response = http::Response::builder()
155156
.status(status_code.unwrap_or(StatusCode::OK))
156157
.body(res)
157158
.expect("Response is serializable; qed");
158159

160+
*response.headers_mut() = headers.unwrap_or_default();
161+
159162
Self { response, context }
160163
}
161164

@@ -174,6 +177,7 @@ impl Response {
174177
extensions: JsonMap<ByteString, Value>,
175178
status_code: Option<StatusCode>,
176179
context: Option<Context>,
180+
headers: Option<http::HeaderMap<http::HeaderValue>>,
177181
) -> Response {
178182
Response::new(
179183
label,
@@ -183,6 +187,7 @@ impl Response {
183187
extensions,
184188
status_code,
185189
context.unwrap_or_default(),
190+
headers,
186191
)
187192
}
188193

@@ -203,6 +208,7 @@ impl Response {
203208
Default::default(),
204209
status_code,
205210
context,
211+
Default::default(),
206212
))
207213
}
208214
}

apollo-router/src/services/supergraph.rs

+14-12
Original file line numberDiff line numberDiff line change
@@ -133,29 +133,31 @@ impl Request {
133133
/// Create a request with an example query, for tests
134134
#[builder(visibility = "pub")]
135135
fn canned_new(
136+
query: Option<String>,
136137
operation_name: Option<String>,
137138
// Skip the `Object` type alias in order to use buildstructor’s map special-casing
138139
extensions: JsonMap<ByteString, Value>,
139140
context: Option<Context>,
140141
headers: MultiMap<TryIntoHeaderName, TryIntoHeaderValue>,
141142
) -> Result<Request, BoxError> {
142-
let query = "
143-
query TopProducts($first: Int) {
144-
topProducts(first: $first) {
145-
upc
146-
name
147-
reviews {
148-
id
149-
product { name }
150-
author { id name }
151-
}
152-
}
143+
let default_query = "
144+
query TopProducts($first: Int) {
145+
topProducts(first: $first) {
146+
upc
147+
name
148+
reviews {
149+
id
150+
product { name }
151+
author { id name }
152+
}
153+
}
153154
}
154155
";
156+
let query = query.unwrap_or(default_query.to_string());
155157
let mut variables = JsonMap::new();
156158
variables.insert("first", 2_usize.into());
157159
Self::fake_new(
158-
Some(query.to_owned()),
160+
Some(query),
159161
operation_name,
160162
variables,
161163
extensions,

apollo-router/tests/fixtures/request_response_test.rhai

+10-10
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,16 @@ fn process_supergraph_request(request) {
4141
}
4242
let expected_query = `
4343

44-
query TopProducts($first: Int) {
45-
topProducts(first: $first) {
46-
upc
47-
name
48-
reviews {
49-
id
50-
product { name }
51-
author { id name }
52-
}
53-
}
44+
query TopProducts($first: Int) {
45+
topProducts(first: $first) {
46+
upc
47+
name
48+
reviews {
49+
id
50+
product { name }
51+
author { id name }
52+
}
53+
}
5454
}
5555
`;
5656
if request.body.query != expected_query {
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "cache-control"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
[dependencies]
8+
anyhow = "1"
9+
apollo-router = { path = "../../../apollo-router" }
10+
http = "0.2"
11+
serde_json = "1"
12+
tokio = { version = "1", features = ["full"] }
13+
tower = { version = "0.4", features = ["full"] }

examples/cache-control/rhai/README.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Rhai script
2+
3+
Demonstrates header and context manipulation via Rhai script.
4+
5+
Usage:
6+
7+
```bash
8+
cargo run -- -s ../../graphql/supergraph.graphql -c ./router.yaml
9+
```
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
rhai:
2+
scripts: src
3+
main: cache_control.rhai
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
fn subgraph_service(service, subgraph) {
2+
// collect the max-age and scope values from cache-control headers and store
3+
// on the context for use in supergraph_service
4+
service.map_response(|response| {
5+
let cache_control = response.headers.values("cache-control").get(0);
6+
7+
// if a subgraph response is uncacheable, the whole response is uncacheable
8+
if cache_control == () {
9+
response.context.cache_control_uncacheable = true;
10+
return;
11+
}
12+
13+
let max_age = get_max_age(cache_control);
14+
15+
// use the smallest max age
16+
response.context.upsert("cache_control_max_age", |current| {
17+
if current == () {
18+
max_age
19+
} else if max_age < current {
20+
max_age
21+
} else {
22+
current
23+
}
24+
});
25+
26+
let scope = if cache_control.contains("public") {
27+
"public"
28+
} else {
29+
"private"
30+
};
31+
32+
// if the scope is ever private, it cannot become public
33+
response.context.upsert("cache_control_scope", |current| {
34+
if current == "private" || scope == "private" {
35+
"private"
36+
} else {
37+
scope
38+
}
39+
});
40+
});
41+
}
42+
43+
fn supergraph_service(service) {
44+
// attach the cache-control header if enough data is available
45+
service.map_response(|response| {
46+
let uncacheable = response.context.cache_control_uncacheable;
47+
let max_age = response.context.cache_control_max_age;
48+
let scope = response.context.cache_control_scope;
49+
50+
if uncacheable != true && max_age != () && scope != () {
51+
response.headers["cache-control"] = `max-age=${max_age}, ${scope}`;
52+
}
53+
});
54+
}
55+
56+
// find the the max-age= part and parse the value into an integer
57+
fn get_max_age(str) {
58+
let max_age = 0;
59+
60+
for part in str.split(",") {
61+
part.remove(" ");
62+
63+
if part.starts_with("max-age=") {
64+
let num = part.split("=").get(1);
65+
66+
if num == () || num == "" {
67+
break;
68+
}
69+
70+
try {
71+
max_age = num.parse_int();
72+
} catch (err) {
73+
log_error(`error parsing max-age from "${str}": ${err}`);
74+
}
75+
break;
76+
}
77+
}
78+
79+
max_age
80+
}

0 commit comments

Comments
 (0)