Add @EnableClusterAware annotation configuration support allowing developers to seamlessly switch between local and client/server development environments.

Resolves gh-52.
This commit is contained in:
John Blum
2019-08-30 16:47:59 -07:00
parent 9bb6f6b0ea
commit c1d7424de1
8 changed files with 1064 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package org.springframework.geode.config.annotation;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.gemfire.config.annotation.EnableClusterConfiguration;
/**
* The {@link ClusterAvailableConfiguration} class is a Spring {@link Configuration @Configuration} class that enables
* configuration when an Apache Geode or Pivotal GemFire cluster of servers are available.
*
* @author John Blum
* @see org.springframework.context.annotation.Conditional
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.data.gemfire.config.annotation.EnableClusterConfiguration
* @since 1.2.0
*/
@Configuration
@Conditional(ClusterAvailableConfiguration.ClusterAvailableCondition.class)
@EnableClusterConfiguration(requireHttps = false, useHttp = true)
class ClusterAvailableConfiguration {
static final class ClusterAvailableCondition extends ClusterAwareConfiguration.ClusterAwareCondition { }
}

View File

@@ -0,0 +1,263 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package org.springframework.geode.config.annotation;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.apache.geode.cache.server.CacheServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.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.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport;
import org.springframework.data.gemfire.support.ConnectionEndpoint;
import org.springframework.data.gemfire.support.ConnectionEndpointList;
import org.springframework.data.gemfire.util.ArrayUtils;
import org.springframework.geode.core.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* The {@link ClusterAwareConfiguration} class is a Spring {@link Configuration @Configuration} class imported by
* {@link EnableClusterAware} used to determine whether a Spring Boot application using Apache Geode should run
* in {@literal local-only mode} or {@literal client/server}.
*
* @author John Blum
* @see java.lang.annotation.Annotation
* @see java.net.Socket
* @see java.net.SocketAddress
* @see org.apache.geode.cache.server.CacheServer
* @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.core.env.ConfigurableEnvironment
* @see org.springframework.core.env.Environment
* @see org.springframework.core.env.PropertySource
* @see org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport
* @since 1.2.0
*/
@Configuration
@Import({ ClusterAvailableConfiguration.class, ClusterNotAvailableConfiguration.class })
public class ClusterAwareConfiguration extends AbstractAnnotationConfigSupport {
static final int DEFAULT_CACHE_SERVER_PORT = CacheServer.DEFAULT_PORT;
static final int DEFAULT_LOCATOR_PORT = 10334;
static final int DEFAULT_TIMEOUT_IN_MILLISECONDS = 500;
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 SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY =
"spring.data.gemfire.cache.client.region.shortcut";
@Override
protected Class<? extends Annotation> getAnnotationType() {
return EnableClusterAware.class;
}
@SuppressWarnings("unused")
static class ClusterAwareCondition implements Condition {
private static final AtomicReference<Boolean> clusterAvailable = new AtomicReference<>(null);
@Override
public synchronized boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
if (clusterAvailable.get() == null) {
Environment environment = context.getEnvironment();
ConnectionEndpointList connectionEndpoints = new ConnectionEndpointList(getDefaultConnectionEndpoints())
.add(getConfiguredConnectionEndpoints(environment));
int connectionCount = countConnections(connectionEndpoints);
configureTopology(environment, connectionEndpoints, connectionCount);
clusterAvailable.set(isMatch(connectionEndpoints, connectionCount));
}
return Boolean.TRUE.equals(clusterAvailable.get());
}
List<ConnectionEndpoint> getDefaultConnectionEndpoints() {
return Arrays.asList(
new ConnectionEndpoint(LOCALHOST, DEFAULT_CACHE_SERVER_PORT),
new ConnectionEndpoint(LOCALHOST, DEFAULT_LOCATOR_PORT)
);
}
List<ConnectionEndpoint> getConfiguredConnectionEndpoints(Environment environment) {
List<ConnectionEndpoint> connectionEndpoints = new ArrayList<>();
if (environment instanceof ConfigurableEnvironment) {
ConfigurableEnvironment configurableEnvironment = (ConfigurableEnvironment) environment;
MutablePropertySources propertySources = configurableEnvironment.getPropertySources();
if (propertySources != null) {
Pattern pattern = Pattern.compile(MATCHING_PROPERTY_PATTERN);
for (PropertySource propertySource : propertySources) {
if (propertySource instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> enumerablePropertySource =
(EnumerablePropertySource<?>) propertySource;
String[] propertyNames = enumerablePropertySource.getPropertyNames();
Arrays.stream(ArrayUtils.nullSafeArray(propertyNames, String.class))
.filter(propertyName-> pattern.matcher(propertyName).find())
.forEach(propertyName -> {
String propertyValue = environment.getProperty(propertyName);
if (StringUtils.hasText(propertyValue)) {
int defaultPort = propertyName.contains("servers")
? DEFAULT_CACHE_SERVER_PORT
: DEFAULT_LOCATOR_PORT;
String[] propertyValueArray = trim(propertyValue.split(","));
ConnectionEndpointList list =
ConnectionEndpointList.parse(defaultPort, propertyValueArray);
connectionEndpoints.addAll(list);
}
});
}
}
}
}
return connectionEndpoints;
}
// TODO: remove when DATAGEODE-228, a BUG in SDG, is fixed!
private String[] trim(String[] array) {
array = ArrayUtils.nullSafeArray(array, String.class);
for (int index = 0; index < array.length; index++) {
array[index] = StringUtils.trimAllWhitespace(array[index]);
}
return array;
}
Logger getLogger() {
return logger;
}
private boolean close(Socket socket) {
return ObjectUtils.<Boolean>doOperationSafely(() -> {
if (socket != null) {
socket.close();
return true;
}
return false;
}, cause -> false);
}
int countConnections(ConnectionEndpointList connectionEndpoints) {
int count = 0;
for (ConnectionEndpoint connectionEndpoint : connectionEndpoints) {
Socket socket = null;
try {
socket = connect(connectionEndpoint);
count++;
}
catch (IOException cause) {
if (getLogger().isInfoEnabled()) {
getLogger().info("Failed to connect to {}", connectionEndpoint);
}
if (getLogger().isDebugEnabled()) {
getLogger().debug("Connection failure caused by:", cause);
}
}
finally {
close(socket);
}
}
return count;
}
Socket connect(ConnectionEndpoint connectionEndpoint) throws IOException {
SocketAddress socketAddress =
new InetSocketAddress(connectionEndpoint.getHost(), connectionEndpoint.getPort());
Socket socket = new Socket();
socket.connect(socketAddress, DEFAULT_TIMEOUT_IN_MILLISECONDS);
return socket;
}
void configureTopology(Environment environment, ConnectionEndpointList connectionEndpoints,
int connectionCount) {
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());
}
}
}
boolean isMatch(ConnectionEndpointList connectionEndpoints, int connectionCount) {
return connectionCount > 0;
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package org.springframework.geode.config.annotation;
import static org.springframework.geode.config.annotation.ClusterAwareConfiguration.LOCAL_CLIENT_REGION_SHORTCUT;
import static org.springframework.geode.config.annotation.ClusterAwareConfiguration.SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
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.client.ClientRegionFactoryBean;
import org.springframework.data.gemfire.config.annotation.RegionConfigurer;
import org.springframework.lang.Nullable;
/**
* The {@link ClusterNotAvailableConfiguration} class is a Spring {@link Configuration @Configuration} class that
* enables configuration when an Apache Geode or Pivotal GemFire cluster of servers is not available.
*
* @author John Blum
* @see org.springframework.beans.factory.config.BeanPostProcessor
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Conditional
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.core.env.Environment
* @see org.springframework.data.gemfire.client.ClientRegionFactoryBean
* @see org.springframework.data.gemfire.config.annotation.RegionConfigurer
* @since 1.2.0
*/
@Configuration
@Conditional(ClusterNotAvailableConfiguration.CusterNotAvailableCondition.class)
@SuppressWarnings("unused")
public class ClusterNotAvailableConfiguration {
@Bean
BeanPostProcessor localClientRegionBeanPostProcessor(Environment environment) {
return new BeanPostProcessor() {
@Nullable @Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ClientRegionFactoryBean) {
ClientRegionFactoryBean<?, ?> clientRegion = (ClientRegionFactoryBean<?, ?>) bean;
configureAsLocalClientRegion(environment, clientRegion);
}
return bean;
}
};
}
@Bean
@SuppressWarnings("unused")
RegionConfigurer localClientRegionConfigurer(Environment environment) {
return new RegionConfigurer() {
@Override
public void configure(String beanName, ClientRegionFactoryBean<?, ?> bean) {
configureAsLocalClientRegion(environment, bean);
}
};
}
protected <K, V> ClientRegionFactoryBean configureAsLocalClientRegion(Environment environment,
ClientRegionFactoryBean<K, V> clientRegion) {
ClientRegionShortcut shortcut =
environment.getProperty(SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY,
ClientRegionShortcut.class, LOCAL_CLIENT_REGION_SHORTCUT);
clientRegion.setPoolName(null);
clientRegion.setShortcut(shortcut);
return clientRegion;
}
static final class CusterNotAvailableCondition extends ClusterAwareConfiguration.ClusterAwareCondition {
@Override
public synchronized boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return !super.matches(context, metadata);
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package org.springframework.geode.config.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
/**
* The {@link EnableClusterAware} helps an Spring Boot application using Apache Geode (or Pivotal GemFire
* / Pivotal Cloud Cache (PCC)) decide whether it needs to operate in {@literal local-only mode}
* or {@literal client/server}.
*
* @author John Blum
* @see java.lang.annotation.Annotation
* @see java.lang.annotation.Documented
* @see java.lang.annotation.Inherited
* @see java.lang.annotation.Retention
* @see java.lang.annotation.Target
* @see org.springframework.context.annotation.Import
* @see org.springframework.data.gemfire.config.annotation.EnableClusterConfiguration
* @since 1.2.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import(ClusterAwareConfiguration.class)
public @interface EnableClusterAware {
}