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

Apollo Server 2:Automatic Persisted Queries + KeyValueCache Interface #1149

Merged
merged 23 commits into from
Jun 11, 2018

Conversation

evans
Copy link
Contributor

@evans evans commented Jun 7, 2018

This adds support and tests for automatic persisted queries into Apollo Server 2. The cache interface contains get, set, and invalidate and uses strings. A cache can be passed into the ApolloServer constructor or added by the registerServer call. It is currently accessible to runHttpQuery.

The current implementation caches query strings over parsed ASTs. Currently, the graphql-js parser is quite optimized, so the overhead is negligible. If in the future, this overhead becomes significant, then we can address it.

@glasser and I had chatted about an implementation as an extension by adding in another method that could modify the arguments to runQuery, similar to formatParams. This is possible. It would change the error behavior and expectations around having a queryString or parsedQuery, probably to include extensions.persistedQuery. It would also need to have some way of short circuiting the response to include an error, which makes ordering/execution state of extensions much more complicated.

the similarity to formatParams to APQ's does argue in favor of extensions

KeyValueCache is chosen over another name, such as KeyValueStore or ApolloCache, since the cache is generally in-memory and not persisted to disk. Prepending Apollo would suggest that the cache is GraphQL specific like the clients, however the server cache does no normalization, so a more generic name is appropriate.

  • CHANGELOG
  • needs docs

@ghost ghost added the ⛲️ feature New addition or enhancement to existing solutions label Jun 7, 2018
@evans evans added this to the Release 2.0 milestone Jun 7, 2018
@evans evans changed the title Apollo Server 2:Automatic Persisted Queries + ApolloCache Interface Apollo Server 2:Automatic Persisted Queries + KeyValueCache Interface Jun 7, 2018
@ghost ghost added the ⛲️ feature New addition or enhancement to existing solutions label Jun 7, 2018
},
): Promise<void>;
get(key: string): Promise<string>;
invalidate(tags: string[]): Promise<void>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we call this invalidateTags to avoid confusion with key-based invalidation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like it! Let's do that


The size of individual GraphQL query strings can be a major pain point. Apollo Server allows implements Automatic Persisted Queries (APQ), a technique that greatly improves network performance for GraphQL with zero build-time configuration. A persisted query is a ID or hash that can be sent to the server instead of the entire GraphQL query string. This smaller signature reduces bandwidth utilization and speeds up client loading times.

To accommodate this new query representation, Apollo Server contains a cache that stores the mapping between hash and query string, efficiently retrieving the corresponding query. Previously, the generation of mapping would require a complex build step. Paired with Apollo Client, Apollo Server provides the ability to generate this mapping automatically.
Copy link
Member

Choose a reason for hiding this comment

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

generation of "the" or "this" mapping?

description: Reduce the size of GraphQL requests over the wire
---

The size of individual GraphQL query strings can be a major pain point. Apollo Server allows implements Automatic Persisted Queries (APQ), a technique that greatly improves network performance for GraphQL with zero build-time configuration. A persisted query is a ID or hash that can be sent to the server instead of the entire GraphQL query string. This smaller signature reduces bandwidth utilization and speeds up client loading times.
Copy link
Member

Choose a reason for hiding this comment

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

maybe worth mentioning it is especially nice for GETs


