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

Implement Query Batching for apollo-runtime #3117

Merged
merged 10 commits into from
May 27, 2021

Conversation

joaquim-verges
Copy link
Contributor

Background

Hello Apollo team!

We, the Twitch Android team, have been using Apollo Android for 3+ years now, and have been very happy with it!

However, we recently ran into scalability issues in our app. Our most busy screen has exploded in features, where each feature is encapsulated and does its own GQL query. We've reached a point where that screen does 45 GQL queries (and therefore 45 HTTP calls) when it comes on screen 🤯 This is not only costly for the client, but also very costly for our backend services.

We explored grouping queries into larger ones, but we like the modularity and encapsulation that we have right now where each feature can do their fetching independently. We also know that our web team (who also uses Apollo) solved this problem by enabling batching for queries in the web Apollo client (and that our server supports it).

We decided to fork the Android library to implement the ability to batch multiple queries in a single HTTP call, similar to what the web client offers. We're about to test our implementation in production, and should will gather data in the next few weeks.

In the meantime, we thought it would be useful to push our implementation upstream and give back to the Apollo community!

Implementation

We followed the approach that the web Apollo client took, where batching is achieved by a "poller" that checks periodically for queries to be batched, packages them into a single request body and sends them to the server. On response, the response body is parsed to extract each sub-response and forwarded back to the corresponding callers.

The implementation below is the minimal one that we needed to get that approach working, and we're looking for early feedback before writing unit tests, samples and later port it to the apollo-runtime-kotlin artifact.

Let us know what you think!

requestBuilder.header(header, value)
}

// TODO add http cache headers (does it make sense for batch HTTP calls?)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

similarly, adding the HTTP cache headers felt weird since a batch can potentially be different between 2 runs, as the queries that will be batched is timing dependent. I guess one could get lucky and get the exact same batch (maybe for single query screens), in which case HTTP caching could come into play. Let us know what you think.

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed, it doesn't make a lot of sense to have HTTP cache + query batching. Ideally we could detect that HTTP + batching is used and throw an error. Maybe based on httpCachePolicy ?

Copy link
Contributor

@martinbonnin martinbonnin May 20, 2021

Choose a reason for hiding this comment

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

And I'd say we need same kind of thing with AutoPersistedQueries too.

edit: and File Upload too...😅. Well for these last two we can rely on users not trying to activate them at the same time as batching. I think that's a fair expectation to have.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we actually use APQ with batching and that works really well :)

Yeah I'll do the same for file uploads, throw an exception if used with batching with a clear message

Copy link
Contributor

Choose a reason for hiding this comment

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

we actually use APQ with batching and that works really well :)

Nice. Good to know 👍

Yeah I'll do the same for file uploads, throw an exception if used with batching with a clear message

File Uploads are a bit weird and work with reflexion at the moment. It might be a bit awkward to detect when it's used. If it becomes too convoluted, I think it's fine to leave that as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

actually, since we're only enabling batching for queries and not mutations, is file upload even possible with queries? (never used it so not sure)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

and looking at http caching, since you can make non-batched queries with the same ApolloClient, would be annoying to have to turn it off entirely if you still want your single queries to take advantage of it.

I'm thinking of just not adding the http cache headers for batch calls, and just silently ignore it. Not sure if I should log something to let the caller know?

Copy link
Contributor

Choose a reason for hiding this comment

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

is file upload even possible with queries?

In theory I think yes.. I don't see anything preventing someone to pass an Upload custom scalar as a field argument. For an example a service that could sign your files using a server-side private key:

query GetSignature($file: Upload) {
  signature(file: $file)
}

In practice, I've never seen anyone do that yet though...

we're only enabling batching for queries and not mutations

Would it make sense to enable batching for mutations? I don't think a theoretical reason to limit to queries?

looking at http caching, since you can make non-batched queries with the same ApolloClient, would be annoying to have to turn it off entirely if you still want your single queries to take advantage of it.

