-
Notifications
You must be signed in to change notification settings - Fork 69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: cache-control calculation from directives #133
feat: cache-control calculation from directives #133
Conversation
I think the tests are failing due to diffplug/spotless#834 |
Can you try running |
This comment has been minimized.
This comment has been minimized.
From what I understand, the CI runs on Java8: federation-jvm/.circleci/config.yml Line 5 in 5560300
Making it run on more Java versions is certainly something to do but for this particular PR, I'd say it's fine to format the source using java8 ? PR on your PR there: lennyburdette#1 |
I'm guessing the build itself failed because CircleCI updated their Java version to use the new "Temurin" distribution instead of the old "AdoptOpenJdk" ones. See #134 |
@martinbonnin @lennyburdette |
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
@lennyburdette
As you've mentioned in #133 (comment) , this is due to a breaking change with Java 16+. There is a workaround, which is to add these lines to your
We can't commit this to the repo though, as these flags don't exist for Java 8. The other option is to develop locally with Java 8 (I use |
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
@lennyburdette |
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Show resolved
Hide resolved
226522f
to
4a5a8ff
Compare
4a5a8ff
to
102b1f0
Compare
@martinbonnin @sachindshinde thanks for the early review! Got the tests to run and made a first pass at fixes. This is for a customer. Not a critical blocker, so review in the next couple weeks should be fine. And please feel free to harshly review my Java! |
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
ExecutionResultImpl resultWithCacheControl = | ||
ExecutionResultImpl.newExecutionResult() | ||
.from(executionResult) | ||
.addExtension(EXTENSION_KEY, state.getOverallPolicyString().get()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extensions are really designed to be passed through to the client as the extension. I don't think it makes much sense to use it as something that is designed to be immediately stripped and returned as an HTTP header.
I especially think this will be confusing because we used to use a cacheControl extension (https://github.com/apollographql/apollo-cache-control) and graphql-java even still has built in support for that (graphql.cachecontrol.CacheControl)!
Other parts of federation-jvm use the "implement an interface on your context
Object" pattern. I think something like that makes more sense. However it looks like graphql-java is migrating from "arbitrary context
Object" to "specific GraphQLContext object which is a key-value map".
So I would suggest that instead of using instrumentExecutionResult you use something like the onCompleted of beginExecution, and do something like parameters.getGraphQLContext.put(CacheControlInstrumentation.RESPONSE_HEADERS, new Map("cache-control", overallPolicyString))
or something. (Note that the key in this context map can be an arbitrary object so maybe use that instead of a string?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this was helpful, thanks! i didn't realize the context api changed to be a map.
one thing i need help with though — how do i update the test apps in spring-example to pluck the value off the context and add it as an HTTP header?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, i don't know anything about spring.
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
return new SimpleInstrumentationContext<ExecutionResult>() { | ||
@Override | ||
public void onCompleted(ExecutionResult result, Throwable t) { | ||
state.restrictCachePolicy(fieldPolicy); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're not implementing dynamic cache control, then you don't actually need to do this on the onCompleted; you can just do it at the end of beginField.
That said, I don't think it would be too hard to implement dynamic cache control! I'd suggest sticking something on the GraphQLContext. Then you could have a static method CacheControlInstrumentation.cacheControlContext(GraphQLContext c) { return (CacheControlContext) c.get(CACHE_CONTROL_CONTEXT) }
to let resolvers access it.
You could then follow the pattern of the TS implementation where the _Entity code is just using dynamic cache control instead of being special-cased.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is good — i'll try implementing it now that i'm using the context for things (instead of extensions).
} | ||
} | ||
|
||
void restrict(Hint hint) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you could probably have Hint extend CacheControlPolicy in which case this duplicate method wouldn't be needed?
} | ||
|
||
void restrict(Integer maxAge) { | ||
this.maxAge = Optional.of(maxAge); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is doing a replace rather than a restrict. I see it is currently only called in a place where you've verified there's no max age already but it could be used elsewhere later... You could also just remove it and have its caller call replace instead (or well, rename it to replace).
That said it's a little weird that the way this overloaded API works is reliant on the fact that all of the fields have different types. Do we really need these single-field overloads? Vs setters with different names?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed this to be a true restrict — i'm open to changing the API but it makes sense to me the way it currently is 🤷🏻
// select the | ||
// most restrictive cache policy from those types. | ||
|
||
if (unwrappedReturnType.getName().equals(_Entity.typeName)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ran out of time this morning after reviewing everything except the two blocks of code around _Entity.
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
Sorry for the huuuuge delay. I left a few additional comments. Mostly about style and trying to understand the |
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
CacheControlState state = parameters.getInstrumentationState(); | ||
|
||
// Attach the policy to the context object | ||
state.overallPolicy.maybeAsString().ifPresent(s -> parameters.getGraphQLContext().put(CONTEXT_KEY, s)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible for state.overallPolicy
to actually be null? From what I understand, as soon as there is one field in a query (which should always be the case), there should always be a maxAge? (even if it's the default one)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have tests that assert that the overall policy is null if no field- or type-level policies are present. If we never encounter a directive, nothing happens at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if no field- or type-level policies are present.
Shouldn't it use the defaultMaxAge
in these cases? Because there will always be one root field queried and these have a defaultMaxAge
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah yes, you're correct. it should be "If we never encounter a directive and no defaultMaxAge is set, nothing happens at all."
so this is related to your next question about whether max-age: 0, public
should be emitted or if it should be null like the TS implementation. @glasser do you have an opinion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, exactly, it's all the same question!
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Show resolved
Hide resolved
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Show resolved
Hide resolved
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
|
||
if (policy.scope != null && (scope == null || !scope.equals(CacheControlScope.PRIVATE))) { | ||
this.scope = policy.scope; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: because you have individual, restrict(maxAge)
and restrict(scope)
below, I would certainly make them accept nullable values and rewrite this as:
void restrict(CacheControlPolicy policy) {
restrict(policy.maxAge)
restrict(policy.scope)
}
It's much more concise now 👍 👍 👍 . Left a few comments. The one about |
I cannot figure out any way to actually use the policy stored on the context when using graphql-java-kickstart/graphql-spring-boot. I looked for any beans/components that had access to both the context/datafetchingenvironment and the http response, but I came up with nothing. I think this is possible with graphql-kotlin and DGS, so maybe I can just document how that would work? I might be able to get some help from customers on that part. |
In DGS, both Kotlin and Java, the DgsContext has the request data. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very small suggestions, other than the hope that we implement dynamic cache control!
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
|
||
// Cache directive on the return type of this field if it's a composite type | ||
|
||
Optional<CacheControlDirective> directive = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand the logic of what you're still using Optional for vs not (I see it's no longer in fields or params which makes sense), but if @martinbonnin is happy with this level of use then I won't complain
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd personally ditch Optional
altogether but don't mind it too much and as long as it's not public API, I think it's fine to ship like this and we can always refine later if/when we want to.
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Show resolved
Hide resolved
.../main/java/com/apollographql/federation/graphqljava/caching/CacheControlInstrumentation.java
Outdated
Show resolved
Hide resolved
@berngp I don't need access to the request data, I need access to the response object so I can add a response header. Is that possible? |
I don't think we offer that today but I think it is a good idea. Will figure a mechanism you could leverage to do that. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I do believe that dynamic cache control is an important feature and that it will be much cleaner to move the Query._entities
-specific code into its resolver instead of here, that doesn't need to stand in the way of merging and releasing static cache control support. Looking great!
Thank you for the thorough review @martinbonnin and @glasser ! I don't have a merge button so can someone take over the release process for this change? |
Oh wait, I'll squash all the commits first |
36612d9
to
7aa4a83
Compare
This change adds support for the @CacheControl directive in the form of a graphql-java instrumentation class, matching the behavior of the apollo-server's built-in cache control plugin: https://github.com/apollographql/apollo-server/tree/291c17e255122d4733b23177500188d68fac55ce/packages/apollo-server-core/src/plugin/cacheControl As well as the special _entities behavior in @apollo/subgraph: https://github.com/apollographql/federation/blob/00c89f82bfcc2e27e6714d894874422f84ae468c/subgraph-js/src/types.ts#L76-L87 This change does not support setting cache hints dynamically in resolvers, only static cache hints in SDL directives. The instrumentation adds the value to the GraphQLContext object for the request. Apollo Gateway looks for a cache-control policy in a subgraph's HTTP headers, so it's up to the subgraph service owner to pluck the value off the context and add it as a response header.
7aa4a83
to
36feea3
Compare
I can do that. @sachindshinde what do we use in this repo? It's ok to create a merge commit, right? Or do we prefer rebasing and linear history? |
@sachindshinde said merge commits are ok. Merging now and I will make a new mavenCentral release hopefully later today. Many thanks @lennyburdette ! |
Huge thank you @lennyburdette 👏 |
This is now released in 0.8.0 |
This has recently been implemented by federation-jvm (see: apollographql/federation-jvm#133) A `CacheControlInstrumentation` instrumentation class must be injected, which adds the appropriate Cache-Control value into the GraphQLContext Fixes Netflix#92
This has recently been implemented by federation-jvm (see: apollographql/federation-jvm#133) A `CacheControlInstrumentation` instrumentation class must be injected, which adds the appropriate Cache-Control value into the GraphQLContext Fixes Netflix#929
This has recently been implemented by federation-jvm (see: apollographql/federation-jvm#133) A `CacheControlInstrumentation` instrumentation class must be injected, which adds the appropriate Cache-Control value into the GraphQLContext Fixes Netflix#929
This change adds support for the @CacheControl directive in the form of a graphql-java instrumentation class, matching the behavior of the apollo-server's built-in cache control plugin:
https://github.com/apollographql/apollo-server/tree/291c17e255122d4733b23177500188d68fac55ce/packages/apollo-server-core/src/plugin/cacheControl
As well as the special _entities behavior in @apollo/subgraph:
https://github.com/apollographql/federation/blob/00c89f82bfcc2e27e6714d894874422f84ae468c/subgraph-js/src/types.ts#L76-L87
This change does not support setting cache hints dynamically in resolvers, only static cache hints in SDL directives.
The instrumentation emits the value in the response's extensions object. Apollo Gateway looks for a cache-control policy in a subgraph's HTTP headers, so it's up to the subgraph service owner to pluck the value off the extensions and add it as a response header.