> Note: using `apollo-link-persisted-query` requires migrating from [apollo-boost](https://www.apollographql.com/docs/react/advanced/boost-migration.html):

Inside Apollo Server, the query registry is stored in a user-configurable cache. By default, Apollo Server uses a in-memory cache (shared with other caching features). Read more here about [how to configure caching](caching.html).
Copy link
Member

Choose a reason for hiding this comment

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

There doesn't actually seem to be a default cache.

I would lean towards the default cache being LRU instead of unbounded FWIW.

Copy link
Member

Choose a reason for hiding this comment

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

Also this link doesn't work.

Copy link
Member

Choose a reason for hiding this comment

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

This link still is dead?

curl "http://localhost:4000/?extensions=\{\"persistedQuery\":\{\"version\":1,\"sha256Hash\":\"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38\"\}\}"
```

Expect a response of: `{"errors": [{"message": "PersistedQueryNotFound"}]}`.
Copy link
Member

Choose a reason for hiding this comment

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

My response includes a stacktrace, etc.

Copy link
Member

Choose a reason for hiding this comment

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

Also unrelatedly, it would be nice if runHttpQuery added a newline to the end of its return values! Would make using curl less ugly. (This is for all calls to JSON.stringify in runHttpQuery; probably add a helper.)

1. Request a persisted query:

```bash
curl "http://localhost:4000/?extensions=\{\"persistedQuery\":\{\"version\":1,\"sha256Hash\":\"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38\"\}\}"
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 these look a little nicer without the escaping (though then you have to pass -g):

curl -g 'http://localhost:4000/?extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}'

},
): Promise<void>;
get(key: string): Promise<string>;
invalidateTags(tags: string[]): Promise<void>;
Copy link
Member

Choose a reason for hiding this comment

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

What is this? It doesn't seem useful for PQ caches (which are hash-based), so it just seems like it might create a larger requirement for implementing an appropriate cache. Same with TTL.

@@ -41,6 +42,7 @@ export interface GraphQLServerOptions<
// cacheControl?: boolean | CacheControlExtensionOptions;
cacheControl?: boolean | any;
extensions?: Array<() => GraphQLExtension>;
cache?: KeyValueCache;
Copy link
Member

Choose a reason for hiding this comment

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

As mentioned on slack I think this should explicitly be persistedQueries: { cache } since you may want a different implementation for other caching solutions. Plus it is surprising that passing something to top level "cache" doesn't actually give you a GraphQL cache.

// It looks like we've received an Apollo Persisted Query.
const sha = extensions.persistedQuery.sha256Hash;

queryString = await optionsObject.cache.get(sha);
Copy link
Member

Choose a reason for hiding this comment

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

Guard against cache not existing (you should be allowed to explicitly disable PQ), send the PersistedQueryNotSupported error in that case.

If you really are encouraging people to pass literally the same cache to multiple features, we should prefix cache keys with some identifier character so that different features don't interact. On the other hand probably there's no good reason to pass the same in-memory hash to multiple features, and for memcache maybe it's just the job of the person building the memcache object to specify a per-feature prefix?

The Engine trace proto has persisted_query_hit and persisted_query_register bools (we don't track PQ misses since there's no trace, and also neither of these are currently shown in the UI). It would be great to get equivalent bools into, say, requestDidStart.

tags?: string[];
},
): Promise<void>;
get(key: string): Promise<string>;
Copy link
Member

Choose a reason for hiding this comment

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

What does this API do if the item is not found in the cache? Is that trivially distinguishable from "error talking to the cache" and "cache value is the empty string"? Promise<string | null> would be one way to do this.

if (sha !== calculatedSha) {
throw new HttpQueryError(400, `provided sha does not match query`);
}
await optionsObject.cache.set(sha, queryString);
Copy link
Member

Choose a reason for hiding this comment

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

Ditto for guard.

In engineproxy we explicitly did this write asynchronously and logged errors but didn't wait on the result. Might be a good idea here too, as long as we're comfortable logging errors (or doing something else with them)!

