GH-149 - Default configuration to await task termination for 2 seconds.
We now tweak the task executor to await termination for 2 seconds unless explicitly disabled or any of the Spring Boot task executor shutdown properties being set. This makes sure that long running application module listeners do not access resources already in shutdown when running integration tests, in which the context could already shutdown while the listener is still running.
This commit is contained in:
@@ -32,15 +32,26 @@
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-test-autoconfigure</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
|
||||
@@ -15,11 +15,22 @@
|
||||
*/
|
||||
package org.springframework.modulith.events.config;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.task.TaskExecutionProperties;
|
||||
import org.springframework.boot.autoconfigure.task.TaskExecutionProperties.Shutdown;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.modulith.events.DefaultEventPublicationRegistry;
|
||||
import org.springframework.modulith.events.EventPublicationRegistry;
|
||||
import org.springframework.modulith.events.EventPublicationRepository;
|
||||
@@ -35,15 +46,16 @@ import org.springframework.modulith.events.support.PersistentApplicationEventMul
|
||||
class EventPublicationConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
EventPublicationRegistry eventPublicationRegistry(EventPublicationRepository repository) {
|
||||
return new DefaultEventPublicationRegistry(repository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
PersistentApplicationEventMulticaster applicationEventMulticaster(
|
||||
EventPublicationRegistry eventPublicationRegistry) {
|
||||
|
||||
return new PersistentApplicationEventMulticaster(() -> eventPublicationRegistry);
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static PersistentApplicationEventMulticaster applicationEventMulticaster(
|
||||
ObjectFactory<EventPublicationRegistry> eventPublicationRegistry) {
|
||||
return new PersistentApplicationEventMulticaster(() -> eventPublicationRegistry.getObject());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -51,4 +63,58 @@ class EventPublicationConfiguration {
|
||||
static CompletionRegisteringAdvisor completionRegisteringAdvisor(ObjectFactory<EventPublicationRegistry> registry) {
|
||||
return new CompletionRegisteringAdvisor(registry::getObject);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
@ConditionalOnProperty(
|
||||
name = "spring.modulith.default-async-termination",
|
||||
havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
static AsyncPropertiesDefaulter asyncPropertiesDefaulter(Environment environment) {
|
||||
return new AsyncPropertiesDefaulter(environment);
|
||||
}
|
||||
|
||||
static class AsyncPropertiesDefaulter implements BeanPostProcessor {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AsyncPropertiesDefaulter.class);
|
||||
private static final String PROPERTY = "spring.task.execution.shutdown.await-termination";
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
AsyncPropertiesDefaulter(Environment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
|
||||
if (!(bean instanceof TaskExecutionProperties p)) {
|
||||
return bean;
|
||||
}
|
||||
|
||||
if (anyPropertyConfigured(PROPERTY, PROPERTY + "-period")) {
|
||||
return bean;
|
||||
}
|
||||
|
||||
LOGGER.debug("Defaulting async shutdown to await termination in 2 seconds.");
|
||||
|
||||
Shutdown shutdown = p.getShutdown();
|
||||
|
||||
shutdown.setAwaitTermination(true);
|
||||
shutdown.setAwaitTerminationPeriod(Duration.ofSeconds(2));
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
private boolean anyPropertyConfigured(String... properties) {
|
||||
|
||||
return Arrays.stream(properties)
|
||||
.map(it -> environment.getProperty(it, (String) null))
|
||||
.anyMatch(it -> it != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": [
|
||||
{
|
||||
"name": "spring.modulith.default-async-termination",
|
||||
"type": "java.lang.boolean",
|
||||
"description": "Whether to configure defaults for the async processing termination, namely to wait for task completion for 2 seconds. See TaskExecutionProperties for details.",
|
||||
"defaultValue": "true"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.modulith.events.config;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.task.TaskExecutionProperties;
|
||||
import org.springframework.boot.autoconfigure.task.TaskExecutionProperties.Shutdown;
|
||||
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.test.context.runner.ContextConsumer;
|
||||
import org.springframework.modulith.events.EventPublicationRepository;
|
||||
import org.springframework.modulith.events.config.EventPublicationConfiguration.AsyncPropertiesDefaulter;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link EventPublicationConfiguration}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class EventPublicationConfigurationIntegrationTests {
|
||||
|
||||
@Mock EventPublicationRepository repository;
|
||||
|
||||
@Test // GH-149
|
||||
void registersAsyncTerminationDefaulterByDefault() {
|
||||
|
||||
basicSetup()
|
||||
.run(context -> assertThat(context).hasSingleBean(AsyncPropertiesDefaulter.class));
|
||||
}
|
||||
|
||||
@Test // GH-149
|
||||
void doesNotRegisterDefaulterIfDisabledExplicitly() {
|
||||
|
||||
basicSetup()
|
||||
.withPropertyValues("spring.modulith.default-async-termination=false")
|
||||
.run(context -> assertThat(context).doesNotHaveBean(AsyncPropertiesDefaulter.class));
|
||||
}
|
||||
|
||||
@Test // GH-149
|
||||
void doesNotApplyDefaultingIfShutdownTerminationPropertyConfigured() {
|
||||
|
||||
basicSetup()
|
||||
.withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class))
|
||||
.withPropertyValues("spring.task.execution.shutdown.await-termination=false")
|
||||
.run(expect(Shutdown::isAwaitTermination, false));
|
||||
}
|
||||
|
||||
@Test // GH-149
|
||||
void doesNotApplyDefaultingIfShutdownTerminationPeriodPropertyConfigured() {
|
||||
|
||||
basicSetup()
|
||||
.withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class))
|
||||
.withPropertyValues("spring.task.execution.shutdown.await-termination-period=10m")
|
||||
.run(expect(Shutdown::getAwaitTerminationPeriod, Duration.ofMinutes(10)));
|
||||
}
|
||||
|
||||
private <T> ContextConsumer<AssertableApplicationContext> expect(Function<Shutdown, T> extractor,
|
||||
T expected) {
|
||||
|
||||
return context -> assertThat(context.getBean(TaskExecutionProperties.class).getShutdown())
|
||||
.extracting(extractor)
|
||||
.isEqualTo(expected);
|
||||
}
|
||||
|
||||
private ApplicationContextRunner basicSetup() {
|
||||
|
||||
return new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(EventPublicationConfiguration.class))
|
||||
.withBean(EventPublicationRepository.class, () -> repository);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user