Skip to content

Commit

Permalink
Some improvements on GraphQL asynchronous configuration.
Browse files Browse the repository at this point in the history
  • Loading branch information
desiderati committed Nov 29, 2023
1 parent e1d14ca commit 86fc0c6
Show file tree
Hide file tree
Showing 16 changed files with 147 additions and 152 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Commons Herd.io
---------------

[![Build Status](https://github.com/desiderati/commons/workflows/Build/badge.svg)](https://github.com/desiderati/commons/actions)
[![Version](https://img.shields.io/badge/Version-3.1.2.RELEASE-red.svg)](https://github.com/desiderati/commons/releases)
[![Version](https://img.shields.io/badge/Version-3.1.3.RELEASE-red.svg)](https://github.com/desiderati/commons/releases)
[![GitHub Stars](https://img.shields.io/github/stars/desiderati/commons.svg?label=GitHub%20Stars)](https://github.com/desiderati/commons/)
[![LICENSE](https://img.shields.io/badge/License-MIT-lightgrey.svg)](https://github.com/desiderati/commons/blob/master/LICENSE)

Expand All @@ -24,6 +24,9 @@ Changelog

All project changes will be documented in this file.

#### [3.1.3.RELEASE] - 2023-11-27
- Some improvements on GraphQL asynchronous configuration.

#### [3.1.2.RELEASE] - 2023-11-27
- Some improvements on GraphQL ErrorHandling.

Expand Down
2 changes: 1 addition & 1 deletion common-parent-info/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
</repositories>

<properties>
<revision>3.1.2.RELEASE</revision>
<revision>3.1.3.RELEASE</revision>

<!-- Major versions. -->
<commons-herd.io.version>${revision}</commons-herd.io.version>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.herd.common.web.security.configuration;

import graphql.kickstart.autoconfigure.web.OnSchemaOrSchemaProviderBean;
import graphql.kickstart.autoconfigure.web.servlet.AsyncServletProperties;
import graphql.kickstart.autoconfigure.web.servlet.GraphQLServletProperties;
import graphql.kickstart.autoconfigure.web.servlet.GraphQLWebSecurityAutoConfiguration;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.task.DelegatingSecurityContextAsyncTaskExecutor;
import org.springframework.web.servlet.DispatcherServlet;

import java.util.concurrent.Executor;

@Configuration
@RequiredArgsConstructor
@ConditionalOnWebApplication(type = Type.SERVLET)
@Conditional(OnSchemaOrSchemaProviderBean.class)
@ConditionalOnProperty(value = "graphql.servlet.enabled", havingValue = "true", matchIfMissing = true)
@AutoConfigureBefore(GraphQLWebSecurityAutoConfiguration.class)
@ConditionalOnClass({DispatcherServlet.class, DefaultAuthenticationEventPublisher.class})
@EnableConfigurationProperties({GraphQLServletProperties.class, AsyncServletProperties.class})
public class GraphQLWebSecurityConfiguration {

private final AsyncServletProperties asyncServletProperties;

@Bean("graphqlAsyncTaskExecutor")
@ConditionalOnProperty(prefix = "spring.mvc.async", name = "thread-context-inheritable", havingValue = "true")
public Executor simpleGraphQLAsyncTaskExecutor(
@Qualifier(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME) AsyncTaskExecutor taskExecutor
) {
if (asyncServletProperties.isDelegateSecurityContext()) {
return new DelegatingSecurityContextAsyncTaskExecutor(taskExecutor);
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package io.herd.common.web.security.configuration;

import graphql.kickstart.autoconfigure.web.servlet.GraphQLWebSecurityAutoConfiguration;
import io.herd.common.web.UrlUtils;
import io.herd.common.web.configuration.CorsProperties;
import io.herd.common.web.configuration.WebAutoConfiguration;
Expand All @@ -31,6 +32,7 @@
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
Expand All @@ -48,6 +50,7 @@
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
Expand All @@ -61,16 +64,22 @@
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

import static org.springframework.security.core.context.SecurityContextHolder.MODE_INHERITABLETHREADLOCAL;

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
@PropertySource("classpath:application-common-web-security.properties")
@AutoConfigureBefore(GraphQLWebSecurityAutoConfiguration.class)
@ComponentScan(basePackages = "io.herd.common.web.security",
// Do not add the auto-configured classes, otherwise the auto-configuration will not work as expected.
excludeFilters = @ComponentScan.Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)
)
@Import(WebAutoConfiguration.class) // To be used with @WebMvcTest
@Import({
WebAutoConfiguration.class,
GraphQLWebSecurityConfiguration.class
}) // To be used with @WebMvcTest
public class WebSecurityAutoConfiguration implements WebMvcConfigurer {

@Value("${spring.web.security.default.authentication.enabled:false}")
Expand Down Expand Up @@ -118,6 +127,18 @@ public class WebSecurityAutoConfiguration implements WebMvcConfigurer {
@Value("${springdoc.api-docs.path:/api-docs}")
private String springDocOpenApiPath;

public WebSecurityAutoConfiguration(
@Value("${graphql.servlet.async.enabled:false}")
boolean graphqlServletAsyncEnabled,

@Value("${graphql.servlet.async.delegate-security-context:true}")
boolean graphqlServletAsyncDelegateSecurityContext
) {
if (graphqlServletAsyncEnabled && graphqlServletAsyncDelegateSecurityContext) {
SecurityContextHolder.setStrategyName(MODE_INHERITABLETHREADLOCAL);
}
}

private JwtAuthenticationFilter jwtAuthenticationFilter;
private JwtAuthorizationFilter jwtAuthorizationFilter;
private SignRequestAuthorizationFilter signRequestAuthorizationFilter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,13 @@ private SecretKey loadSecretKey() {
public String generateToken(JwtTokenConfigurer configurer) {
Claims tokenPayload = Jwts.claims();
configurer.configure(tokenPayload);
if (expirationPeriod > 0) {
tokenPayload.setExpiration(
Date.from(LocalDateTime.now().plusHours(expirationPeriod).toInstant(ZoneOffset.UTC))
);
}
tokenPayload.setExpiration(
Date.from(
LocalDateTime.now().plusHours(
expirationPeriod > 0 ? expirationPeriod : Integer.MAX_VALUE
).toInstant(ZoneOffset.UTC)
)
);

JwtBuilder builder = Jwts.builder().setClaims(tokenPayload);
if (jwtEncryptionMethod == JwtEncryptionMethod.ASYMMETRIC) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,51 +18,40 @@
*/
package io.herd.common.web.configuration;

import io.herd.common.configuration.AsyncProperties;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.CallableProcessingInterceptor;
import org.springframework.web.context.request.async.TimeoutCallableProcessingInterceptor;
import org.springframework.web.filter.RequestContextFilter;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.concurrent.Callable;

@Slf4j
@Configuration
@EnableConfigurationProperties(AsyncProperties.class)
@ConditionalOnProperty(name = "spring.async.enabled", havingValue = "true")
public class AsyncWebConfiguration {

private final AsyncProperties asyncProperties;

@Autowired
public AsyncWebConfiguration(AsyncProperties asyncProperties) {
this.asyncProperties = asyncProperties;
}

/** Configure async support for Spring MVC. */
/**
* Configure async support for Spring MVC.
*/
@Bean
public WebMvcConfigurer webMvcConfigurerConfigurer(
@Qualifier("defaultAsyncTaskExecutor") AsyncTaskExecutor taskExecutor,
CallableProcessingInterceptor callableProcessingInterceptor
) {
public WebMvcConfigurer webMvcConfigurerConfigurer(CallableProcessingInterceptor callableProcessingInterceptor) {
return new WebMvcConfigurer() {
@Override
public void configureAsyncSupport(@NotNull AsyncSupportConfigurer configurer) {
log.info("Configuring Spring MVC with asynchronous task executor...");
configurer.setDefaultTimeout(asyncProperties.getTaskTimeout()).setTaskExecutor(taskExecutor);
log.info("Configuring Spring MVC with custom CallableProcessingInterceptor...");
configurer.registerCallableInterceptors(callableProcessingInterceptor);
WebMvcConfigurer.super.configureAsyncSupport(configurer);
}
};
}
Expand All @@ -80,4 +69,21 @@ public <T> Object handleTimeout(NativeWebRequest request, Callable<T> task) thro
}
};
}

@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
@ConditionalOnProperty(prefix = "spring.mvc.async", name = "thread-context-inheritable", havingValue = "true")
public AsyncTaskExecutor simpleAsyncTaskExecutor() {
log.info("Creating simple asynchronous task executor...");
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
executor.setThreadNamePrefix("simple-task-");
return executor;
}

@Bean
@ConditionalOnProperty(prefix = "spring.mvc.async", name = "thread-context-inheritable", havingValue = "true")
public RequestContextFilter requestContextFilter() {
RequestContextFilter requestContextFilter = new OrderedRequestContextFilter();
requestContextFilter.setThreadContextInheritable(true);
return requestContextFilter;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
@ConditionalOnJpa
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnProperty(prefix = "spring.jpa", name = "open-in-view", havingValue = "true")
public class JpaWebAutoConfiguration {
public class JpaWebConfiguration {

@Bean
public Filter openEntityManagerInViewFilter() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
// Need to be auto-loaded too.
AsyncWebConfiguration.class,
JpaAutoConfiguration.class,
JpaWebAutoConfiguration.class,
JpaWebConfiguration.class,
OpenApiConfiguration.class
})
public class WebAutoConfiguration implements WebMvcRegistrations, WebMvcConfigurer, RepositoryRestConfigurer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ private boolean shouldLogAsWarning(Throwable throwable, String errorMessage) {

if (isSameClass) {
List<String> msgsList = entry.getValue();
if (msgsList.size() == 0) {
if (msgsList.isEmpty()) {
return true;
} else {
for (String msg : msgsList) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import graphql.execution.DataFetcherExceptionHandlerResult
import graphql.kickstart.spring.error.ErrorContext
import io.herd.common.exception.ApplicationException
import org.springframework.context.MessageSource
import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.web.bind.annotation.ExceptionHandler
import java.io.Serializable
import java.lang.reflect.UndeclaredThrowableException
Expand All @@ -41,9 +42,6 @@ open class GraphQLExceptionHandler(
private const val DEFAULT_ERROR_CODE = "internal_server_error"
private const val DEFAULT_ERROR_MESSAGE =
"The server encountered an unexpected condition that prevented it from fulfilling the request!"

// TODO Felipe Desiderati: Extrair a localização a partir da requisição.
private val DEFAULT_LOCALE = Locale("pt", "BR")
}

override fun handleException(
Expand Down Expand Up @@ -103,11 +101,11 @@ open class GraphQLExceptionHandler(
errorContext: ErrorContext,
extensions: Map<String, Any?> = mapOf()
): GraphQLError {
val internalServerError = getMessage(DEFAULT_LOCALE, DEFAULT_ERROR_CODE, DEFAULT_ERROR_MESSAGE)
val internalServerError = getMessage(LocaleContextHolder.getLocale(), DEFAULT_ERROR_CODE, DEFAULT_ERROR_MESSAGE)
return GraphqlErrorBuilder.newError()
.message(
getMessage(
DEFAULT_LOCALE,
LocaleContextHolder.getLocale(),
message ?: DEFAULT_ERROR_CODE,
internalServerError
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
"description": "Only the paths defined here will be exposed on Swagger UI.",
"defaultValue": "/v.*,/public/v.*"
},
{
"name": "spring.mvc.async.thread-context-inheritable",
"type": "java.lang.Boolean",
"description": "Set whether to expose the LocaleContext and RequestAttributes as inheritable for child threads (using an InheritableThreadLocal). If true, a SimpleAsyncTaskExecutor will be used (instead of a ThreadPoolTaskExecutor) since it will never re-use threads. WARNING: Do not use inheritance for child threads if you are accessing a thread pool which is configured to potentially add new threads on demand, since this will expose the inherited context to such a pooled thread.",
"defaultValue": false
},
{
"name": "spring.web.cors.allowed-methods",
"type": "java.lang.String",
Expand Down
15 changes: 15 additions & 0 deletions common-web/src/main/resources/application-common-web.properties
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@ springdoc.package-to-scan=
# Only the paths defined here will be exposed on Swagger UI.
springdoc.paths-to-expose=/v.*,/public/v.*

#
# Async Support
#
# Set whether to expose the LocaleContext and RequestAttributes as inheritable for child threads, using
# an InheritableThreadLocal. If configured as true, a SimpleAsyncTaskExecutor will be used (instead of a
# ThreadPoolTaskExecutor), since it will never re-use threads.
#
# WARNING: Do not use inheritance for child threads if you are accessing a thread pool which is configured
# to potentially add new threads on demand, since this will expose the inherited context to such a pooled thread.
spring.mvc.async.thread-context-inheritable=false

# Amount of time before asynchronous request handling times out. If this value is not set, the default timeout
# of the underlying implementation is used.
spring.mvc.async.request-timeout=30000

#
# GraphQL Support
#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,44 +21,19 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Slf4j
@Configuration
@EnableAsync
@EnableScheduling
@EnableConfigurationProperties(AsyncProperties.class)
@ConditionalOnProperty(name = "spring.async.enabled", havingValue = "true")
public class AsyncConfiguration implements AsyncConfigurer {

private final AsyncProperties asyncProperties;

@Autowired
public AsyncConfiguration(AsyncProperties asyncProperties) {
this.asyncProperties = asyncProperties;
}

@Bean(name = "defaultAsyncTaskExecutor")
public AsyncTaskExecutor defaultAsyncTaskExecutor() {
log.info("Creating default asynchronous task executor...");
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("Async-");
executor.setCorePoolSize(asyncProperties.getInitialPoolSize());
executor.setMaxPoolSize(asyncProperties.getMaxPoolSize());
executor.setQueueCapacity(asyncProperties.getQueueCapacity());
executor.initialize();
return executor;
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
Expand Down
Loading

0 comments on commit 86fc0c6

Please sign in to comment.