Add 'strictMatch' annotation attribute to the @EnableClusterAware annotation.

Using 'strictMatch' enables fail-fast behavior so that users can configure their Spring Boot, Apache Geode ClientCache applications to fail on startup if no cluster is available across any environment.

Additionally, enhanced the log output to give users better information about the runtime environment and whether an Apache Geode-based cluster was found and available.

Resolves gh-99.
This commit is contained in:
John Blum
2021-01-15 19:32:44 -08:00
parent 62848e9277
commit 4090cf4ebd
9 changed files with 560 additions and 47 deletions

View File

@@ -16,11 +16,16 @@
package org.springframework.geode.config.annotation;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.data.gemfire.config.annotation.EnableClusterConfiguration;
import org.springframework.lang.NonNull;
import org.slf4j.Logger;
/**
* The {@link ClusterAvailableConfiguration} class is a Spring {@link Configuration} class that enables configuration
@@ -30,9 +35,13 @@ import org.springframework.data.gemfire.config.annotation.EnableClusterConfigura
* @see org.springframework.boot.autoconfigure.condition.AnyNestedCondition
* @see org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform
* @see org.springframework.boot.cloud.CloudPlatform
* @see org.springframework.context.annotation.ConditionContext
* @see org.springframework.context.annotation.Conditional
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.core.env.Environment
* @see org.springframework.core.type.AnnotatedTypeMetadata
* @see org.springframework.data.gemfire.config.annotation.EnableClusterConfiguration
* @see org.springframework.geode.config.annotation.ClusterAwareConfiguration
* @since 1.2.0
*/
@Configuration
@@ -47,17 +56,115 @@ public class ClusterAvailableConfiguration {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@Conditional(ClusterAvailableCondition.class)
static class IsClusterAvailableCondition { }
//@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
@Conditional(CloudFoundryClusterAvailableCondition.class)
static class IsCloudFoundryClusterAvailableCondition { }
@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
static class IsCloudFoundryEnvironmentCondition { }
//@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
@Conditional(KubernetesClusterAvailableCondition.class)
static class IsKubernetesClusterAvailableCondition { }
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
static class IsKubernetesEnvironmentCondition { }
@Conditional(StandaloneClusterAvailableCondition.class)
static class IsStandaloneClusterAvailableCondition { }
}
public static final class ClusterAvailableCondition extends ClusterAwareConfiguration.ClusterAwareCondition { }
protected static abstract class AbstractCloudPlatformAvailableCondition
extends ClusterAwareConfiguration.ClusterAwareCondition {
protected abstract String getCloudPlatformName();
@Override
protected String getRuntimeEnvironmentName() {
return getCloudPlatformName();
}
protected abstract boolean isCloudPlatformActive(@NonNull Environment environment);
protected boolean isInfoLoggingEnabled() {
return getLogger().isInfoEnabled();
}
protected boolean isMatchingStrictOrLoggable(boolean match, boolean strictMatch) {
return match && (strictMatch || isInfoLoggingEnabled());
}
@Override
protected void logConnectedRuntimeEnvironment(@NonNull Logger logger) {
if (logger.isInfoEnabled()) {
logger.info("Spring Boot application is running in a client/server topology,"
+ " inside a [{}] Cloud-managed Environment", getRuntimeEnvironmentName());
}
}
protected void logRuntimeEnvironment(@NonNull Logger logger) {
if (logger.isInfoEnabled()) {
logger.info("Spring Boot application is running in a [{}] Cloud-managed Environment",
getRuntimeEnvironmentName());
}
}
@Override
public synchronized final boolean matches(@NonNull ConditionContext conditionContext,
@NonNull AnnotatedTypeMetadata typeMetadata) {
boolean match = isCloudPlatformActive(conditionContext.getEnvironment());
boolean strictMatch = isStrictMatch(typeMetadata, conditionContext);
if (isMatchingStrictOrLoggable(match, strictMatch)) {
logRuntimeEnvironment(getLogger());
match |= super.matches(conditionContext, typeMetadata);
}
return match;
}
}
public static final class CloudFoundryClusterAvailableCondition extends AbstractCloudPlatformAvailableCondition {
protected static final String CLOUD_FOUNDRY_NAME = "CloudFoundry";
protected static final String RUNTIME_ENVIRONMENT_NAME = "VMware Tanzu GemFire for VMs";
@Override
protected String getCloudPlatformName() {
return CLOUD_FOUNDRY_NAME;
}
@Override
protected String getRuntimeEnvironmentName() {
return RUNTIME_ENVIRONMENT_NAME;
}
@Override
protected boolean isCloudPlatformActive(@NonNull Environment environment) {
return CloudPlatform.CLOUD_FOUNDRY.isActive(environment);
}
}
public static final class KubernetesClusterAvailableCondition extends AbstractCloudPlatformAvailableCondition {
protected static final String KUBERNETES_NAME = "Kubernetes";
protected static final String RUNTIME_ENVIRONMENT_NAME = "VMware Tanzu GemFire for K8S";
@Override
protected String getCloudPlatformName() {
return KUBERNETES_NAME;
}
@Override
protected String getRuntimeEnvironmentName() {
return RUNTIME_ENVIRONMENT_NAME;
}
@Override
protected boolean isCloudPlatformActive(@NonNull Environment environment) {
return CloudPlatform.KUBERNETES.isActive(environment);
}
}
public static final class StandaloneClusterAvailableCondition
extends ClusterAwareConfiguration.ClusterAwareCondition { }
}

View File

@@ -30,6 +30,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -43,20 +44,24 @@ import org.apache.geode.cache.client.Pool;
import org.apache.geode.cache.client.PoolManager;
import org.apache.geode.cache.server.CacheServer;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportAware;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport;
import org.springframework.data.gemfire.support.ConnectionEndpoint;
import org.springframework.data.gemfire.support.ConnectionEndpointList;
@@ -86,19 +91,21 @@ import org.slf4j.LoggerFactory;
* @see org.apache.geode.cache.client.Pool
* @see org.apache.geode.cache.client.PoolManager
* @see org.apache.geode.cache.server.CacheServer
* @see org.springframework.boot.cloud.CloudPlatform
* @see org.springframework.context.ApplicationListener
* @see org.springframework.context.ConfigurableApplicationContext
* @see org.springframework.context.annotation.Condition
* @see org.springframework.context.annotation.ConditionContext
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.Import
* @see org.springframework.context.annotation.ImportAware
* @see org.springframework.context.event.ContextClosedEvent
* @see org.springframework.core.annotation.AnnotationAttributes
* @see org.springframework.core.env.ConfigurableEnvironment
* @see org.springframework.core.env.EnumerablePropertySource
* @see org.springframework.core.env.Environment
* @see org.springframework.core.env.PropertySource
* @see org.springframework.core.type.AnnotatedTypeMetadata
* @see org.springframework.core.type.AnnotationMetadata
* @see org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport
* @see org.springframework.data.gemfire.support.ConnectionEndpoint
* @see org.springframework.data.gemfire.support.ConnectionEndpointList
@@ -107,9 +114,10 @@ import org.slf4j.LoggerFactory;
*/
@Configuration
@Import({ ClusterAvailableConfiguration.class, ClusterNotAvailableConfiguration.class })
public class ClusterAwareConfiguration extends AbstractAnnotationConfigSupport {
public class ClusterAwareConfiguration extends AbstractAnnotationConfigSupport implements ImportAware {
static final boolean DEFAULT_CLUSTER_CONDITION_MATCH = false;
static final boolean DEFAULT_CLUSTER_CONDITION_STRICT_MATCH = false;
static final int DEFAULT_CACHE_SERVER_PORT = CacheServer.DEFAULT_PORT;
static final int DEFAULT_LOCATOR_PORT = 10334;
@@ -117,17 +125,22 @@ public class ClusterAwareConfiguration extends AbstractAnnotationConfigSupport {
static final ClientRegionShortcut LOCAL_CLIENT_REGION_SHORTCUT = ClientRegionShortcut.LOCAL;
static final Logger logger = LoggerFactory.getLogger(ClusterAwareConfiguration.class);
static final String LOCALHOST = "localhost";
static final String MATCHING_PROPERTY_PATTERN = "spring\\.data\\.gemfire\\.pool\\..*locators|servers";
static final String STRICT_MATCH_ATTRIBUTE_NAME = "strictMatch";
static final String SPRING_BOOT_DATA_GEMFIRE_CLUSTER_CONDITION_MATCH_PROPERTY =
"spring.boot.data.gemfire.cluster.condition.match";
static final String SPRING_BOOT_DATA_GEMFIRE_CLUSTER_CONDITION_MATCH_STRICT_PROPERTY =
"spring.boot.data.gemfire.cluster.condition.match.strict";
static final String SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY =
"spring.data.gemfire.cache.client.region.shortcut";
private static final AtomicBoolean strictMatchConfiguration =
new AtomicBoolean(DEFAULT_CLUSTER_CONDITION_STRICT_MATCH);
private static final Function<ConditionContext, Boolean> configuredMatchFunction = conditionContext ->
Optional.ofNullable(conditionContext)
.map(ConditionContext::getEnvironment)
@@ -135,17 +148,43 @@ public class ClusterAwareConfiguration extends AbstractAnnotationConfigSupport {
Boolean.class, DEFAULT_CLUSTER_CONDITION_MATCH))
.orElse(DEFAULT_CLUSTER_CONDITION_MATCH);
private static final Logger logger = LoggerFactory.getLogger(ClusterAwareConfiguration.class);
/**
* @inheritDoc
*/
@Override
protected Class<? extends Annotation> getAnnotationType() {
protected @NonNull Class<? extends Annotation> getAnnotationType() {
return EnableClusterAware.class;
}
protected boolean isStrictMatchConfigured(@NonNull AnnotationAttributes enableClusterAwareAttributes) {
return enableClusterAwareAttributes != null
&& Boolean.TRUE.equals(enableClusterAwareAttributes.getBoolean(STRICT_MATCH_ATTRIBUTE_NAME));
}
/**
* @inheritDoc
*/
@Override
public void setImportMetadata(@NonNull AnnotationMetadata importMetadata) {
if (isAnnotationPresent(importMetadata)) {
AnnotationAttributes enableClusterAwareAttributes = getAnnotationAttributes(importMetadata);
strictMatchConfiguration.set(isStrictMatchConfigured(enableClusterAwareAttributes));
}
}
@SuppressWarnings("unused")
public static class ClusterAwareCondition implements Condition {
private static final AtomicReference<Boolean> clusterAvailable = new AtomicReference<>(null);
private static ApplicationListener<ContextClosedEvent> clusterAwareConditionResetOnContextClosedApplicationListener() {
protected static final String RUNTIME_ENVIRONMENT_NAME = "Apache Geode-based Cluster on Bare Metal";
private static @NonNull ApplicationListener<ContextClosedEvent> clusterAwareConditionResetOnContextClosedApplicationListener() {
return contextClosedEvent-> reset();
}
@@ -157,20 +196,88 @@ public class ClusterAwareConfiguration extends AbstractAnnotationConfigSupport {
clusterAvailable.set(null);
}
public static boolean wasClusterAvailabilityEvaluated() {
return clusterAvailable.get() != null;
}
/**
* Returns a {@link String} containing a description of the runtime environment.
*
* @return a {@link String} containing a description of the runtime environment.
*/
protected String getRuntimeEnvironmentName() {
return RUNTIME_ENVIRONMENT_NAME;
}
/**
* @inheritDoc
*/
@Override
public synchronized boolean matches(@NonNull ConditionContext conditionContext,
@NonNull AnnotatedTypeMetadata metadata) {
@NonNull AnnotatedTypeMetadata typeMetadata) {
return isMatch(conditionContext) || doCachedMatch(conditionContext);
boolean matches = isMatch(conditionContext) || doCachedMatch(conditionContext);
boolean strictMatch = isStrictMatch(typeMetadata, conditionContext);
failOnStrictMatchAndNoMatches(strictMatch, matches);
return matches;
}
boolean isMatch(@NonNull ConditionContext conditionContext) {
return isAvailable() || configuredMatchFunction.apply(conditionContext);
}
protected boolean isStrictMatch(@NonNull AnnotatedTypeMetadata typeMetadata,
@NonNull ConditionContext conditionContext) {
Environment environment = conditionContext.getEnvironment();
Function<ConfigurableListableBeanFactory, Boolean> isStrictMatchEnabledFunction = beanFactory -> {
boolean strictMatchEnabled = strictMatchConfiguration.get();
if (!strictMatchEnabled) {
String annotationName = EnableClusterAware.class.getName();
strictMatchEnabled = beanFactory != null
&& Arrays.stream(ArrayUtils.nullSafeArray(beanFactory.getBeanDefinitionNames(), String.class))
.map(beanFactory::getBeanDefinition)
.filter(AnnotatedBeanDefinition.class::isInstance)
.map(AnnotatedBeanDefinition.class::cast)
.map(AnnotatedBeanDefinition::getMetadata)
.filter(annotationMetadata -> annotationMetadata.hasAnnotation(annotationName))
.findFirst()
.map(annotationMetadata -> annotationMetadata.getAnnotationAttributes(annotationName))
.map(AnnotationAttributes::fromMap)
.map(annotationAttributes -> annotationAttributes.getBoolean(STRICT_MATCH_ATTRIBUTE_NAME))
.orElse(DEFAULT_CLUSTER_CONDITION_STRICT_MATCH);
}
return strictMatchEnabled;
};
return environment.getProperty(SPRING_BOOT_DATA_GEMFIRE_CLUSTER_CONDITION_MATCH_STRICT_PROPERTY,
Boolean.class, isStrictMatchEnabledFunction.apply(conditionContext.getBeanFactory()));
}
protected boolean isStrictMatchAndNoMatches(boolean strictMatch, boolean matches) {
return strictMatch && !matches;
}
protected void failOnStrictMatchAndNoMatches(boolean strictMatch, boolean matches) {
if (isStrictMatchAndNoMatches(strictMatch, matches)) {
String message =
String.format("Failed to find available cluster in [%1$s] when strictMatch was [%2$s]",
getRuntimeEnvironmentName(), strictMatch);
throw new ClusterNotAvailableException(message);
}
}
/**
* Caches the result of the computed {@link #doMatch(ConditionContext)} operation.
*
@@ -236,7 +343,7 @@ public class ClusterAwareConfiguration extends AbstractAnnotationConfigSupport {
}
boolean isMatch(@NonNull ConnectionEndpointList connectionEndpoints, int connectionCount) {
return connectionCount > 0;
return isConnected(connectionCount);
}
protected @NonNull Logger getLogger() {
@@ -438,39 +545,54 @@ public class ClusterAwareConfiguration extends AbstractAnnotationConfigSupport {
}, cause -> false);
}
protected void configureTopology(@NonNull Environment environment, @NonNull ConnectionEndpointList connectionEndpoints,
int connectionCount) {
protected boolean isConnected(int connectionCount) {
return connectionCount > 0;
}
protected boolean isNotConnected(int connectionCount) {
return !isConnected(connectionCount);
}
protected void configureTopology(@NonNull Environment environment,
@NonNull ConnectionEndpointList connectionEndpoints, int connectionCount) {
if (isConnected(connectionCount)) {
logConnectedRuntimeEnvironment(getLogger(), connectionCount);
}
else {
if (connectionCount < 1) {
if (!environment.containsProperty(SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY)) {
System.setProperty(SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY,
LOCAL_CLIENT_REGION_SHORTCUT.name());
}
if (getLogger().isInfoEnabled()) {
getLogger().info("No cluster found; Spring Boot application is running in standalone [LOCAL] mode");
}
logUnconnectedRuntimeEnvironment(getLogger());
}
else {
if (getLogger().isInfoEnabled()) {
getLogger().info("Cluster was found; Auto-configuration made [{}] successful connection(s);"
+ " Spring Boot application is running in a client/server topology", connectionCount);
}
}
if (getLogger().isInfoEnabled()) {
if (CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) {
getLogger().info("Spring Boot application is running in a client/server topology,"
+ " inside a VMware Tanzu GemFire for VMs environment");
}
else if (CloudPlatform.KUBERNETES.isActive(environment)) {
getLogger().info("Spring Boot application is running in a client/server topology,"
+ " inside a VMware Tanzu GemFire for K8S environment");
}
else {
getLogger().info("Spring Boot application is running in a client/server topology,"
+ " using a standalone Apache Geode-based cluster");
}
}
protected void logConnectedRuntimeEnvironment(@NonNull Logger logger, int connectionCount) {
if (logger.isInfoEnabled()) {
logger.info("Cluster was found; Auto-configuration made [{}] successful connection(s);",
connectionCount);
}
logConnectedRuntimeEnvironment(logger);
}
protected void logConnectedRuntimeEnvironment(@NonNull Logger logger) {
if (logger.isInfoEnabled()) {
logger.info("Spring Boot application is running in a client/server topology,"
+ " using a standalone Apache Geode-based cluster");
}
}
protected void logUnconnectedRuntimeEnvironment(@NonNull Logger logger) {
if (logger.isInfoEnabled()) {
logger.info("No cluster was found; Spring Boot application will run in standalone [LOCAL] mode"
+ " unless strictMode is false and the application is running in a Cloud-managed Environment.");
}
}
}

View File

@@ -47,12 +47,16 @@ import org.springframework.lang.Nullable;
* @see org.springframework.boot.autoconfigure.condition.AllNestedConditions
* @see org.springframework.boot.cloud.CloudPlatform
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Condition
* @see org.springframework.context.annotation.ConditionContext
* @see org.springframework.context.annotation.Conditional
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.core.env.Environment
* @see org.springframework.core.type.AnnotatedTypeMetadata
* @see org.springframework.data.gemfire.client.ClientRegionFactoryBean
* @see org.springframework.data.gemfire.config.annotation.RegionConfigurer
* @see org.springframework.data.gemfire.config.annotation.support.CacheTypeAwareRegionFactoryBean
* @see org.springframework.geode.config.annotation.ClusterAwareConfiguration
* @since 1.2.0
*/
@Configuration
@@ -149,9 +153,9 @@ public class ClusterNotAvailableConfiguration {
@Override
public synchronized boolean matches(@NonNull ConditionContext conditionContext,
@NonNull AnnotatedTypeMetadata metadata) {
@NonNull AnnotatedTypeMetadata typeMetadata) {
return !super.matches(conditionContext, metadata);
return !super.matches(conditionContext, typeMetadata);
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2020 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.geode.config.annotation;
/**
* The {@link ClusterNotAvailableException} is a {@link RuntimeException} indicating that no Apache Geode cluster
* was provisioned and available to service Apache Geode {@link org.apache.geode.cache.client.ClientCache} applications.
*
* @author John Blum
* @see java.lang.RuntimeException
* @see org.apache.geode.cache.client.ClientCache
* @since 1.0.0
*/
@SuppressWarnings("unused")
public class ClusterNotAvailableException extends RuntimeException {
/**
* Constructs a new uninitialized instance of {@link ClusterNotAvailableException}.
*/
public ClusterNotAvailableException() { }
/**
* Constructs a new instance of {@link ClusterNotAvailableException} initialized with
* the given {@link String message} describing the exception.
*
* @param message {@link String} containing a description of the exception.
*/
public ClusterNotAvailableException(String message) {
super(message);
}
/**
* Constructs a new instance of {@link ClusterNotAvailableException} initialized with
* the given {@link Throwable} as the cause of this exception.
*
* @param cause {@link Throwable} indicating the cause of this exception.
*/
public ClusterNotAvailableException(Throwable cause) {
super(cause);
}
/**
* Constructs a new instance of {@link ClusterNotAvailableException} initialized with
* the given {@link String message} describing the exception along with the given {@link Throwable}
* as the cause of this exception.
*
* @param message {@link String} containing a description of the exception.
* @param cause {@link Throwable} indicating the cause of this exception.
*/
public ClusterNotAvailableException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -22,6 +22,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.apache.geode.cache.client.ClientCache;
import org.springframework.context.annotation.Import;
/**
@@ -34,6 +36,7 @@ import org.springframework.context.annotation.Import;
* @see java.lang.annotation.Inherited
* @see java.lang.annotation.Retention
* @see java.lang.annotation.Target
* @see org.apache.geode.cache.client.ClientCache
* @see org.springframework.context.annotation.Import
* @see org.springframework.data.gemfire.config.annotation.EnableClusterConfiguration
* @since 1.2.0
@@ -43,6 +46,35 @@ import org.springframework.context.annotation.Import;
@Inherited
@Documented
@Import(ClusterAwareConfiguration.class)
@SuppressWarnings("unused")
public @interface EnableClusterAware {
/**
* Determines whether the matching algorithm is strict.
*
* This means that at least 1 connection to a cluster of servers (1 or more) must be established
* before the cluster aware logic considers that a cluster actually exists.
*
* Previously, in cloud-managed environments (e.g. VMware Tanzu Application Service (TAS) for VMs, formerly known as
* Pivotal Platform or Pivotal CloudFoundry (PCF), or Kubernetes, known as VMware Tanzu Application Service for K8S)
* it was assumed that a cluster would be provisioned and available, and that the Spring Boot, Apache Geode
* {@link ClientCache} application would connect to the cluster on deployment (push).
*
* However, is entirely possible that users may push Spring Boot, Apache Geode {@link ClientCache} applications
* to a cloud-managed environment where not cluster was provisioned and is available, and user simply want their
* apps to run in local-only mode.
*
* The strict match configuration setting absolutely requires that at least 1 connection must be established. Use
* of this configuration setting also promotes a fail-fast protocol, or at least early detection (when log levels
* are adjusted accordingly) that a cluster is not available.
*
* Use {@literal spring.boot.data.gemfire.cluster.condition.match.strict}
* in Spring Boot {@literal application.properties}.
*
* Defaults to {@literal false}.
*
* @return a boolean value indicating whether strict matching mode is enabled.
*/
boolean strictMatch() default ClusterAwareConfiguration.DEFAULT_CLUSTER_CONDITION_STRICT_MATCH;
}

View File

@@ -37,7 +37,7 @@ import org.springframework.data.gemfire.tests.mock.annotation.EnableGemFireMockO
import org.springframework.test.context.junit4.SpringRunner;
/**
* Integration Tests for {@link EnableClusterAware} and {@link ClusterAvailableConfiguration.ClusterAvailableCondition}
* Integration Tests for {@link EnableClusterAware} and {@link ClusterAvailableConfiguration.StandaloneClusterAvailableCondition}
* specifically when the Spring Boot application is run in CloudFoundry.
*
* @author John Blum

View File

@@ -118,6 +118,7 @@ public class ClusterAwareConfigurationUnitTests extends IntegrationTestsSupport
doReturn(false).when(this.condition).isMatch(eq(mockConditionContext));
doReturn(true).when(this.condition).doCachedMatch(eq(mockConditionContext));
doReturn(false).when(this.condition).isStrictMatch(any(), eq(mockConditionContext));
assertThat(this.condition.matches(mockConditionContext, null)).isTrue();
@@ -125,6 +126,7 @@ public class ClusterAwareConfigurationUnitTests extends IntegrationTestsSupport
order.verify(this.condition, times(1)).isMatch(eq(mockConditionContext));
order.verify(this.condition, times(1)).doCachedMatch(eq(mockConditionContext));
order.verify(this.condition, times(1)).isStrictMatch(any(), eq(mockConditionContext));
verifyNoInteractions(mockConditionContext);
}
@@ -135,6 +137,7 @@ public class ClusterAwareConfigurationUnitTests extends IntegrationTestsSupport
ConditionContext mockConditionContext = mock(ConditionContext.class);
doReturn(true).when(this.condition).isMatch(eq(mockConditionContext));
doReturn(false).when(this.condition).isStrictMatch(any(), eq(mockConditionContext));
assertThat(this.condition.matches(mockConditionContext, null)).isTrue();
@@ -142,6 +145,7 @@ public class ClusterAwareConfigurationUnitTests extends IntegrationTestsSupport
order.verify(this.condition, times(1)).isMatch(eq(mockConditionContext));
order.verify(this.condition, never()).doCachedMatch(any());
order.verify(this.condition, times(1)).isStrictMatch(any(), eq(mockConditionContext));
verifyNoInteractions(mockConditionContext);
}
@@ -237,7 +241,7 @@ public class ClusterAwareConfigurationUnitTests extends IntegrationTestsSupport
verify(this.condition, atLeastOnce()).getLogger();
verifyNoMoreInteractions(this.condition, mockConditionContext);
verifyNoMoreInteractions(mockConditionContext);
verifyNoInteractions(mockConnectionEndpointList, mockEnvironment);
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2020 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.geode.config.annotation;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Properties;
import org.junit.AfterClass;
import org.junit.Test;
import org.apache.geode.cache.client.ClientCache;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.data.gemfire.config.annotation.ClientCacheApplication;
import org.springframework.data.gemfire.tests.integration.SpringBootApplicationIntegrationTestsSupport;
import org.springframework.data.gemfire.tests.mock.annotation.EnableGemFireMockObjects;
/**
* Integration Tests for {@link EnableClusterAware} and {@link ClusterAwareConfiguration}
* as well as {@link ClusterAvailableConfiguration} when {@code strictMode} is {@literal true}
* configured in Spring Boot {@literal application.properties}
* using the {@literal spring.boot.data.gemfire.cluster.condition.match.strict} property
* when no Apache Geode Cluster was provisioned and made available to service Apache Geode
* {@link ClientCache clients}.
*
* @author John Blum
* @see java.util.Properties
* @see org.junit.Test
* @see org.apache.geode.cache.client.ClientCache
* @see org.springframework.boot.builder.SpringApplicationBuilder
* @see org.springframework.data.gemfire.config.annotation.ClientCacheApplication
* @see org.springframework.data.gemfire.tests.integration.SpringBootApplicationIntegrationTestsSupport
* @see org.springframework.data.gemfire.tests.mock.annotation.EnableGemFireMockObjects
* @see org.springframework.geode.config.annotation.EnableClusterAware
* @since 1.4.1
*/
public class PropertyConfiguredStrictMatchingClusterNotAvailableConfigurationIntegrationTests
extends SpringBootApplicationIntegrationTestsSupport {
@AfterClass
public static void tearDown() {
ClusterAwareConfiguration.ClusterAwareCondition.reset();
}
@Override
protected SpringApplicationBuilder processBeforeBuild(SpringApplicationBuilder springApplicationBuilder) {
Properties testProperties = new Properties();
testProperties
.setProperty(ClusterAwareConfiguration.SPRING_BOOT_DATA_GEMFIRE_CLUSTER_CONDITION_MATCH_STRICT_PROPERTY,
Boolean.TRUE.toString());
return springApplicationBuilder.properties(testProperties);
}
@Test(expected = ClusterNotAvailableException.class)
public void clusterNotAvailableExceptionThrownWhenClusterIsNotAvailableAndStrictMatchIsTrue() throws Throwable {
try {
newApplicationContext(TestGeodeClientConfiguration.class);
}
catch (Throwable expected) {
expected = NestedExceptionUtils.getMostSpecificCause(expected);
assertThat(expected).isInstanceOf(ClusterNotAvailableException.class);
assertThat(expected).hasMessage("Failed to find available cluster in [%s] when strictMatch was [true]",
ClusterAwareConfiguration.ClusterAwareCondition.RUNTIME_ENVIRONMENT_NAME);
assertThat(expected).hasNoCause();
throw expected;
}
}
@ClientCacheApplication
@EnableClusterAware(strictMatch = false)
@EnableGemFireMockObjects
@SuppressWarnings("all")
static class TestGeodeClientConfiguration { }
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2020 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.geode.config.annotation;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.AfterClass;
import org.junit.Test;
import org.apache.geode.cache.client.ClientCache;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.data.gemfire.config.annotation.ClientCacheApplication;
import org.springframework.data.gemfire.tests.integration.SpringBootApplicationIntegrationTestsSupport;
import org.springframework.data.gemfire.tests.mock.annotation.EnableGemFireMockObjects;
/**
* Integration Tests for {@link EnableClusterAware} and {@link ClusterAwareConfiguration}
* as well as {@link ClusterAvailableConfiguration} when {@code strictMode} is {@literal true}
* using the {@link EnableClusterAware#strictMatch()} annotation attribute when no Apache Geode Cluster
* was provisioned and made available to service Apache Geode {@link ClientCache clients}.
*
* @author John Blum
* @see org.junit.Test
* @see org.apache.geode.cache.client.ClientCache
* @see org.springframework.data.gemfire.config.annotation.ClientCacheApplication
* @see org.springframework.data.gemfire.tests.integration.SpringBootApplicationIntegrationTestsSupport
* @see org.springframework.data.gemfire.tests.mock.annotation.EnableGemFireMockObjects
* @see org.springframework.geode.config.annotation.EnableClusterAware
* @since 1.4.1
*/
public class StrictMatchingClusterNotAvailableConfigurationIntegrationTests
extends SpringBootApplicationIntegrationTestsSupport {
@AfterClass
public static void tearDown() {
ClusterAwareConfiguration.ClusterAwareCondition.reset();
}
@Test(expected = ClusterNotAvailableException.class)
public void clusterNotAvailableExceptionThrownWhenClusterIsNotAvailableAndStrictMatchIsTrue() throws Throwable {
try {
newApplicationContext(TestGeodeClientConfiguration.class);
}
catch (Throwable expected) {
expected = NestedExceptionUtils.getMostSpecificCause(expected);
assertThat(expected).isInstanceOf(ClusterNotAvailableException.class);
assertThat(expected).hasMessage("Failed to find available cluster in [%s] when strictMatch was [true]",
ClusterAwareConfiguration.ClusterAwareCondition.RUNTIME_ENVIRONMENT_NAME);
assertThat(expected).hasNoCause();
throw expected;
}
}
@ClientCacheApplication
@EnableGemFireMockObjects
@EnableClusterAware(strictMatch = true)
static class TestGeodeClientConfiguration { }
}