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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"operationName":"AllPlanets","variables":{},"query":"query AllPlanets { allPlanets(first: 300) { __typename planets { __typename ...PlanetFragment filmConnection { __typename totalCount films { __typename title ...FilmFragment } } } } } fragment PlanetFragment on Planet { __typename name climates surfaceWater } fragment FilmFragment on Film { __typename title producers }"},{"operationName":"EpisodeHeroName","variables":{"episode":"EMPIRE"},"query":"query EpisodeHeroName($episode: Episode) { hero(episode: $episode) { __typename name } }"}]
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.apollographql.apollo.internal.batch

import com.apollographql.apollo.Utils
import com.apollographql.apollo.api.ScalarTypeAdapters
import com.apollographql.apollo.exception.ApolloException
import com.apollographql.apollo.integration.httpcache.AllPlanetsQuery
import com.apollographql.apollo.integration.normalizer.EpisodeHeroNameQuery
import com.apollographql.apollo.integration.normalizer.type.Episode
import com.apollographql.apollo.interceptor.ApolloInterceptor
import com.apollographql.apollo.internal.interceptor.ApolloServerInterceptor
import com.apollographql.apollo.request.RequestHeaders
import com.google.common.base.Predicate
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert
import okhttp3.Call
import okhttp3.Callback
import okhttp3.HttpUrl
import okhttp3.Request
import okhttp3.Response
import okio.Buffer
import okio.Timeout
import org.junit.Test

