Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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.conditions;

import org.springframework.context.annotation.Conditional;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
* Conditional that matches when it is established that the DGS Framework should consider reloading components at runtime.
* Consider that reloading components should only be enabled in <i>local</i> and/or <i>development</i> since
* call latencies will increase significantly while _reloading_ of the components is happening.
*/

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnDgsReloadCondition.class)
public @interface ConditionalOnDgsReload {
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
requires static spring.webflux;
requires kotlin.stdlib;
requires org.slf4j;
requires spring.core;


exports com.netflix.graphql.dgs.autoconfig to kotlin.reflect, spring.beans, spring.core;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.netflix.graphql.dgs.DgsComponent
import com.netflix.graphql.dgs.DgsDataLoaderCustomizer
import com.netflix.graphql.dgs.DgsDataLoaderInstrumentation
import com.netflix.graphql.dgs.DgsDataLoaderOptionsProvider
import com.netflix.graphql.dgs.DgsDataLoaderReloadController
import com.netflix.graphql.dgs.DgsDefaultPreparsedDocumentProvider
import com.netflix.graphql.dgs.DgsExecutionResult
import com.netflix.graphql.dgs.DgsFederationResolver
Expand All @@ -38,6 +39,8 @@ import com.netflix.graphql.dgs.context.GraphQLContextContributorInstrumentation
import com.netflix.graphql.dgs.exceptions.DefaultDataFetcherExceptionHandler
import com.netflix.graphql.dgs.internal.DataFetcherResultProcessor
import com.netflix.graphql.dgs.internal.DefaultDataLoaderOptionsProvider
import com.netflix.graphql.dgs.internal.DefaultDgsDataLoaderProvider
import com.netflix.graphql.dgs.internal.DefaultDgsDataLoaderReloadController
import com.netflix.graphql.dgs.internal.DefaultDgsGraphQLContextBuilder
import com.netflix.graphql.dgs.internal.DgsDataLoaderInstrumentationDataLoaderCustomizer
import com.netflix.graphql.dgs.internal.DgsDataLoaderProvider
Expand All @@ -50,6 +53,7 @@ import com.netflix.graphql.dgs.internal.FluxDataFetcherResultProcessor
import com.netflix.graphql.dgs.internal.GraphQLJavaErrorInstrumentation
import com.netflix.graphql.dgs.internal.MonoDataFetcherResultProcessor
import com.netflix.graphql.dgs.internal.QueryValueCustomizer
import com.netflix.graphql.dgs.internal.ReloadableDgsDataLoaderProvider
import com.netflix.graphql.dgs.internal.method.ArgumentResolver
import com.netflix.graphql.dgs.internal.method.MethodDataFetcherFactory
import com.netflix.graphql.dgs.mvc.internal.method.HandlerMethodArgumentResolverAdapter
Expand All @@ -61,6 +65,8 @@ import com.netflix.graphql.dgs.springgraphql.DgsGraphQLSourceBuilder
import com.netflix.graphql.dgs.springgraphql.ReloadableGraphQLSource
import com.netflix.graphql.dgs.springgraphql.SpringGraphQLDgsQueryExecutor
import com.netflix.graphql.dgs.springgraphql.SpringGraphQLDgsReactiveQueryExecutor
import com.netflix.graphql.dgs.springgraphql.conditions.ConditionalOnDgsReload
import com.netflix.graphql.dgs.springgraphql.conditions.OnDgsReloadCondition
import com.netflix.graphql.dgs.springgraphql.webflux.DgsWebFluxGraphQLInterceptor
import com.netflix.graphql.dgs.springgraphql.webmvc.DgsWebMvcGraphQLInterceptor
import graphql.GraphQLError
Expand Down Expand Up @@ -100,6 +106,7 @@ import org.springframework.boot.system.JavaVersion
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import org.springframework.core.DefaultParameterNameDiscoverer
import org.springframework.core.Ordered
import org.springframework.core.PriorityOrdered
Expand Down Expand Up @@ -144,7 +151,7 @@ import java.util.function.Consumer
import java.util.stream.Collectors

/**
* Framework auto configuration based on open source Spring only, without Netflix integrations.
* Framework autoconfiguration based on open source Spring only, without Netflix integrations.
* This does NOT have logging, tracing, metrics and security integration.
*/
@Suppress("SpringJavaInjectionPointsAutowiringInspection")
Expand Down Expand Up @@ -221,16 +228,82 @@ open class DgsSpringGraphQLAutoConfiguration(
extensionProviders: List<DataLoaderInstrumentationExtensionProvider>,
customizers: List<DgsDataLoaderCustomizer>,
): DgsDataLoaderProvider =
DgsDataLoaderProvider(
DefaultDgsDataLoaderProvider(
applicationContext = applicationContext,
extensionProviders = extensionProviders,
customizers = customizers,
dataLoaderOptionsProvider = dataloaderOptionProvider,
scheduledExecutorService = dgsScheduledExecutorService,
scheduleDuration = dataloaderConfigProps.scheduleDuration,
enableTickerMode = dataloaderConfigProps.tickerModeEnabled,
customizers = customizers,
)

/**
* Autoconfiguration for DGS Data Loader reloading.
*
* This configuration is only activated when the 'dgs.reload' property is set to `true`.
* It provides the necessary beans for data loader reloading, including:
* - [ReloadableDgsDataLoaderProvider] that wraps the default provider implementation, the [DefaultDgsDataLoaderProvider].
* - An implementation of a [DgsDataLoaderReloadController] that can be used ot force reloading of _Data Loaders_.
*
* **The reloading functionality is designed to be used primarily in development**,
* it is discouraged to be used in production.
*/
@AutoConfiguration
@ConditionalOnDgsReload
open class DgsDataLoaderReloadAutoConfiguration(
private val dataloaderConfigProps: DgsDataloaderConfigurationProperties,
) {
/**
* Creates a [ReloadableDgsDataLoaderProvider] that wraps the standard [DgsDataLoaderProvider].
*
* This provider supports dynamic reloading of data loaders based on the [DgsReloadDataLoadersIndicator].
* It maintains the same interface as the standard provider but adds reload capabilities.
*
* The `@Primary` annotation ensures this bean takes precedence over the standard `DgsDataLoaderProvider`
* when reload functionality is enabled.
*
*/
@Bean
@Primary
open fun reloadableDgsDataLoaderProvider(
applicationContext: ApplicationContext,
dataLoaderOptionProvider: DgsDataLoaderOptionsProvider,
@Qualifier("dgsScheduledExecutorService") dgsScheduledExecutorService: ScheduledExecutorService,
extensionProviders: List<DataLoaderInstrumentationExtensionProvider>,
customizers: List<DgsDataLoaderCustomizer>,
): DgsDataLoaderProvider {
LOG.info("Creating reloadable data loader provider with reload support enabled")
return ReloadableDgsDataLoaderProvider(
applicationContext = applicationContext,
extensionProviders = extensionProviders,
customizers = customizers,
dataLoaderOptionsProvider = dataLoaderOptionProvider,
scheduledExecutorService = dgsScheduledExecutorService,
scheduleDuration = dataloaderConfigProps.scheduleDuration,
enableTickerMode = dataloaderConfigProps.tickerModeEnabled,
)
}

/**
* Creates the default data loader reload controller.
*
* This controller provides a programmatic API for triggering data loader reloads
* and accessing reload statistics. It's only available when reload functionality is enabled.
*
* @return DgsDataLoaderReloadController instance
*/
@Bean
@ConditionalOnMissingBean
open fun dgsDataLoaderReloadController(
reloadableDgsDataLoaderProvider: ReloadableDgsDataLoaderProvider,
): DgsDataLoaderReloadController {
LOG.info("Creating data loader reload controller")
// Get the actual ReloadableDgsDataLoaderProvider instance from the context
return DefaultDgsDataLoaderReloadController(reloadableDgsDataLoaderProvider)
}
}

@Bean
open fun entityFetcherRegistry(): EntityFetcherRegistry = EntityFetcherRegistry()

Expand Down Expand Up @@ -272,9 +345,7 @@ open class DgsSpringGraphQLAutoConfiguration(
@Bean
@ConditionalOnMissingBean
open fun defaultReloadSchemaIndicator(environment: Environment): ReloadSchemaIndicator {
val isLaptopProfile = environment.activeProfiles.contains("laptop")
val hotReloadSetting = environment.getProperty("dgs.reload", Boolean::class.java, isLaptopProfile)

val hotReloadSetting = OnDgsReloadCondition.evaluate(environment)
return ReloadSchemaIndicator {
hotReloadSetting
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.conditions

import org.springframework.boot.autoconfigure.condition.ConditionOutcome
import org.springframework.boot.autoconfigure.condition.SpringBootCondition
import org.springframework.context.annotation.ConditionContext
import org.springframework.core.env.Environment
import org.springframework.core.type.AnnotatedTypeMetadata
import kotlin.collections.contains

class OnDgsReloadCondition : SpringBootCondition() {
companion object {
/**
* `true`, if the _DGS Reload flag_ is enabled.
*/
fun evaluate(environment: Environment): Boolean {
val isLaptopProfile = environment.activeProfiles.contains("laptop")
val reloadEnabled = environment.getProperty("dgs.reload", Boolean::class.java, isLaptopProfile)
return reloadEnabled
}
}

override fun getMatchOutcome(
context: ConditionContext?,
metadata: AnnotatedTypeMetadata?,
): ConditionOutcome? {
val environment = context!!.environment
val reloadEnabled = evaluate(environment)
return if (reloadEnabled) {
ConditionOutcome.match("DgsReload enabled.")
} else {
ConditionOutcome.noMatch("DgsReload disabled")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* 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

import com.netflix.graphql.dgs.internal.DefaultDgsDataLoaderReloadController
import com.netflix.graphql.dgs.internal.ReloadableDgsDataLoaderProvider
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertNotNull
import org.junit.jupiter.api.assertNull

class DefaultDgsDataLoaderReloadControllerTest {
@Test
fun `should successfully reload data loaders`() {
val mockProvider = mockk<ReloadableDgsDataLoaderProvider>()
every { mockProvider.forceReload() } returns true

val controller = DefaultDgsDataLoaderReloadController(mockProvider)

val result = controller.reloadDataLoaders()

assertTrue(result)
assertNotNull(controller.getLastReloadTime())
verify(exactly = 1) { mockProvider.forceReload() }
}

@Test
fun `should handle reload failures gracefully`() {
val mockProvider = mockk<ReloadableDgsDataLoaderProvider>()
every { mockProvider.forceReload() } returns false

val controller = DefaultDgsDataLoaderReloadController(mockProvider)

val result = controller.reloadDataLoaders()

assertFalse(result)
// Last reload time should not be set on failure
assertNull(controller.getLastReloadTime())
verify(exactly = 1) { mockProvider.forceReload() }
}

@Test
fun `should handle provider exceptions`() {
val mockProvider = mockk<ReloadableDgsDataLoaderProvider>()
every { mockProvider.forceReload() } throws RuntimeException("Test exception")

val controller = DefaultDgsDataLoaderReloadController(mockProvider)

val result = controller.reloadDataLoaders()

assertFalse(result)
assertNull(controller.getLastReloadTime())
verify(exactly = 1) { mockProvider.forceReload() }
}

@Test
fun `should always report reload as enabled`() {
val mockProvider = mockk<ReloadableDgsDataLoaderProvider>()
val controller = DefaultDgsDataLoaderReloadController(mockProvider)

assertTrue(controller.isReloadEnabled())
}

@Test
fun `should track reload statistics`() {
val mockProvider = mockk<ReloadableDgsDataLoaderProvider>()
every { mockProvider.forceReload() } returns true

val controller = DefaultDgsDataLoaderReloadController(mockProvider)

// Initial stats
val initialStats = controller.getReloadStats()
assertEquals(0L, initialStats.totalReloads)
assertNull(initialStats.lastReloadTime)
assertNull(initialStats.lastReloadDuration)
assertTrue(initialStats.isEnabled)

// Perform some reloads
controller.reloadDataLoaders()
Thread.sleep(1) // Ensure different timestamp
controller.reloadDataLoaders()

val finalStats = controller.getReloadStats()
assertEquals(2L, finalStats.totalReloads)
assertNotNull(finalStats.lastReloadTime)
assertNotNull(finalStats.lastReloadDuration)
assertTrue(finalStats.isEnabled)

verify(exactly = 2) { mockProvider.forceReload() }
}

@Test
fun `should update last reload time only on success`() {
val mockProvider = mockk<ReloadableDgsDataLoaderProvider>()
val controller = DefaultDgsDataLoaderReloadController(mockProvider)

// First reload succeeds
every { mockProvider.forceReload() } returns true
controller.reloadDataLoaders()
val firstReloadTime = controller.getLastReloadTime()
assertNotNull(firstReloadTime)

Thread.sleep(1) // Ensure different timestamp

// Second reload fails
every { mockProvider.forceReload() } returns false
controller.reloadDataLoaders()
val secondReloadTime = controller.getLastReloadTime()

// Time should remain the same as the first successful reload
assertEquals(firstReloadTime, secondReloadTime)

val stats = controller.getReloadStats()
assertEquals(1L, stats.totalReloads) // Only one successful reload
}
}
Loading
Loading