-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Conversation
}, | ||
): Promise<void>; | ||
get(key: string): Promise<string>; | ||
invalidate(tags: string[]): Promise<void>; |
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.
Should we call this invalidateTags
to avoid confusion with key-based invalidation?
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 like it! Let's do that
docs/source/features/apq.md
Outdated
|
||
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. |
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.
generation of "the" or "this" mapping?
docs/source/features/apq.md
Outdated
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. |
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.
maybe worth mentioning it is especially nice for GETs
docs/source/features/apq.md
Outdated
|
||
> 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). |
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.
There doesn't actually seem to be a default cache.
I would lean towards the default cache being LRU instead of unbounded FWIW.
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.
Also this link doesn't work.
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 link still is dead?
docs/source/features/apq.md
Outdated
curl "http://localhost:4000/?extensions=\{\"persistedQuery\":\{\"version\":1,\"sha256Hash\":\"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38\"\}\}" | ||
``` | ||
|
||
Expect a response of: `{"errors": [{"message": "PersistedQueryNotFound"}]}`. |
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.
My response includes a stacktrace, etc.
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.
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.)
docs/source/features/apq.md
Outdated
1. Request a persisted query: | ||
|
||
```bash | ||
curl "http://localhost:4000/?extensions=\{\"persistedQuery\":\{\"version\":1,\"sha256Hash\":\"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38\"\}\}" |
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 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>; |
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.
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; |
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.
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); |
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.
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>; |
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.
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); |
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.
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 }), |
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.
Do we need Keyv
here? I think we could just use QuickLru
as the cache implementation directly.
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.
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 = { |
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.
maybe clone requestOptions if we're going to mutate it?
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.
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
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.
oops, missed that.
? await this.context({ req: request }) | ||
? await this.context({ | ||
req: request, | ||
//This cache should be the full response cache |
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.
clean this up
optionsObject, | ||
); | ||
} else if (extensions.persistedQuery.version !== 1) { | ||
throw new HttpQueryError(400, 'Unsuported persited query version'); |
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.
the first two words both have typos
optionsObject.persistedQueries.cache | ||
.set(sha, queryString) | ||
.catch(error => { | ||
optionsObject.logFunction({ |
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.
isn't logFunction optional?
throw new HttpQueryError(400, 'provided sha does not match query'); | ||
} | ||
|
||
optionsObject.persistedQueries.cache |
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.
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 }), |
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.
nice find with keyv. odd that they don't have a memcached.
cb13f4d
to
ae8ef9a
Compare
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.
almost ready, a few tweaks
return optionsObject.persistedQueries.cache.set(sha, queryString); | ||
}) | ||
.catch(error => { | ||
console.log(error); |
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 think this should only happen if you don't provide logFunction.
docs/source/features/apq.md
Outdated
|
||
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. |
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.
"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?
docs/source/features/apq.md
Outdated
|
||
> 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). |
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 link still is dead?
tags?: string[]; | ||
}, | ||
): Promise<void>; | ||
get(key: string): Promise<string> | Promise<null | undefined>; |
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.
Not sure how this differs from Promise<string |null | undefined>
get(key: string): Promise<string | null>; | ||
} | ||
|
||
export interface KeyValueCache extends PersistedQueryCache { |
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 doesn't seem to be used, so let's hold off on it until we're using it?
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 torunHttpQuery
.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 toformatParams
. 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.KeyValueCache
is chosen over another name, such asKeyValueStore
orApolloCache
, 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.