Skip to content
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

Cache control support with federation #870

Merged
merged 5 commits into from
Aug 3, 2021
Merged

Conversation

mandiwise
Copy link
Contributor

@mandiwise mandiwise commented Jul 10, 2021

This PR allows you to use the @cacheControl directive with Apollo Federation. To take advantage of it, you need to be using Apollo Server 3.0.2 or newer in your gateway and all your subgraphs.

Specifically, this PR:

  • Changes the Query._entities resolver in subgraphs to respect @cacheControl directives on the individual types that are in the _Entity union. If all entity types returned in a given call have @cacheControl directives (or use info.cacheControl in their __resolveReference), then the field's cache control policy comes from the most restrictive of these directives instead of the default of defaultMaxAge (eg uncacheable by default). (If any entity type returned in a call does not specify a cache control policy, then it still defaults to defaultMaxAge).
  • Provide the full incoming GraphQLRequestContext to GraphQLDataSource.process. (Also make it possible for the method to tell if it's running a health check or fetching schemas, as has been requested.)
  • In RemoteGraphQLDataSource, process HTTP cache-control response headers from subgraphs and use them to restrict the operation's overall cache policy. As usual with Apollo Server cache policy, if there is no cache-control header from a subgraph then assume the whole operation is not cacheable.

(If you want to support cache control with a non-Apollo Server subgraph, just make sure it returns a proper cache-control HTTP header.)

Fixes #356.
Fixes apollographql/apollo-server#5449.
Aided by apollographql/apollo-server#5248.

@glasser
Copy link
Member

glasser commented Jul 15, 2021

The replace method is used instead of the restrict because the default maxAge of 0 for the _entites field will never be overwritten by any value >0 set on the member type's definition when restrict is used. This may create unpredictable results if two different entity types were named in the representations field argument, but it may be fine to omit this edge case for now.

This is a bug in the new AS3 cache control logic. My intention is that defaultMaxAge should not be applied if you use dynamic cache control methods. I will try to get this fix into an AS 3.0.01 ASAP. Thanks for discovering it. apollographql/apollo-server#5488

@@ -13,6 +13,17 @@ import {
isObjectType,
} from 'graphql';
import { PromiseOrValue } from 'graphql/jsutils/PromiseOrValue';
import { CacheHint, CachePolicy } from 'apollo-server-types';

declare module 'graphql/type/definition' {
Copy link
Member

Choose a reason for hiding this comment

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

This is probably not required any more once you rebase on #875 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This still seems to be necessary even after a rebase

Copy link
Member

Choose a reason for hiding this comment

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

My worry is that if you end up loading both this and apollo-server-core in your project, TypeScript will be sad to see the same declaration twice.

I'm thinking of making a chance so that the type under cacheControl is exported from apollo-server-types, so you can do something like:

  if ((info as any).cacheControl?.cacheHintFromType) {
    const cacheControl: ResolveInfoCacheControl = info.cacheControl;
    const cacheHint = cacheControl.cacheHintFromType(type);

@@ -87,6 +98,15 @@ export const entitiesField: GraphQLFieldConfig<any, any> = {
);
}

if (info.cacheControl && info.cacheControl.cacheHint.replace) {
Copy link
Member

Choose a reason for hiding this comment

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

perhaps

if (info.cacheControl?.cacheHint?.replace) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

info.cacheControl.cacheHintFromType(type);

if (cacheHint) {
info.cacheControl.cacheHint.replace(cacheHint);
Copy link
Member

Choose a reason for hiding this comment

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

This should be restrict, but requires the bug fix mentioned above.

Copy link
Member

Choose a reason for hiding this comment

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

Fix released in 3.0.1 so it should be able to be restrict. I think it's fine for us to release this with the caveat that it doesn't work with 3.0.0.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to use the restrict method with 3.0.1

Copy link
Member

@glasser glasser left a comment

Choose a reason for hiding this comment

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

Let me get out another version of AS with the type I'm mentioning here.

Then the other trick is, it would be nice to have tests, but I could write them as part of the half of apollographql/apollo-server#5449 I was going to do... So maybe I should just augment this PR with that work when I get the time for it.

const cacheHint: CacheHint | undefined =
info.cacheControl.cacheHintFromType(type);

if (cacheHint?.maxAge || cacheHint?.scope) {
Copy link
Member

Choose a reason for hiding this comment

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

I think this could just be if (cacheHint)

@@ -13,6 +13,17 @@ import {
isObjectType,
} from 'graphql';
import { PromiseOrValue } from 'graphql/jsutils/PromiseOrValue';
import { CacheHint, CachePolicy } from 'apollo-server-types';

declare module 'graphql/type/definition' {
Copy link
Member

Choose a reason for hiding this comment

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

My worry is that if you end up loading both this and apollo-server-core in your project, TypeScript will be sad to see the same declaration twice.

I'm thinking of making a chance so that the type under cacheControl is exported from apollo-server-types, so you can do something like:

  if ((info as any).cacheControl?.cacheHintFromType) {
    const cacheControl: ResolveInfoCacheControl = info.cacheControl;
    const cacheHint = cacheControl.cacheHintFromType(type);

glasser added a commit to apollographql/apollo-server that referenced this pull request Jul 19, 2021
This makes `info.cacheControl` available to TypeScript packages that
depend on `apollo-server-types` rather than only being declared deep
inside `apollo-server-core`. Additionally, it gives a name to type used
for `info.cacheControl`.

Intended for use cases like
apollographql/federation#870

We've run into tricky issues with `declare module` before so if this
ends up causing more problems than it's worth, we may revert it.
glasser added a commit to apollographql/apollo-server that referenced this pull request Jul 19, 2021
This makes `info.cacheControl` available to TypeScript packages that
depend on `apollo-server-types` rather than only being declared deep
inside `apollo-server-core`. Additionally, it gives a name to type used
for `info.cacheControl`.

Intended for use cases like
apollographql/federation#870

We've run into tricky issues with `declare module` before so if this
ends up causing more problems than it's worth, we may revert it.
glasser added a commit to apollographql/apollo-server that referenced this pull request Jul 19, 2021
)

This makes `info.cacheControl` available to TypeScript packages that
depend on `apollo-server-types` rather than only being declared deep
inside `apollo-server-core`. Additionally, it gives a name to type used
for `info.cacheControl`.

Intended for use cases like
apollographql/federation#870

We've run into tricky issues with `declare module` before so if this
ends up causing more problems than it's worth, we may revert it.
@glasser
Copy link
Member

glasser commented Jul 20, 2021

@mandiwise I rebased, upgraded apollo-server-types to the version I just released, and fixed the above. Later I will add my promised half of the project and write some tests.

@glasser glasser changed the title Check for cache hint when resolving entities field Cache control support with federation Jul 20, 2021
@glasser glasser force-pushed the main branch 2 times, most recently from 9b46f6c to 10b4a30 Compare July 21, 2021 05:14
@glasser
Copy link
Member

glasser commented Jul 21, 2021

@mandiwise: I squashed your commits together and rewrote the PR description. This PR now implements cache control for federation, end-to-end. It requires Apollo Server 3.0.2 to work properly. It is tested end to end. I have not had a chance to seek out and update docs in this repo or apollo-server yet, and might not get to that this week.

@vany0114
Copy link

This is great guys @mandiwise @glasser thanks for tackle this, I've been waiting for this. Do you guys already have an ETA to release it?

@glasser
Copy link
Member

glasser commented Jul 21, 2021

Once we've got docs and code review!

@netlify
Copy link

netlify bot commented Jul 22, 2021

👷 Deploy request for apollo-federation-docs pending review.
Visit the deploys page to approve it

🔨 Explore the source changes: 3742084fa6cb3c11c5d4ee76a39f2bd18bf8975a

* one of the strings if this operation is generated by the gateway without an
* incoming request.
*/
topLevelRequestContext:
Copy link
Member

Choose a reason for hiding this comment

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

@trevor-scheer Do you like this name?

Do you like this type or should we go with something more like {kind: 'health check'}|{kind: 'loading schema'}|{kind: 'incoming operation', requestContext: GraphQLRequestContext}

Copy link
Member

Choose a reason for hiding this comment

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

OK, I think we definitely want the kind sort of thing in case we want more information associated with health checks etc, so I've rebased the PR so it does that. Still curious if you like the topLevelRequestContext name :) I think it's better than incomingRequestContext because not all are based on incoming requests. Of course the fact that it doesn't always have a requestContext on it is a little odd. 🤷

@mandiwise
Copy link
Contributor Author

The caching docs have been updated in this PR: apollographql/apollo-server#5536

@glasser
Copy link
Member

glasser commented Jul 23, 2021

I could possibly move the kind up a level to be a top level option and then have incomingRequestContext: GraphQLRequestContext be a top level option that exists when kind is incoming operation. Thoughts @abernix ?

(edit: I did this)

Copy link
Member

@glasser glasser left a comment

Choose a reason for hiding this comment

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

Adding my approval for technical reasons, but this still needs review by somebody else. To be organized by @abernix.

Copy link
Member

@trevor-scheer trevor-scheer left a comment

Choose a reason for hiding this comment

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

💵 🎛 🥳

@@ -15,6 +15,14 @@ beforeEach(() => {
fetch.mockReset();
});

// Right now, none of these tests care what's on topLevelRequestContext, so we
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Right now, none of these tests care what's on topLevelRequestContext, so we
// Right now, none of these tests care what's on incomingRequestContext, so we

Copy link
Member

Choose a reason for hiding this comment

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

Done

Comment on lines 27 to 40
/**
* Equivalent to incomingRequestContext.context (provided here for
* backwards compatibility): the object created by the Apollo Server
* `context` function.
*/
Copy link
Member

Choose a reason for hiding this comment

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

Can we add a @deprecated annotation on this doc block?

Suggested change
/**
* Equivalent to incomingRequestContext.context (provided here for
* backwards compatibility): the object created by the Apollo Server
* `context` function.
*/
/**
* Equivalent to incomingRequestContext.context (provided here for
* backwards compatibility): the object created by the Apollo Server
* `context` function.
*
* @deprecated Use `incomingRequestContext.context` instead
*/

Copy link
Member

Choose a reason for hiding this comment

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

Done (with a few more words)

context: GraphQLRequestContext<TContext>['context'];
}
| {
kind: 'health check';
Copy link
Member

Choose a reason for hiding this comment

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

This is maybe just a bit off of convention. Tempting to make this an enum or enum-esque a la graphql's Kind https://github.com/graphql/graphql-js/blob/main/src/language/kinds.ts

That being said, it's not like TS struggles at all with these string types - it'll auto-complete and type-check these kind properties just fine as-is.

Copy link
Member

Choose a reason for hiding this comment

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

OK, enum it is.

glasser added 4 commits August 2, 2021 22:31
This lets the data source pay attention to any piece of the incoming
request (say, its HTTP structure, or the incoming operation itself), not
just the user context object.

In the two cases where we use the data source without an incoming
request (health checks and schema loading via introspection), pass a
special string to indicate this, which lets data sources handle these
operations specially.

Fixes apollographql#419. Fixes apollographql#835.
Use them to restrict the overall cache policy.

This behavior can be disabled by passing
`honorSubgraphCacheControlHeader: false` to the RemoteGraphQLDataSource
constructor (if you are setting your cache policy some other way).
@glasser glasser enabled auto-merge August 3, 2021 05:35
@glasser glasser merged commit 616caf2 into apollographql:main Aug 3, 2021
@glasser
Copy link
Member

glasser commented Aug 3, 2021

Merged!

Let's do a minor version bump release of federation and gateway tomorrow, and then merge apollographql/apollo-server#5536 too (adding version number references to it).

@vany0114
Copy link

@glasser Guys I'm trying to use the cache control in a federated schema (using the versions mentioned here) using type-graphql but it's not working. I used to have a workaround that was merging the schemas and it was working fine, but after I moved to the latest version I obviously removed it (the workaround) but is not working anymore.

workaround:

const finalSchema = mergeSchemas({
    schemaDirectives: {
      cacheControl: CacheControlDirective,
    },
    schemas: [federatedSchema, schema],
  });

I'm registering the cacheContol directive in the subgraph like this:

  SchemaDirectiveVisitor.visitSchemaDirectives(schema, {
    cacheControl: CacheControlDirective,
  });

Also the enum:

registerEnumType(CacheScope, {
  name: 'CacheControlScope',
  description: 'Scopt of Cache Control',
});

Any ideas what might I be missing?

@glasser
Copy link
Member

glasser commented Sep 21, 2021

@vany0114 I'm not really familiar enough with the details of what you're doing to help out. For example, I'm not sure what the CacheControlDirective symbol you have above is; I don't know much about type-graphql; and I'm not even sure if you're doing this in a subgraph or in your gateway. Can you provide a full reproduction example (a repo to git clone or a codesandbox.io sandbox, with full instructions of what to do to see the issue)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cache control and federation Apollo Federation/Gateway doesn't support Cache Control
4 participants