-
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
Changes from 22 commits
719fa93
bba1902
fc788d0
53e25b6
2268c7b
681a99a
6e463c7
45ca6d7
2a85be9
81431d2
c961b43
085b71d
11a005e
d480191
1481c7e
87cbe51
317cf0d
b130457
1ae8e32
8cba571
0f46177
fd27bc8
dfde27c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
--- | ||
title: Automatic Persisted Queries | ||
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. 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. | ||
|
||
<h2 id="setup">Setup</h2> | ||
|
||
To get started with APQ, add the [Automatic Persisted Queries Link](https://github.com/apollographql/apollo-link-persisted-queries) to the client codebase with `npm install apollo-link-persisted-queries`. Next incorporate the APQ link with Apollo Client's link chain before the HTTP link: | ||
|
||
```js | ||
import { createPersistedQueryLink } from "apollo-link-persisted-queries"; | ||
import { createHttpLink } from "apollo-link-http"; | ||
import { InMemoryCache } from "apollo-cache-inmemory"; | ||
import ApolloClient from "apollo-client"; | ||
|
||
const link = createPersistedQueryLink().concat(createHttpLink({ uri: "/graphql" })); | ||
|
||
const client = new ApolloClient({ | ||
cache: new InMemoryCache(), | ||
link: link, | ||
}); | ||
``` | ||
|
||
> 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 commentThe 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. This link still is dead? |
||
|
||
<h2 id="verify">Verify</h2> | ||
|
||
Apollo Server's persisted queries configuration can be tested from the command-line. The following examples assume Apollo Server is running at `localhost:4000/`. | ||
This example persists a dummy query of `{__typename}`, using its sha256 hash: `ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38`. | ||
|
||
|
||
1. Request a persisted query: | ||
|
||
```bash | ||
curl -g 'http://localhost:4000/?extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}' | ||
``` | ||
|
||
Expect a response of: `{"errors": [{"message": "PersistedQueryNotFound", "extensions": {...}}]}`. | ||
|
||
2. Store the query to the cache: | ||
|
||
```bash | ||
curl -g 'http://localhost:4000/?query={__typename}&extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}' | ||
``` | ||
|
||
Expect a response of `{"data": {"__typename": "Query"}}"`. | ||
|
||
3. Request the persisted query again: | ||
|
||
```bash | ||
curl -g 'http://localhost:4000/?extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}' | ||
``` | ||
|
||
Expect a response of `{"data": {"__typename": "Query"}}"`, as the query string is loaded from the cache. | ||
|
||
<h2 id="get">Using GET requests with APQ to enable CDNs</h2> | ||
|
||
A great application for APQ is running Apollo Server behind a CDN. Many CDNs only cache GET requests, but many GraphQL queries are too long to fit comfortably in a cacheable GET request. When the APQ link is created with `createPersistedQueryLink({useGETForHashedQueries: true})`, Apollo Client automatically sends the short hashed queries as GET requests allowing a CDN to serve those request. For full-length queries and for all mutations, Apollo Client will continue to use POST requests. For more about this pattern, read about how [Apollo Server provides cache information to CDNs](). | ||
|
||
<h2 id="how-it-works">How it works</h2> | ||
|
||
The mechanism is based on a lightweight protocol extension between Apollo Client and Apollo Server. It works as follows: | ||
|
||
- When the client makes a query, it will optimistically send a short (64-byte) cryptographic hash instead of the full query text. | ||
- *Optimized Path:* If a request containing a persisted query hash is detected, Apollo Server will look it up to find a corresponding query in its registry. Upon finding a match, Apollo Server will expand the request with the full text of the query and execute it. | ||
- *New Query Path:* In the unlikely event that the query is not already in the Apollo Server registry (this only happens the very first time that Apollo Server sees a query), it will ask the client to resend the request using the full text of the query. At that point Apollo Server will store the query / hash mapping in the registry for all subsequent requests to benefit from. | ||
|
||
<div align="center"> | ||
<h3 id="Optimized-Path">**Optimized Path**</h3> | ||
</div> | ||
|
||
![Optimized path](../img/persistedQueries.optPath.png) | ||
|
||
<div align="center"> | ||
<h3 id="New-Query-Path">**New Query Path**</h3> | ||
</div> | ||
|
||
![New query path](../img/persistedQueries.newPath.png) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,8 +27,15 @@ import { | |
ExecutionParams, | ||
} from 'subscriptions-transport-ws'; | ||
|
||
//use as default persisted query store | ||
import Keyv = require('keyv'); | ||
import QuickLru = require('quick-lru'); | ||
|
||
import { formatApolloErrors } from './errors'; | ||
import { GraphQLServerOptions as GraphQLOptions } from './graphqlOptions'; | ||
import { | ||
GraphQLServerOptions as GraphQLOptions, | ||
PersistedQueryOptions, | ||
} from './graphqlOptions'; | ||
import { LogFunction } from './logging'; | ||
|
||
import { | ||
|
@@ -108,7 +115,22 @@ export class ApolloServerBase<Request = RequestInit> { | |
: noIntro; | ||
} | ||
|
||
this.requestOptions = requestOptions; | ||
if (requestOptions.persistedQueries !== false) { | ||
if (!requestOptions.persistedQueries) { | ||
//maxSize is the number of elements that can be stored inside of the cache | ||
//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 commentThe 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. oops, missed that. |
||
cache: new Keyv({ store: lru }), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keyv adds the ability to have the same Promise based interface There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice find with keyv. odd that they don't have a memcached. |
||
}; | ||
} | ||
} else { | ||
//the user does not want to use persisted queries, so we remove the field | ||
delete requestOptions.persistedQueries; | ||
} | ||
|
||
this.requestOptions = requestOptions as GraphQLOptions; | ||
this.context = context; | ||
|
||
if ( | ||
|
@@ -380,7 +402,9 @@ const typeDefs = gql\`${startSchema}\` | |
try { | ||
context = | ||
typeof this.context === 'function' | ||
? await this.context({ req: request }) | ||
? await this.context({ | ||
req: request, | ||
}) | ||
: context; | ||
} catch (error) { | ||
//Defer context error resolution to inside of runQuery | ||
|
@@ -397,6 +421,8 @@ const typeDefs = gql\`${startSchema}\` | |
// avoid a bad side effect of the otherwise useful noUnusedLocals option | ||
// (https://github.com/Microsoft/TypeScript/issues/21673). | ||
logFunction: this.requestOptions.logFunction as LogFunction, | ||
persistedQueries: this.requestOptions | ||
.persistedQueries as PersistedQueryOptions, | ||
fieldResolver: this.requestOptions.fieldResolver as GraphQLFieldResolver< | ||
any, | ||
any | ||
|
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
perhaps?