From 4090cf4ebd7e7c0305f6b5ae75958e7c6af8b6c9 Mon Sep 17 00:00:00 2001 From: John Blum Date: Fri, 15 Jan 2021 19:32:44 -0800 Subject: [PATCH] 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. --- .../ClusterAvailableConfiguration.java | 123 ++++++++++- .../annotation/ClusterAwareConfiguration.java | 192 ++++++++++++++---- .../ClusterNotAvailableConfiguration.java | 8 +- .../ClusterNotAvailableException.java | 66 ++++++ .../config/annotation/EnableClusterAware.java | 32 +++ ...vailableConfigurationIntegrationTests.java | 2 +- .../ClusterAwareConfigurationUnitTests.java | 6 +- ...vailableConfigurationIntegrationTests.java | 99 +++++++++ ...vailableConfigurationIntegrationTests.java | 79 +++++++ 9 files changed, 560 insertions(+), 47 deletions(-) create mode 100644 spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterNotAvailableException.java create mode 100644 spring-geode/src/test/java/org/springframework/geode/config/annotation/PropertyConfiguredStrictMatchingClusterNotAvailableConfigurationIntegrationTests.java create mode 100644 spring-geode/src/test/java/org/springframework/geode/config/annotation/StrictMatchingClusterNotAvailableConfigurationIntegrationTests.java 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 { } + +}