class BatchHttpRequestTest {

@Test
fun testCombiningOperationsInRequestBody() {
val planetsQuery = AllPlanetsQuery()
val episodeQuery = EpisodeHeroNameQuery.builder().episode(Episode.EMPIRE).build()
val customHeaderName = "appHeader"
val customHeaderValue = "appHeaderValue"
val queryList = listOf(
QueryToBatch(
ApolloInterceptor.InterceptorRequest.builder(planetsQuery)
.requestHeaders(RequestHeaders.builder().addHeader(customHeaderName, customHeaderValue).build())
.build(),
NoOpCallback()
),
QueryToBatch(ApolloInterceptor.InterceptorRequest.builder(episodeQuery).build(), NoOpCallback())
)
val predicate = Predicate<Request> { request ->
assertThat(request).isNotNull()
assertThat(request!!.header(ApolloServerInterceptor.HEADER_ACCEPT_TYPE)).isEqualTo(ApolloServerInterceptor.ACCEPT_TYPE)
assertThat(request.header(customHeaderName)).isEqualTo(customHeaderValue)
assertRequestBody(request)
true
}
BatchHttpCallImpl(
queryList,
HttpUrl.get("https://google.com"),
AssertHttpCallFactory(predicate),
ScalarTypeAdapters(emptyMap())
).execute()
}

private fun assertRequestBody(request: Request?) {
assertThat(request!!.body()!!.contentType()).isEqualTo(ApolloServerInterceptor.MEDIA_TYPE)
val bodyBuffer = Buffer()
try {
request.body()!!.writeTo(bodyBuffer)
} catch (e: Exception) {
throw RuntimeException(e)
}
Utils.checkTestFixture(bodyBuffer.readUtf8(), "BatchHttpRequestTest/expectedBatchRequestBody.json")
}

private class AssertHttpCallFactory constructor(val predicate: Predicate<Request>) : Call.Factory {
override fun newCall(request: Request): Call {
if (!predicate.apply(request)) {
Assert.fail("Assertion failed")
}
return NoOpCall()
}
}

private class NoOpCall : Call {
override fun request(): Request {
throw UnsupportedOperationException()
}

override fun execute(): Response {
throw UnsupportedOperationException()
}

override fun enqueue(responseCallback: Callback) {}
override fun cancel() {}
override fun isExecuted(): Boolean {
return false
}

override fun isCanceled(): Boolean {
return false
}

override fun clone(): Call {
throw UnsupportedOperationException()
}

override fun timeout(): Timeout {
throw UnsupportedOperationException()
}
}

private class NoOpCallback: ApolloInterceptor.CallBack {
override fun onResponse(response: ApolloInterceptor.InterceptorResponse) {
}

override fun onFetch(sourceType: ApolloInterceptor.FetchSourceType?) {
}

override fun onFailure(e: ApolloException) {
}

override fun onCompleted() {
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package com.apollographql.apollo.internal.batch

import com.apollographql.apollo.ApolloCall
import com.apollographql.apollo.ApolloClient
import com.apollographql.apollo.Utils.immediateExecutor
import com.apollographql.apollo.Utils.immediateExecutorService
import com.apollographql.apollo.Utils.mockResponse
import com.apollographql.apollo.api.Response
import com.apollographql.apollo.exception.ApolloException
import com.apollographql.apollo.integration.httpcache.AllPlanetsQuery
import com.apollographql.apollo.integration.normalizer.EpisodeHeroNameQuery
import com.apollographql.apollo.integration.normalizer.type.Episode
import com.google.common.truth.Truth.assertThat
import okhttp3.Dispatcher
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.net.HttpURLConnection
import java.util.concurrent.TimeUnit

class BatchHttpResponseTest {

lateinit var apolloClient: ApolloClient
lateinit var server: MockWebServer

@Before
fun setUp() {
server = MockWebServer()
val okHttpClient = OkHttpClient.Builder()
.writeTimeout(2, TimeUnit.SECONDS)
.readTimeout(2, TimeUnit.SECONDS)
.dispatcher(Dispatcher(immediateExecutorService()))
.build()
apolloClient = ApolloClient.builder()
.serverUrl(server.url("/"))
.okHttpClient(okHttpClient)
.dispatcher(immediateExecutor())
.batchingConfiguration(BatchConfig(batchingEnabled = true, batchIntervalMs = 2000, maxBatchSize = 2))
.build()
apolloClient.startBatchPoller()
}

@After
fun tearDown() {
apolloClient.stopBatchPoller()
server.shutdown()
}

@Test
fun testBatchingDisabled() {
val planetsQuery = AllPlanetsQuery()
val episodeQuery = EpisodeHeroNameQuery.builder().episode(Episode.EMPIRE).build()
val planetsCallback = PlanetsCallback()
val episodeCallback = EpisodeCallback()

server.enqueue(mockResponse("AllPlanetsNullableField.json"))
apolloClient.query(planetsQuery).toBuilder().canBeBatched(false).build().enqueue(planetsCallback)
assertThat(planetsCallback.exceptions).isEmpty()
assertThat(planetsCallback.responseList[0].data?.allPlanets()?.planets()?.size).isEqualTo(60)

server.enqueue(mockResponse("EpisodeHeroNameResponse.json"))
apolloClient.query(episodeQuery).toBuilder().canBeBatched(false).build().enqueue(episodeCallback)
assertThat(episodeCallback.exceptions).isEmpty()
assertThat(episodeCallback.responseList[0].data?.hero()?.name()).isEqualTo("R2-D2")
}

@Test
fun testMultipleQueryBatchingSuccess() {
val planetsQuery = AllPlanetsQuery()
val episodeQuery = EpisodeHeroNameQuery.builder().episode(Episode.EMPIRE).build()
val planetsCallback = PlanetsCallback()
val episodeCallback = EpisodeCallback()

server.enqueue(mockResponse("BatchQueryResponse.json"))
apolloClient.query(planetsQuery).toBuilder().canBeBatched(true).build().enqueue(planetsCallback)
apolloClient.query(episodeQuery).toBuilder().canBeBatched(true).build().enqueue(episodeCallback)
assertThat(planetsCallback.exceptions).isEmpty()
assertThat(episodeCallback.exceptions).isEmpty()
assertThat(planetsCallback.responseList[0].data?.allPlanets()?.planets()?.size).isEqualTo(60)
assertThat(episodeCallback.responseList[0].data?.hero()?.name()).isEqualTo("R2-D2")
}

@Test
fun testMultipleQueryBatchingError() {
val planetsQuery = AllPlanetsQuery()
val episodeQuery = EpisodeHeroNameQuery.builder().episode(Episode.EMPIRE).build()
val planetsCallback = PlanetsCallback()
val episodeCallback = EpisodeCallback()

server.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_INTERNAL_ERROR).setBody("Server Error"))
apolloClient.query(planetsQuery).toBuilder().canBeBatched(true).build().enqueue(planetsCallback)
apolloClient.query(episodeQuery).toBuilder().canBeBatched(true).build().enqueue(episodeCallback)
assertThat(planetsCallback.exceptions.size).isEqualTo(1)
assertThat(episodeCallback.exceptions.size).isEqualTo(1)
}

class PlanetsCallback : ApolloCall.Callback<AllPlanetsQuery.Data>() {
val responseList: MutableList<Response<AllPlanetsQuery.Data>> = ArrayList()
val exceptions: MutableList<Exception> = ArrayList()

@Volatile
var completed = false
override fun onResponse(response: Response<AllPlanetsQuery.Data>) {
check(!completed) { "onCompleted already called Do not reuse callback." }
responseList.add(response)
}

override fun onFailure(e: ApolloException) {
check(!completed) { "onCompleted already called Do not reuse callback." }
exceptions.add(e)
}

override fun onStatusEvent(event: ApolloCall.StatusEvent) {
if (event == ApolloCall.StatusEvent.COMPLETED) {
check(!completed) { "onCompleted already called Do not reuse callback." }
completed = true
}
}
}

class EpisodeCallback : ApolloCall.Callback<EpisodeHeroNameQuery.Data>() {
val responseList: MutableList<Response<EpisodeHeroNameQuery.Data>> = ArrayList()
val exceptions: MutableList<Exception> = ArrayList()

@Volatile
var completed = false
override fun onResponse(response: Response<EpisodeHeroNameQuery.Data>) {
check(!completed) { "onCompleted already called Do not reuse callback." }
responseList.add(response)
}

override fun onFailure(e: ApolloException) {
check(!completed) { "onCompleted already called Do not reuse callback." }
exceptions.add(e)
}

override fun onStatusEvent(event: ApolloCall.StatusEvent) {
if (event == ApolloCall.StatusEvent.COMPLETED) {
check(!completed) { "onCompleted already called Do not reuse callback." }
completed = true
}
}
}
}
Loading