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:
Oliver Drotbohm
2023-02-26 11:32:52 +01:00
parent c51ac5c6fb
commit 47a8a7fa77
4 changed files with 187 additions and 6 deletions

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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"
}
]
}

View File

@@ -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);
}
}