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