Skip to content

Commit

Permalink
Clean up code and add tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
srinivasankavitha committed Feb 21, 2025
1 parent 4b8c77a commit cafde75
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,28 @@ package com.netflix.graphql.dgs.apq
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.CaffeineSpec
import com.netflix.graphql.dgs.internal.QueryValueCustomizer
import com.netflix.graphql.dgs.springgraphql.autoconfig.DgsSpringGraphQLAutoConfiguration
import graphql.execution.preparsed.PreparsedDocumentEntry
import graphql.execution.preparsed.persisted.ApolloPersistedQuerySupport
import graphql.execution.preparsed.PreparsedDocumentProvider
import graphql.execution.preparsed.persisted.PersistedQueryCache
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.binder.cache.CaffeineCacheMetrics
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.AutoConfiguration
import org.springframework.boot.autoconfigure.AutoConfigureAfter
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.Duration
import java.util.*

@AutoConfiguration
@AutoConfigureAfter(DgsSpringGraphQLAutoConfiguration::class)
@ConditionalOnProperty(
prefix = DgsAPQSupportProperties.PREFIX,
name = ["enabled"],
Expand All @@ -45,19 +49,17 @@ import java.time.Duration
)
@EnableConfigurationProperties(DgsAPQSupportProperties::class)
open class DgsAPQSupportAutoConfiguration {
/* @Bean
@ConditionalOnBean(PersistedQueryCache::class)
open fun apolloPersistedQuerySupport(persistedQueryCache: PersistedQueryCache): ApolloPersistedQuerySupport =
DgsAPQPreParsedDocumentProvider(persistedQueryCache, null)*/

@Bean
@ConditionalOnBean(ApolloPersistedQuerySupport::class)
open fun apolloAPQQueryValueCustomizer(): QueryValueCustomizer =
QueryValueCustomizer { query ->
if (query.isNullOrBlank()) {
ApolloPersistedQuerySupport.PERSISTED_QUERY_MARKER
} else {
query
open fun apqSourceBuilderCustomizer(
preparsedDocumentProvider: Optional<PreparsedDocumentProvider>,
persistedQueryCache: Optional<PersistedQueryCache>,
): GraphQlSourceBuilderCustomizer =
GraphQlSourceBuilderCustomizer { builder ->
builder.configureGraphQl { graphQlBuilder ->
// For non-APQ queries, the user specified PreparsedDocumentProvider should be used, so we configure the DgsAPQPreparsedDocumentProvider to
// wrap the user specified one and delegate appropriately since we can only have one PreParsedDocumentProvider bean
val apqPreParsedDocumentProvider = DgsAPQPreParsedDocumentProvider(persistedQueryCache.get(), preparsedDocumentProvider)
graphQlBuilder.preparsedDocumentProvider(apqPreParsedDocumentProvider)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import com.netflix.graphql.dgs.DgsQueryExecutor
import com.netflix.graphql.dgs.DgsRuntimeWiring
import com.netflix.graphql.dgs.DgsTypeDefinitionRegistry
import com.netflix.graphql.dgs.ReloadSchemaIndicator
import com.netflix.graphql.dgs.apq.DgsAPQPreParsedDocumentProvider
import com.netflix.graphql.dgs.autoconfig.DgsConfigurationProperties
import com.netflix.graphql.dgs.autoconfig.DgsDataloaderConfigurationProperties
import com.netflix.graphql.dgs.autoconfig.DgsInputArgumentConfiguration
Expand Down Expand Up @@ -480,14 +479,9 @@ open class DgsSpringGraphQLAutoConfiguration(
GraphQlSourceBuilderCustomizer { builder ->
builder.configureGraphQl { graphQlBuilder ->
val apqEnabled = environment.getProperty("dgs.graphql.apq.enabled", Boolean::class.java, false)
if (apqEnabled) {
// Use the APQ PreparsedDocumentProvider if apq is enabled, wrapping the user provided preparsedDocumentProvider
val apqPreParsedDocumentProvider = DgsAPQPreParsedDocumentProvider(persistedQueryCache.get(), preparsedDocumentProvider)
graphQlBuilder.preparsedDocumentProvider(apqPreParsedDocumentProvider)
} else {
if (preparsedDocumentProvider.isPresent) {
graphQlBuilder.preparsedDocumentProvider(preparsedDocumentProvider.get())
}
// if apq is enabled use the APQPreparsedDocumentProvider instead
if (preparsedDocumentProvider.isPresent && !apqEnabled) {
graphQlBuilder.preparsedDocumentProvider(preparsedDocumentProvider.get())
}

if (providedQueryExecutionStrategy.isPresent) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright 2025 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.graphql.dgs.springgraphql.apq

import com.netflix.graphql.dgs.apq.DgsAPQPreParsedDocumentProvider
import graphql.ExecutionInput
import graphql.execution.preparsed.PreparsedDocumentEntry
import graphql.execution.preparsed.PreparsedDocumentProvider
import graphql.execution.preparsed.persisted.PersistedQueryCache
import graphql.execution.preparsed.persisted.PersistedQuerySupport.PERSISTED_QUERY_MARKER
import graphql.language.Document
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.junit5.MockKExtension
import io.mockk.mockk
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import java.util.*
import java.util.concurrent.CompletableFuture

@ExtendWith(MockKExtension::class)
class DgsAPQPreParsedDocumentProviderTest {
@Autowired
private lateinit var dgsAPQPreParsedDocumentProvider: DgsAPQPreParsedDocumentProvider

@MockK
lateinit var preparsedDocumentProvider: PreparsedDocumentProvider

@MockK
lateinit var persistedQueryCache: PersistedQueryCache

@BeforeEach
fun setUp() {
dgsAPQPreParsedDocumentProvider = DgsAPQPreParsedDocumentProvider(persistedQueryCache, Optional.of(preparsedDocumentProvider))
}

@Test
fun `APQ only queries with just the hash use the persisted query cache`() {
var count = 0
val document = mockk<Document>()
val computeFunction = { _: ExecutionInput ->
count++
PreparsedDocumentEntry(document)
}
val extensions: MutableMap<String, Any> = HashMap()
extensions["persistedQuery"] =
mapOf("version" to "1", "sha256Hash" to "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38")
val executionInput =
ExecutionInput
.Builder()
.query(PERSISTED_QUERY_MARKER)
.extensions(extensions)
.build()

every {
persistedQueryCache.getPersistedQueryDocumentAsync(any(), any(), any())
}.returns(CompletableFuture.completedFuture(computeFunction(executionInput)))

dgsAPQPreParsedDocumentProvider.getDocumentAsync(executionInput, computeFunction)
assertThat(count == 1)
}

@Test
fun `APQ queries with query and hash use the persisted query cache`() {
var count = 0
val document = mockk<Document>()
val computeFunction = { _: ExecutionInput ->
count++
PreparsedDocumentEntry(document)
}
val extensions: MutableMap<String, Any> = HashMap()
extensions["persistedQuery"] =
mapOf("version" to "1", "sha256Hash" to "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38")
val executionInput =
ExecutionInput
.Builder()
.query("{__typename}")
.extensions(extensions)
.build()

every {
persistedQueryCache.getPersistedQueryDocumentAsync(any(), any(), any())
}.returns(CompletableFuture.completedFuture(computeFunction(executionInput)))

dgsAPQPreParsedDocumentProvider.getDocumentAsync(executionInput, computeFunction)
assertThat(count == 1)
}

@Test
fun `Plain queries (non-APQ) use the user specified preparsed document provider`() {
var count = 0
val document = mockk<Document>()
val computeFunction = { _: ExecutionInput ->
count++
PreparsedDocumentEntry(document)
}
val extensions: MutableMap<String, Any> = HashMap()
extensions["persistedQuery"] =
mapOf("version" to "1", "sha256Hash" to "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38")
val executionInput = ExecutionInput.Builder().query("{__typename}").build()

every {
preparsedDocumentProvider.getDocumentAsync(executionInput, any())
}.returns(CompletableFuture.completedFuture(computeFunction(executionInput)))

dgsAPQPreParsedDocumentProvider.getDocumentAsync(executionInput, computeFunction)
assertThat(count == 1)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2024 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.graphql.dgs.springgraphql.autoconfig

import com.netflix.graphql.dgs.apq.DgsAPQSupportAutoConfiguration
import graphql.execution.preparsed.persisted.PersistedQueryCache
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.boot.autoconfigure.AutoConfigurations
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration
import org.springframework.boot.test.context.runner.ApplicationContextRunner

class DgsAPQAutoConfigurationTest {
private val autoConfigurations =
AutoConfigurations.of(
DgsSpringGraphQLAutoConfiguration::class.java,
DgsAPQSupportAutoConfiguration::class.java,
GraphQlAutoConfiguration::class.java,
)

@Test
fun shouldContributeAPQBeans() {
val contextRunner =
ApplicationContextRunner()
.withConfiguration(autoConfigurations)
.withPropertyValues("dgs.graphql.apq.enabled=true")

contextRunner.run { context ->
assertThat(context)
.hasBean("apqSourceBuilderCustomizer")
.hasBean("sourceBuilderCustomizer")
.hasSingleBean(PersistedQueryCache::class.java)
}
}

@Test
fun shouldNotContributeAPQBeans() {
val contextRunner =
ApplicationContextRunner()
.withConfiguration(autoConfigurations)

contextRunner.run { context ->
assertThat(context)
.doesNotHaveBean("apqSourceBuilderCustomizer")
.hasBean("sourceBuilderCustomizer")
.doesNotHaveBean(PersistedQueryCache::class.java)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -211,18 +211,24 @@ class DgsWebMVCAutomatedPersistedQueriesSmokeTest {

@Configuration
open class PreparsedDocumentProviderConfig {
private val cache: Cache<String, PreparsedDocumentEntry> = Caffeine.newBuilder().maximumSize(250)
.expireAfterAccess(5, TimeUnit.MINUTES).recordStats().build()

private val cache: Cache<String, PreparsedDocumentEntry> =
Caffeine
.newBuilder()
.maximumSize(250)
.expireAfterAccess(5, TimeUnit.MINUTES)
.recordStats()
.build()

@Bean
open fun preparsedDocumentProvider(): PreparsedDocumentProvider {
return PreparsedDocumentProvider { executionInput: ExecutionInput, parseAndValidateFunction: Function<ExecutionInput?, PreparsedDocumentEntry?> ->
open fun preparsedDocumentProvider(): PreparsedDocumentProvider =
PreparsedDocumentProvider {
executionInput: ExecutionInput,
parseAndValidateFunction: Function<ExecutionInput?, PreparsedDocumentEntry?>,
->
val mapCompute =
Function { key: String? -> parseAndValidateFunction.apply(executionInput) }
CompletableFuture.completedFuture(cache[executionInput.query, mapCompute])
}
}
}
}
}
Expand Down

0 comments on commit cafde75

Please sign in to comment.