From 47a8a7fa7799f3165cffc8819e8da208c6219153 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Sun, 26 Feb 2023 11:32:52 +0100 Subject: [PATCH] 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. --- .../spring-modulith-events-core/pom.xml | 15 ++- .../config/EventPublicationConfiguration.java | 74 ++++++++++++++- .../spring-configuration-metadata.json | 10 ++ ...licationConfigurationIntegrationTests.java | 94 +++++++++++++++++++ 4 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 spring-modulith-events/spring-modulith-events-core/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/config/EventPublicationConfigurationIntegrationTests.java diff --git a/spring-modulith-events/spring-modulith-events-core/pom.xml b/spring-modulith-events/spring-modulith-events-core/pom.xml index 14b25218..52aeaf21 100644 --- a/spring-modulith-events/spring-modulith-events-core/pom.xml +++ b/spring-modulith-events/spring-modulith-events-core/pom.xml @@ -32,15 +32,26 @@ org.springframework spring-aop - + + + org.springframework.boot + spring-boot-autoconfigure + + - + org.springframework spring-test test + + org.springframework.boot + spring-boot-test-autoconfigure + test + + org.slf4j diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EventPublicationConfiguration.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EventPublicationConfiguration.java index 0cc09d51..54f07746 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EventPublicationConfiguration.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EventPublicationConfiguration.java @@ -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) { + return new PersistentApplicationEventMulticaster(() -> eventPublicationRegistry.getObject()); } @Bean @@ -51,4 +63,58 @@ class EventPublicationConfiguration { static CompletionRegisteringAdvisor completionRegisteringAdvisor(ObjectFactory 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); + } + } } diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/resources/META-INF/spring-configuration-metadata.json b/spring-modulith-events/spring-modulith-events-core/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 00000000..517150cd --- /dev/null +++ b/spring-modulith-events/spring-modulith-events-core/src/main/resources/META-INF/spring-configuration-metadata.json @@ -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" + } + ] +} diff --git a/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/config/EventPublicationConfigurationIntegrationTests.java b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/config/EventPublicationConfigurationIntegrationTests.java new file mode 100644 index 00000000..91b86c69 --- /dev/null +++ b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/config/EventPublicationConfigurationIntegrationTests.java @@ -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 ContextConsumer expect(Function 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); + } +}