diff --git a/spring-data-geode-test/src/main/java/org/springframework/data/gemfire/tests/integration/ForkingClientServerIntegrationTestsSupport.java b/spring-data-geode-test/src/main/java/org/springframework/data/gemfire/tests/integration/ForkingClientServerIntegrationTestsSupport.java index 19bfa72..c2c5a4d 100644 --- a/spring-data-geode-test/src/main/java/org/springframework/data/gemfire/tests/integration/ForkingClientServerIntegrationTestsSupport.java +++ b/spring-data-geode-test/src/main/java/org/springframework/data/gemfire/tests/integration/ForkingClientServerIntegrationTestsSupport.java @@ -16,23 +16,34 @@ package org.springframework.data.gemfire.tests.integration; import java.io.IOException; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; import org.junit.AfterClass; import org.apache.geode.cache.Cache; import org.apache.geode.cache.client.ClientCache; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; import org.springframework.data.gemfire.config.annotation.CacheServerApplication; import org.springframework.data.gemfire.config.annotation.ClientCacheApplication; import org.springframework.data.gemfire.config.annotation.EnablePdx; import org.springframework.data.gemfire.tests.integration.config.ClientServerIntegrationTestsConfiguration; import org.springframework.data.gemfire.tests.process.ProcessWrapper; import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.data.gemfire.util.SpringUtils; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; @@ -106,7 +117,7 @@ public abstract class ForkingClientServerIntegrationTestsSupport extends ClientS @EnablePdx @ClientCacheApplication(logLevel = GEMFIRE_LOG_LEVEL) - protected static class BaseGemFireClientConfiguration extends ClientServerIntegrationTestsConfiguration { } + public static class BaseGemFireClientConfiguration extends ClientServerIntegrationTestsConfiguration { } @EnablePdx @CacheServerApplication(name = "ForkingClientServerIntegrationTestsSupport", logLevel = GEMFIRE_LOG_LEVEL) @@ -120,4 +131,71 @@ public abstract class ForkingClientServerIntegrationTestsSupport extends ClientS applicationContext.registerShutdownHook(); } } + + @Configuration + public static class SpringApplicationTerminatorConfiguration { + + @Bean + public SpringApplicationTerminatorConfigurer springApplicationTerminatorConfigurer() { + return SpringApplicationTerminatorConfigurer.EMPTY; + } + + @EventListener(classes = ContextRefreshedEvent.class) + public void configureTerminator(@NonNull ContextRefreshedEvent event) { + + Function safeSpringApplicationTerminatorConfigurerResolver = + applicationContext -> SpringUtils.safeGetValue(() -> + applicationContext.getBean(SpringApplicationTerminatorConfigurer.class), + (SpringApplicationTerminatorConfigurer) null); + + Runnable springApplicationTerminatorRunnable = () -> System.exit(-1); + + ThreadFactory springApplicationTerminatorThreadFactory = runnable -> { + + Thread springApplicationTerminatorThread = + new Thread(runnable, "Spring Application Terminator Thread"); + + springApplicationTerminatorThread.setDaemon(true); + springApplicationTerminatorThread.setPriority(Thread.NORM_PRIORITY); + + return springApplicationTerminatorThread; + }; + + Optional.ofNullable(event) + .map(ContextRefreshedEvent::getApplicationContext) + .map(safeSpringApplicationTerminatorConfigurerResolver) + .filter(SpringApplicationTerminatorConfigurer::isNotEmpty) + .filter(this::delayIsGreaterThanZero) + .ifPresent(configurer -> + Executors.newScheduledThreadPool(1, springApplicationTerminatorThreadFactory) + .schedule(springApplicationTerminatorRunnable, configurer.delay().toMillis(), + TimeUnit.MILLISECONDS) + ); + } + + private boolean delayIsGreaterThanZero(@NonNull SpringApplicationTerminatorConfigurer configurer) { + + return Optional.ofNullable(configurer) + .map(SpringApplicationTerminatorConfigurer::delay) + .map(delay -> Duration.ZERO.compareTo(delay) > 0) + .orElse(false); + } + } + + public interface SpringApplicationTerminatorConfigurer { + + SpringApplicationTerminatorConfigurer EMPTY = new SpringApplicationTerminatorConfigurer() { }; + + default boolean isEmpty() { + return EMPTY.equals(this); + } + + default boolean isNotEmpty() { + return !isEmpty(); + } + + default Duration delay() { + return Duration.ZERO; + } + } } diff --git a/spring-data-geode-test/src/test/java/org/springframework/data/gemfire/tests/integration/SpringApplicationTerminatingIntegrationTests.java b/spring-data-geode-test/src/test/java/org/springframework/data/gemfire/tests/integration/SpringApplicationTerminatingIntegrationTests.java new file mode 100644 index 0000000..bdec9f0 --- /dev/null +++ b/spring-data-geode-test/src/test/java/org/springframework/data/gemfire/tests/integration/SpringApplicationTerminatingIntegrationTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2019 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.data.gemfire.tests.integration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.Scanner; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.data.gemfire.tests.process.ProcessWrapper; + +/** + * Integration Tests testing and asserting Spring Application termination configuration. + * + * @author John Blum + * @see java.time.Duration + * @see java.time.Instant + * @see org.junit.Test + * @see org.springframework.context.annotation.AnnotationConfigApplicationContext + * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.context.annotation.Import + * @see org.springframework.context.annotation.Profile + * @see org.springframework.data.gemfire.tests.process.ProcessWrapper + * @since 0.0.24 + */ +@SuppressWarnings("unused") +public class SpringApplicationTerminatingIntegrationTests extends ForkingClientServerIntegrationTestsSupport { + + private static Instant startTime; + + private static ProcessWrapper springApplicationProcess; + + @BeforeClass + public static void startSpringApplication() throws IOException { + + springApplicationProcess = run(TestSpringApplicationConfiguration.class, + "-Dspring.profiles.active=SpringApplicationTerminator"); + + startTime = Instant.now(); + } + + @AfterClass + public static void assertSpringApplicationTerminated() { + + Condition springApplicationTerminatedCondition = () -> springApplicationProcess.isNotRunning(); + + waitOn(springApplicationTerminatedCondition, Duration.ofSeconds(10L).toMillis()); + + assertThat(Duration.between(startTime, Instant.now())).isGreaterThan(Duration.ofSeconds(5)); + } + + @Test + public void springApplicationIsRunning() { + + assertThat(springApplicationProcess).isNotNull(); + assertThat(springApplicationProcess.isAlive()).isTrue(); + assertThat(springApplicationProcess.isRunning()).isTrue(); + } + + @Configuration + @Profile("SpringApplicationTerminator") + @Import(SpringApplicationTerminatorConfiguration.class) + static class TestSpringApplicationConfiguration { + + public static void main(String[] args) { + + AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + + applicationContext.register(TestSpringApplicationConfiguration.class); + applicationContext.registerShutdownHook(); + + new Scanner(System.in).nextLine(); + } + + @Bean + SpringApplicationTerminatorConfigurer terminatorConfigurer() { + + return new SpringApplicationTerminatorConfigurer() { + + @Override + public Duration delay() { + return Duration.ofSeconds(5); + } + }; + } + } +}