Improve test context/infrastructure logic to reliably determine whether the GemFire/Geode cache instance was properly closed.

This test logic is necessary to prevent interference between tests, particularly for integration tests involving live GemFire/Geode objects, especially since GemFire/Geode is rife with Singletons.

For example, SSL configuration settings linger between tests if the cache is not fully closed (when all resources are adequately released) in a timely manner.
This commit is contained in:
John Blum
2021-03-18 13:54:48 -07:00
parent 3d13306ace
commit 14fa273445
2 changed files with 153 additions and 15 deletions

View File

@@ -24,10 +24,14 @@ import java.io.IOException;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -40,14 +44,24 @@ import org.apache.geode.cache.CacheClosedException;
import org.apache.geode.cache.GemFireCache;
import org.apache.geode.distributed.Locator;
import org.apache.geode.internal.InternalDataSerializer;
import org.apache.geode.internal.cache.CacheLifecycleListener;
import org.apache.geode.internal.cache.GemFireCacheImpl;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.internal.net.SSLConfigurationFactory;
import org.apache.geode.internal.net.SocketCreatorFactory;
import org.apache.shiro.util.Assert;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.data.gemfire.GemfireUtils;
import org.springframework.data.gemfire.support.GemfireBeanFactoryLocator;
import org.springframework.data.gemfire.tests.mock.GemFireMockObjectsSupport;
import org.springframework.data.gemfire.tests.util.FileUtils;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
/**
@@ -97,15 +111,17 @@ public abstract class IntegrationTestsSupport {
private static final Predicate<String> SPRING_DOT_SYSTEM_PROPERTY_NAME_PREDICATE =
propertyName -> String.valueOf(propertyName).toLowerCase().startsWith("spring");
private static final Predicate<String> ALL_SYSTEM_PROPERTIES_NAME_PREDICATE =
JAVAX_NET_SSL_NAME_PREDICATE
.or(GEMFIRE_DOT_SYSTEM_PROPERTY_NAME_PREDICATE)
.or(GEODE_DOT_SYSTEM_PROPERTY_NAME_PREDICATE)
.or(SPRING_DOT_SYSTEM_PROPERTY_NAME_PREDICATE);
private static final Predicate<String> ALL_SYSTEM_PROPERTIES_NAME_PREDICATE = JAVAX_NET_SSL_NAME_PREDICATE
.or(GEMFIRE_DOT_SYSTEM_PROPERTY_NAME_PREDICATE)
.or(GEODE_DOT_SYSTEM_PROPERTY_NAME_PREDICATE)
.or(SPRING_DOT_SYSTEM_PROPERTY_NAME_PREDICATE);
private static final TestContextCacheLifecycleListenerAdapter cacheLifecycleListener =
TestContextCacheLifecycleListenerAdapter.getInstance();
@BeforeClass
public static void closeAnyGemFireCacheInstanceBeforeTestExecution() {
closeGemFireCacheWaitOnCloseEvent();
closeGemFireCacheWaitOnCacheClosedEvent();
}
@BeforeClass
@@ -113,8 +129,6 @@ public abstract class IntegrationTestsSupport {
SocketCreatorFactory.close();
}
// TODO: Remove once GEODE-7157 (https://issues.apache.org/jira/browse/GEODE-7157) is fixed!
// Do the job of Apache Geode & VMware GemFire since it cannot do its own damn job!
@BeforeClass
public static void closeAnySslConfigurationBeforeTestExecution() {
@@ -179,11 +193,11 @@ public abstract class IntegrationTestsSupport {
.forEach(InternalDataSerializer::unregister);
}
public static void closeGemFireCacheWaitOnCloseEvent() {
closeGemFireCacheWaitOnCloseEvent(DEFAULT_WAIT_DURATION);
public static void closeGemFireCacheWaitOnCacheClosedEvent() {
closeGemFireCacheWaitOnCacheClosedEvent(DEFAULT_WAIT_DURATION);
}
public static void closeGemFireCacheWaitOnCloseEvent(long duration) {
public static void closeGemFireCacheWaitOnCacheClosedEvent(long duration) {
AtomicBoolean closed = new AtomicBoolean(false);
@@ -191,9 +205,8 @@ public abstract class IntegrationTestsSupport {
try {
return Optional.ofNullable(GemfireUtils.resolveGemFireCache())
.filter(cache -> !closed.get())
.filter(cache -> !cache.isClosed())
.map(IntegrationTestsSupport::close)
.map(GemFireCache::isClosed)
.map(cacheLifecycleListener::isClosed)
.orElse(true);
}
catch (CacheClosedException ignore) {
@@ -203,7 +216,7 @@ public abstract class IntegrationTestsSupport {
}, duration);
}
private static GemFireCache close(GemFireCache cache) {
private static @Nullable GemFireCache close(@Nullable GemFireCache cache) {
return Optional.ofNullable(cache)
.map(it -> {
@@ -372,4 +385,125 @@ public abstract class IntegrationTestsSupport {
protected interface Condition {
boolean evaluate();
}
protected static abstract class AbstractApplicationContextCacheLifecycleListenerAdapter
implements ApplicationEventPublisherAware, CacheLifecycleListener {
private ApplicationEventPublisher applicationEventPublisher;
/**
* @inheritDoc
*/
@Override
public void setApplicationEventPublisher(@Nullable ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
protected Optional<ApplicationEventPublisher> getApplicationEventPublisher() {
return Optional.ofNullable(this.applicationEventPublisher);
}
/**
* @inheritDoc
*/
@Override
public void cacheCreated(InternalCache cache) {
getApplicationEventPublisher().ifPresent(eventPublisher ->
eventPublisher.publishEvent(new CacheCreatedEvent(cache)));
}
/**
* @inheritDoc
*/
@Override
public void cacheClosed(InternalCache cache) {
getApplicationEventPublisher().ifPresent(eventPublisher ->
eventPublisher.publishEvent(new CacheClosedEvent(cache)));
}
}
protected static class TestContextCacheLifecycleListenerAdapter
extends AbstractApplicationContextCacheLifecycleListenerAdapter {
private static final AtomicReference<TestContextCacheLifecycleListenerAdapter> INSTANCE =
new AtomicReference<>(null);
public static TestContextCacheLifecycleListenerAdapter getInstance() {
return INSTANCE.updateAndGet(instance -> instance != null ? instance
: newTestContextCacheLifecycleListener());
}
private static TestContextCacheLifecycleListenerAdapter newTestContextCacheLifecycleListener() {
TestContextCacheLifecycleListenerAdapter listener = new TestContextCacheLifecycleListenerAdapter();
GemFireCacheImpl.addCacheLifecycleListener(listener);
return listener;
}
private final Map<GemFireCache, Object> cacheReferences = Collections.synchronizedMap(new WeakHashMap<>());
public boolean isClosed(@Nullable GemFireCache cache) {
return cache == null || (cache.isClosed() && wasCacheClosed(cache));
}
protected boolean wasCacheClosed(@Nullable GemFireCache cache) {
return !this.cacheReferences.containsKey(cache);
}
/**
* @inheritDoc
*/
@Override
public void cacheCreated(@Nullable InternalCache cache) {
if (cache != null) {
this.cacheReferences.put(cache, this);
super.cacheCreated(cache);
}
}
/**
* @inheritDoc
*/
@Override
public void cacheClosed(@Nullable InternalCache cache) {
if (cache != null) {
this.cacheReferences.remove(cache);
super.cacheClosed(cache);
}
}
}
protected static class AbstractCacheEvent extends ApplicationEvent {
protected static <T> T requireNonNull(@NonNull T target, String message) {
Assert.notNull(target, message);
return target;
}
protected AbstractCacheEvent(@NonNull GemFireCache cache) {
super(requireNonNull(cache, "GemFireCache must not be null"));
}
public @NonNull GemFireCache getCache() {
return (GemFireCache) getSource();
}
}
public static class CacheCreatedEvent extends AbstractCacheEvent {
public CacheCreatedEvent(@NonNull GemFireCache cache) {
super(cache);
}
}
public static class CacheClosedEvent extends AbstractCacheEvent {
public CacheClosedEvent(@NonNull GemFireCache cache) {
super(cache);
}
}
}

View File

@@ -22,6 +22,7 @@ import org.junit.After;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.gemfire.util.ArrayUtils;
/**
* The {@link SpringApplicationContextIntegrationTestsSupport} class is an extension of {@link IntegrationTestsSupport}
@@ -54,7 +55,7 @@ public abstract class SpringApplicationContextIntegrationTestsSupport extends In
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(annotatedClasses);
applicationContext.register(ArrayUtils.nullSafeArray(annotatedClasses, Class.class));
applicationContext.registerShutdownHook();
processBeforeRefresh(applicationContext);
applicationContext.refresh();
@@ -63,6 +64,9 @@ public abstract class SpringApplicationContextIntegrationTestsSupport extends In
}
protected ConfigurableApplicationContext processBeforeRefresh(ConfigurableApplicationContext applicationContext) {
TestContextCacheLifecycleListenerAdapter.getInstance().setApplicationEventPublisher(applicationContext);
return applicationContext;
}