Right , makes sense 👍

I'm thinking of just not adding the http cache headers for batch calls, and just silently ignore it. Not sure if I should log something to let the caller know?

Could you do something per-query?

    if (batchPoller != null) {
      if (httpCachePolicy != null) {
        throw new IllegalArgumentException("Using httpCachePolicy and batching at the same time does not make sense");
      }
      interceptors.add(new ApolloBatchingInterceptor(batchPoller));

@martinbonnin
Copy link
Contributor

martinbonnin commented May 20, 2021

Awesome contribution 😃 ! That'll make a lot of users happy and close the oldest open issue in this repo 🎉 .

A few comments but looks good overall 👍

PS: I just pushed the metalava signatures used to enforce API compatibility. You can regenerate them with

# run with Java8
./gradlew assemble metalavaGenerateSignature --console=plain

@joaquim-verges
Copy link
Contributor Author

Adding some unit tests next.

@joaquim-verges
Copy link
Contributor Author

@martinbonnin feeling pretty good about the latest implementation and tests, let me know what you think!

@martinbonnin
Copy link
Contributor

Thanks for adding more tests! A couple of remaining comments:

  • The testMultipleQueryBatchingError failed, I guess on timing issues. You can use a RxJava test subscriber (like here) or more recent tests use coroutines (like here) for synchronization.
  • API: What about ApolloQueryCall.canBeBatched(boolean canBeBatched) instead of ApolloClient.batchQuery. This would be more consistent with the other per-query parameters like cacheHeaders, responseFetcher, etc..
  • We did not settle on batching mutations. Just curious if you had any thoughts on that. I think it's ok to not include it in the first version as it can always be added later.

@joaquim-verges
Copy link
Contributor Author

joaquim-verges commented May 25, 2021

The testMultipleQueryBatchingError failed, I guess on timing issues. You can use a RxJava test subscriber (like here) or more recent tests use coroutines (like here) for synchronization.

Yeah I hesitated between controlling the time or testing the automatic trigger, I'll switch it to a more controlled test

API: What about ApolloQueryCall.canBeBatched(boolean canBeBatched) instead of ApolloClient.batchQuery. This would be more consistent with the other per-query parameters like cacheHeaders, responseFetcher, etc..

Great suggestion, should have thought of it earlier!

We did not settle on batching mutations. Just curious if you had any thoughts on that. I think it's ok to not include it in the first version as it can always be added later.

Still torn on that, it should work fine I think but since mutations are usually slower and more isolated one-offs in general, it feels a bit weird to mix them with queries? But yeah as you mentioned, can easily be enabled later if needed.

@joaquim-verges
Copy link
Contributor Author

@martinbonnin should I add canBeBatched only to ApolloQueryCall.Builder ? or also add one ApolloQueryCall directly ? I see a bunch of them are deprecated in favor of the builder one?

@martinbonnin
Copy link
Contributor

@martinbonnin should I add canBeBatched only to ApolloQueryCall.Builder ? or also add one ApolloQueryCall directly ? I see a bunch of them are deprecated in favor of the builder one?

Yup, you're right 👍 . ApolloQueryCall.Builder is the way to go now because of this. It's more verbose than I'd ideally like to and 3.0 will change that but for consistency let's put everything in ApolloQueryCall.Builder in main.

@joaquim-verges
Copy link
Contributor Author

running metalava locally is changing way more signatures that it should for some reasons, mind running it for me?

@martinbonnin
Copy link
Contributor

running metalava locally is changing way more signatures that it should for some reasons, mind running it for me?

Done. Not sure why but metalava signatures depend on the local JVM. I have to use JDK8 to get signatures matching the CI.

Copy link
Contributor

@martinbonnin martinbonnin left a comment

Choose a reason for hiding this comment

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

LGTM!

@martinbonnin martinbonnin merged commit f73b210 into apollographql:main May 27, 2021
@martinbonnin
Copy link
Contributor

Thanks @joaquim-verges for the awesome contribution!

@joaquim-verges
Copy link
Contributor Author

Glad I could help!

martinbonnin added a commit that referenced this pull request Nov 22, 2021
* Add a "Who is Apollo" section to the main README (#3073)

* Slight tweaks to the "Who is Apollo?" README section (#3079)

To align with apollographql/apollo-client#8079.

* Update dependency gatsby-theme-apollo-docs to v4.7.3

* Issue #3095: Expose channel capacity parameter. (#3096)

Co-authored-by: Neal Sanche <[email protected]>

* Update dependency gatsby to v2.32.13

* Update dependency gatsby-theme-apollo-docs to v4.7.4

* Fix Float input fields Java codegen with default values in json schemas (#3109)

* Fix Float input fields Java codegen with default values in json schemas

see #3108

* add a test case for lists in defaultValues and handle it

Co-authored-by: Martin Bonnin <[email protected]>

* bump Kotlin and Coroutines to 1.5.0 (#3113)

* bump Kotlin and Coroutines to 1.5.0

* make the KotlinCompile task depend on antlr generation

* update metalava-plugin

* update metalava

* update metalava using Jdk8.

Not really sure why the signatures would differ based on the java
version...

* release 2.5.7

* version is now 2.5.8-SNAPSHOT

* Update README.md (#3120)

* Replace spectrum with discourse (#3123)

* Discourse: pre-fill the category and tags

* Update multi-modules.mdx (#3125)

* Implement Query Batching for `apollo-runtime` (#3117)

* Implement Query Batching for `apollo-runtime`

* update metalava signatures

* Trigger HTTP call on max batch size, added synchronization, throw when using GET method

* Unit tests for BatchPoller

* Fix checkstyle

* Propagate parsing errors in callbacks, better syncrhonization, use LinkedList

* Added some tests for batch response parsing

* Added tests for request body + headers

* Builder API to enable per query batching, dont rely on time for tests

* update Metalava signatures

Co-authored-by: Martin Bonnin <[email protected]>

* pass 'operationName' in the introspection query (#3126)

* bump junit (#3131)

* Update dependency gatsby-theme-apollo-docs to v4.7.6

* feat: add Reactor support (#3138)

* feat: add Reactor support

resolves #627

* remove .gitignore that is handled by the top level one

* update metalava signatures

Co-authored-by: Martin Bonnin <[email protected]>

* Update dependency gatsby-theme-apollo-docs to v4.7.9

* add reactor in the left column of the doc (#3141)

* release 2.5.8

* version is now 2.5.9-SNAPSHOT

* Fix overriding `canBeBatched` value when cloning `QueryCall` with `toBuilder()` (#3146)

* Update get-started-multiplatform.mdx (#3150)

update dep version forcing as per gradle new api

* Update badge from Bintray to MavenCentral for apollo-idling-resource (#3153)

Thanks!

* Update 09-write-your-first-mutation.mdx (#3154)

* Update 01-configure-project.mdx

* Update 02-add-the-graphql-schema.mdx

* release 2.5.9

* version is now 2.5.10-SNAPSHOT

* refactor: RxJava 2/3 support (#3166)

* Update dependency striptags to 3.2.0 [SECURITY] (#3178)

Co-authored-by: Renovate Bot <[email protected]>

* Update dependency gatsby-theme-apollo-docs to v4.7.11

* Update dependency gatsby-theme-apollo-docs to v4.7.12

* remove duplicate executionContext (#3213)

* feat: add Mutiny support (#3198)

* feat: add Mutiny support

* Update gatsby-config.js

Co-authored-by: Martin Bonnin <[email protected]>

* add v3 docs (#3223)

* Add 3.0 alpha notice (#3231)

* Update dependency gatsby-theme-apollo-docs to v4.7.13

* July 2021 ROADMAP update (#3255)

* Update ROADMAP.md

* Update dependency gatsby-theme-apollo-docs to v4.7.14

* Update multi-modules.mdx

Add a not about multi platform, See #3297

* Update multi-modules.mdx

Remove deleted branch

* Replace Spectrum with community.apollographql.com (#3299)

* Update ROADMAP.md

* Update dependency path-parse to 1.0.7 [SECURITY] (#3296)

Co-authored-by: Renovate Bot <[email protected]>

* Update dependency gatsby-theme-apollo-docs to v4.7.15

* Remove root redirect and build deploy previews at / (#3309)

* Deploy the prod site at the root (#3312)

* Update dependency gatsby-theme-apollo-docs to v4.7.16

* Remove path prefixes for docs redirects (#3329)

* Updated coroutines for use native-mt version (#3330)

* Updated coroutines for use native-mt version

* Update get-started-multiplatform.mdx

Co-authored-by: Martin Bonnin <[email protected]>

* Update dependency gatsby-theme-apollo-docs to v4.7.18

* Increase Json nesting to 256 (#3334)

Closes #3308

* Bump docs theme to add Algolia search (#3346)

* Configure for updated docs theme

* Upgrade docs theme

* Bump docs theme version

* Bump docs theme version

* Update docs theme version

* Move config from docs netlify.toml to root level netlify.toml

* Upgrade docs theme

* Upgrade docs theme

* Upgrade docs theme

* Upgrade docs theme

* Bump docs theme to v5

Co-authored-by: Trevor Blades <[email protected]>

* Update dependency gatsby-theme-apollo-docs to v5.0.1

* Add Reactor && Mutiny to advanced topics (#3355)

* feat: add Mutiny support

* Update gatsby-config.js

* doc: Add Reactor && Mutiny to advanced topics

Co-authored-by: Martin Bonnin <[email protected]>

* Update dependency gatsby-theme-apollo-docs to v5.0.2

* Update dependency gatsby-theme-apollo-docs to v5.0.3

* Update dependency gatsby-theme-apollo-docs to v5.0.7

* Who is Apollo README updates (#3379)

Small updates to align "Who is Apollo" sections across repos.

* Update dependency gatsby-theme-apollo-docs to v5.0.8

* Give a hint at errorPayload (#3381)

See #3369

* docs: Configure custom Algolia filters (#3378)

* Configure algolia filters

* Upgrade theme

* Upgrade theme

* Adding Benoit (#3388)

* Workaround wrong input field default values (#3398)

* add a test case for #3394

* do not generate defaultValues for input fields of input object type

* make an exception for emptyList for backward compatibility

* do not crash when trying to indent the GraphQL document (#3399)

* Minor improvements to the Tutorial documentation (#3396)

* Minor improvements to the Tutorial documentation

* Rephrase a sentence

Co-authored-by: Martin Bonnin <[email protected]>

* Remove unneeded dependency

* Use 2.5.9 to be consistent with the code on tutorial's repo

Co-authored-by: Martin Bonnin <[email protected]>

* Add ./gradlew push${MainService}ApolloOperations (#3403)

* add RegisterOperations

* add ApolloRegisterOperationsTask

* add a test case

* fix bad copy/paste

* update metalava signatures

* add some doc

* Update docs/source/advanced/operation-safelisting.mdx

Co-authored-by: Benoit Lubek <[email protected]>

* Update docs/source/advanced/operation-safelisting.mdx

Co-authored-by: Benoit Lubek <[email protected]>

* add the link to the operation registry server documentation

Co-authored-by: Benoit Lubek <[email protected]>

* Replace 'data graph' with 'graph' (#3389)

* Operation Registry: normalize queries before computing the hash (#3406)

* add normalization

* be robust to Number.toString() in JS being different from
Double.toString() in Java

* remove extra "only"

* workaround a doc issue where the 3.x was used instead of 2.x

* release 2.5.10

* version is now 2.5.11-SNAPSHOT

* Indicate the supported native platforms in the doc (2.x) (#3419)

* Indicate the supported native platforms in the doc.

* Clarification about the `js` target also being available

* Added toString method for Input (#3427)

* Fix outdated doc about FileUpload (#3429)

* Update ROADMAP.md (#3445)

* update ROADMAP.md

* update ROADMAP.md

* Update dependency gatsby-theme-apollo-docs to v5.3.1

* Make safelistingHash closer to its apollo-tooling counterpart (#3471)

* take #2 on query normalization

* added a comment

* add comment

* Update dependency gatsby-theme-apollo-docs to v5.3.2

* Update dependency gatsby-theme-apollo-docs to v5.3.3

* Update dependency url-parse to 1.5.2 [SECURITY] (#3522)

Co-authored-by: Renovate Bot <[email protected]>

* Update dependency ws to 7.4.6 [SECURITY] (#3523)

Co-authored-by: Renovate Bot <[email protected]>

* Update dependency ssri to 7.1.1 [SECURITY] (#3521)

Co-authored-by: Renovate Bot <[email protected]>

* Update dependency postcss to 7.0.36 [SECURITY] (#3519)

Co-authored-by: Renovate Bot <[email protected]>

* Update dependency object-path to 0.11.8 [SECURITY] (#3518)

Co-authored-by: Renovate Bot <[email protected]>

* Update dependency dns-packet to 1.3.2 [SECURITY] (#3517)

Co-authored-by: Renovate Bot <[email protected]>

* Update dependency browserslist to 4.16.5 [SECURITY] (#3516)

Co-authored-by: Renovate Bot <[email protected]>

* Update dependency axios to 0.21.2 [SECURITY] (#3515)

Co-authored-by: Renovate Bot <[email protected]>

* Bump docs theme and align React version w/ other docsets (#3539)

* Always make sure the Response is closed (#3541)

* release 2.5.11

* version is now 2.5.12-SNAPSHOT

* Gateway clarification based on license change

* Update dependency gatsby-theme-apollo-docs to v5.3.8

* update docs dependencies

* pass 'operationName' in the introspection query (#3126)

* update ROADMAP

* add multiplatform in the multimodules doc

* Remove root redirect and build deploy previews at / (#3309)

* Deploy the prod site at the root (#3312)

* Remove path prefixes for docs redirects (#3329)

* add a Kotlin Native Doc

inspired by commit 2e2f5d9

* Bump docs theme to add Algolia search (#3346)

* Configure for updated docs theme

* Upgrade docs theme

* Bump docs theme version

* Bump docs theme version

* Update docs theme version

* Move config from docs netlify.toml to root level netlify.toml

* Upgrade docs theme

* Upgrade docs theme

* Upgrade docs theme

* Upgrade docs theme

* Bump docs theme to v5

Co-authored-by: Trevor Blades <[email protected]>

* update algolia filters

* Adding Benoit (#3388)

* replace data graph with 'graph'

* premliminary work for registerOperations

* wip

* wip

* fix sorting

* add Gradle tasks for registerOperations

* add safelisting doc

* update apiDump

* revert debug and fix tests that require mpp-utils

Co-authored-by: Danielle Man <[email protected]>
Co-authored-by: Hugh Willson <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-authored-by: Neal Sanche <[email protected]>
Co-authored-by: Neal Sanche <[email protected]>
Co-authored-by: Joaquim Verges <[email protected]>
Co-authored-by: Moncef AOUDIA <[email protected]>
Co-authored-by: Fabio Santo <[email protected]>
Co-authored-by: Nicola Corti <[email protected]>
Co-authored-by: Robert Theis <[email protected]>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Stephen Barlow <[email protected]>
Co-authored-by: Trevor Blades <[email protected]>
Co-authored-by: Виталий <[email protected]>
Co-authored-by: Janessa Garrow <[email protected]>
Co-authored-by: Benoit Lubek <[email protected]>
Co-authored-by: Andrew Orobator <[email protected]>
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.

2 participants