requestOptions.persistedQueries = {
cache: new Keyv(),
cache: new Keyv({ store: lru }),
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need Keyv here? I think we could just use QuickLru as the cache implementation directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Keyv adds the ability to have the same Promise based interface

//https://github.com/withspectrum/spectrum has about 200 instances of gql`
//300 queries seems reasonable
const lru = new QuickLru({ maxSize: 300 });
requestOptions.persistedQueries = {
Copy link
Member

Choose a reason for hiding this comment

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

maybe clone requestOptions if we're going to mutate it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

requestOptions is a clone of config, set in the destructuring. Are you thinking that we need to deep clone as opposed to the just having an internal reference

Copy link
Member

Choose a reason for hiding this comment

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

oops, missed that.

? await this.context({ req: request })
? await this.context({
req: request,
//This cache should be the full response cache
Copy link
Member

Choose a reason for hiding this comment

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

clean this up

optionsObject,
);
} else if (extensions.persistedQuery.version !== 1) {
throw new HttpQueryError(400, 'Unsuported persited query version');
Copy link
Member

Choose a reason for hiding this comment

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

the first two words both have typos

optionsObject.persistedQueries.cache
.set(sha, queryString)
.catch(error => {
optionsObject.logFunction({
Copy link
Member

Choose a reason for hiding this comment

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

isn't logFunction optional?

throw new HttpQueryError(400, 'provided sha does not match query');
}

optionsObject.persistedQueries.cache
Copy link
Member

Choose a reason for hiding this comment

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

worth having a comment explicitly saying we're purposefully not waiting here.

also you could do the whole operation async by starting with Promise.resolve().then, but maybe this is fine.

//300 queries seems reasonable
const lru = new QuickLru({ maxSize: 300 });
requestOptions.persistedQueries = {
cache: new Keyv({ store: lru }),
Copy link
Member

Choose a reason for hiding this comment

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

nice find with keyv. odd that they don't have a memcached.

@evans evans force-pushed the server-2.0/apq branch 2 times, most recently from cb13f4d to ae8ef9a Compare June 11, 2018 21:27
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.

almost ready, a few tweaks

return optionsObject.persistedQueries.cache.set(sha, queryString);
})
.catch(error => {
console.log(error);
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 should only happen if you don't provide logFunction.


The size of individual GraphQL query strings can be a major pain point. Apollo Server allows implements Automatic Persisted Queries (APQ), a technique that greatly improves network performance for GraphQL with zero build-time configuration. A persisted query is a ID or hash that can be sent to the server instead of the entire GraphQL query string. This smaller signature reduces bandwidth utilization and speeds up client loading times. Persisted queries are especially nice paired with GET requests, enabling the browser cache and [integration with a CDN](#get).

To accommodate the new query representation, Apollo Server contains a cache that stores the mapping between hash and query string. Previously, the generation of mapping would require a complex build step. Apollo Server paired with Apollo Client provides the ability to generate the mapping automatically.
Copy link
Member

Choose a reason for hiding this comment

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

"previously" isn't really a thing that actually happened though

With Apollo Persisted Queries, the ID is a deterministic hash of the input query, so you don't need a complex build step to share the ID between clients and servers. If a server doesn't know about a given hash, the client can expand the query for it; Apollo Server caches that mapping.

perhaps?


> Note: using `apollo-link-persisted-query` requires migrating from [apollo-boost](https://www.apollographql.com/docs/react/advanced/boost-migration.html):

Inside Apollo Server, the query registry is stored in a user-configurable cache. By default, Apollo Server uses a in-memory cache (shared with other caching features). Read more here about [how to configure caching](caching.html).
Copy link
Member

Choose a reason for hiding this comment

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

This link still is dead?

tags?: string[];
},
): Promise<void>;
get(key: string): Promise<string> | Promise<null | undefined>;
Copy link
Member

Choose a reason for hiding this comment

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

Not sure how this differs from Promise<string |null | undefined>

get(key: string): Promise<string | null>;
}

export interface KeyValueCache extends PersistedQueryCache {
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't seem to be used, so let's hold off on it until we're using it?

@glasser glasser merged commit a7cd3a4 into version-2 Jun 11, 2018
@glasser glasser deleted the server-2.0/apq branch June 11, 2018 22:44
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 23, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
⛲️ feature New addition or enhancement to existing solutions
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants