diff --git a/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterAvailableConfiguration.java b/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterAvailableConfiguration.java index 0eda1ceb..dcfcd6e7 100644 --- a/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterAvailableConfiguration.java +++ b/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterAvailableConfiguration.java @@ -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 { } } diff --git a/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterAwareConfiguration.java b/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterAwareConfiguration.java index 38e3cede..88a0b096 100644 --- a/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterAwareConfiguration.java +++ b/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterAwareConfiguration.java @@ -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 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 getAnnotationType() { + protected @NonNull Class 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 clusterAvailable = new AtomicReference<>(null); - private static ApplicationListener clusterAwareConditionResetOnContextClosedApplicationListener() { + protected static final String RUNTIME_ENVIRONMENT_NAME = "Apache Geode-based Cluster on Bare Metal"; + + private static @NonNull ApplicationListener 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 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."); } } } diff --git a/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterNotAvailableConfiguration.java b/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterNotAvailableConfiguration.java index 685bd327..00af3a25 100644 --- a/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterNotAvailableConfiguration.java +++ b/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterNotAvailableConfiguration.java @@ -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); } } diff --git a/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterNotAvailableException.java b/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterNotAvailableException.java new file mode 100644 index 00000000..45011e89 --- /dev/null +++ b/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterNotAvailableException.java @@ -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); + } +} diff --git a/spring-geode/src/main/java/org/springframework/geode/config/annotation/EnableClusterAware.java b/spring-geode/src/main/java/org/springframework/geode/config/annotation/EnableClusterAware.java index 4ddb70ce..f6da96d3 100644 --- a/spring-geode/src/main/java/org/springframework/geode/config/annotation/EnableClusterAware.java +++ b/spring-geode/src/main/java/org/springframework/geode/config/annotation/EnableClusterAware.java @@ -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; + } diff --git a/spring-geode/src/test/java/org/springframework/geode/config/annotation/CloudFoundryClusterAvailableConfigurationIntegrationTests.java b/spring-geode/src/test/java/org/springframework/geode/config/annotation/CloudFoundryClusterAvailableConfigurationIntegrationTests.java index 803142bb..c237e547 100644 --- a/spring-geode/src/test/java/org/springframework/geode/config/annotation/CloudFoundryClusterAvailableConfigurationIntegrationTests.java +++ b/spring-geode/src/test/java/org/springframework/geode/config/annotation/CloudFoundryClusterAvailableConfigurationIntegrationTests.java @@ -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 diff --git a/spring-geode/src/test/java/org/springframework/geode/config/annotation/ClusterAwareConfigurationUnitTests.java b/spring-geode/src/test/java/org/springframework/geode/config/annotation/ClusterAwareConfigurationUnitTests.java index 93b7c4cd..a6ffa7e7 100644 --- a/spring-geode/src/test/java/org/springframework/geode/config/annotation/ClusterAwareConfigurationUnitTests.java +++ b/spring-geode/src/test/java/org/springframework/geode/config/annotation/ClusterAwareConfigurationUnitTests.java @@ -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); } diff --git a/spring-geode/src/test/java/org/springframework/geode/config/annotation/PropertyConfiguredStrictMatchingClusterNotAvailableConfigurationIntegrationTests.java b/spring-geode/src/test/java/org/springframework/geode/config/annotation/PropertyConfiguredStrictMatchingClusterNotAvailableConfigurationIntegrationTests.java new file mode 100644 index 00000000..a7980710 --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/config/annotation/PropertyConfiguredStrictMatchingClusterNotAvailableConfigurationIntegrationTests.java @@ -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 { } + +} diff --git a/spring-geode/src/test/java/org/springframework/geode/config/annotation/StrictMatchingClusterNotAvailableConfigurationIntegrationTests.java b/spring-geode/src/test/java/org/springframework/geode/config/annotation/StrictMatchingClusterNotAvailableConfigurationIntegrationTests.java new file mode 100644 index 00000000..add974fe --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/config/annotation/StrictMatchingClusterNotAvailableConfigurationIntegrationTests.java @@ -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 { } + +}