WIP on loadbalancer stuff

- Added caching for stickyrule to work
- Adding polling conditions for flickering test
- Added better exception handling
- Removed wiremock tests, did some refactoring
- Added default value of LoadBalancer for a dependency
- Running build in the travis container, added caching, bumped up Groovy to 2.4.4
This commit is contained in:
Marcin Grzejszczak
2015-08-02 21:44:21 +02:00
committed by Spencer Gibb
parent d9d3976650
commit 85a139f60f
30 changed files with 590 additions and 117 deletions

View File

@@ -1,3 +1,9 @@
sudo: false
cache:
directories:
- $HOME/.m2
language: java
before_install:
- git config user.name "$GIT_NAME"
@@ -10,8 +16,8 @@ install:
- mvn --settings .settings.xml install -P docs -q -U -DskipTests=true -Dmaven.test.redirectTestOutputToFile=true
- ./docs/src/main/asciidoc/ghpages.sh
script:
- '[ "${TRAVIS_PULL_REQUEST}" != "false" ] || mvn --settings .settings.xml deploy -nsu -Dmaven.test.redirectTestOutputToFile=true'
- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] || mvn --settings .settings.xml install -nsu -Dmaven.test.redirectTestOutputToFile=true'
- '[ "${TRAVIS_PULL_REQUEST}" != "false" ] || mvn --settings .settings.xml clean deploy -nsu'
- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] || mvn --settings .settings.xml clean install -nsu'
env:
global:
- GIT_NAME="Spencer Gibb"

View File

@@ -224,7 +224,7 @@
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.3</version>
<version>2.4.4</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -1,4 +1,4 @@
package org.springframework.cloud.zookeeper.config;
package org.springframework.cloud.zookeeper.common;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.zookeeper.ZookeeperAutoConfiguration;

View File

@@ -1,4 +1,4 @@
package org.springframework.cloud.zookeeper.config;
package org.springframework.cloud.zookeeper.common;
import java.nio.charset.Charset;
import java.util.ArrayList;

View File

@@ -1,4 +1,4 @@
package org.springframework.cloud.zookeeper.config;
package org.springframework.cloud.zookeeper.common;
import java.util.ArrayList;
import java.util.Arrays;

View File

@@ -0,0 +1,17 @@
package org.springframework.cloud.zookeeper.discovery;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Wrapper annotation to enable Ribbon for Zookeeper
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@ConditionalOnProperty(value = "ribbon.zookeeper.enabled", matchIfMissing = true)
public @interface ConditionalOnRibbonZookeeper {
}

View File

@@ -18,7 +18,6 @@ package org.springframework.cloud.zookeeper.discovery;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
@@ -31,7 +30,7 @@ import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties
@ConditionalOnBean(SpringClientFactory.class)
@ConditionalOnProperty(value = "ribbon.zookeeper.enabled", matchIfMissing = true)
@ConditionalOnRibbonZookeeper
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@RibbonClients(defaultConfiguration = ZookeeperRibbonClientConfiguration.class)
public class RibbonZookeeperAutoConfiguration {

View File

@@ -20,15 +20,15 @@ import com.netflix.client.config.IClientConfig;
import com.netflix.config.ConfigurationManager;
import com.netflix.config.DynamicPropertyFactory;
import com.netflix.config.DynamicStringProperty;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.ServerList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.zookeeper.discovery.dependency.DependenciesPassedCondition;
import org.springframework.cloud.zookeeper.discovery.dependency.ConditionalOnDependenciesPassed;
import org.springframework.cloud.zookeeper.discovery.dependency.DependenciesBasedLoadBalancer;
import org.springframework.cloud.zookeeper.discovery.dependency.ZookeeperDependencies;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@@ -37,7 +37,7 @@ import static com.netflix.client.config.CommonClientConfigKey.DeploymentContextB
import static com.netflix.client.config.CommonClientConfigKey.EnableZoneAffinity;
/**
* Preprocessor that configures defaults for eureka-discovered ribbon clients. Such as:
* Preprocessor that configures defaults for zookeeper-discovered ribbon clients. Such as:
* <code>@zone</code>, NIWSServerListClassName, DeploymentContextBasedVipAddresses,
* NFLoadBalancerRuleClassName, NIWSServerListFilterClassName and more
*
@@ -61,14 +61,20 @@ public class ZookeeperRibbonClientConfiguration {
@Bean
@ConditionalOnMissingBean
@Conditional(DependenciesPassedCondition.class)
@ConditionalOnProperty(value = "spring.cloud.zookeeper.dependencies.enabled", matchIfMissing = true)
@ConditionalOnDependenciesPassed
public ServerList<?> ribbonServerListFromDependencies(IClientConfig config, ZookeeperDependencies zookeeperDependencies) {
ZookeeperServerList serverList = new ZookeeperServerList(serviceDiscovery.getServiceDiscovery());
serverList.initFromDependencies(config, zookeeperDependencies);
return serverList;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnDependenciesPassed
public ILoadBalancer dependenciesBasedLoadBalancer(ZookeeperDependencies zookeeperDependencies, ServerList serverList) {
return new DependenciesBasedLoadBalancer(zookeeperDependencies, serverList);
}
@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(IClientConfig config) {

View File

@@ -0,0 +1,21 @@
package org.springframework.cloud.zookeeper.discovery.dependency;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Conditional;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to turn on a feature if Zookeeper dependencies have been passed
*
* @author Marcin Grzejszczak, 4financeIT
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(DependenciesPassedCondition.class)
@ConditionalOnProperty(value = "spring.cloud.zookeeper.dependencies.enabled", matchIfMissing = true)
public @interface ConditionalOnDependenciesPassed {
}

View File

@@ -0,0 +1,75 @@
package org.springframework.cloud.zookeeper.discovery.dependency;
import com.netflix.loadbalancer.*;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* LoadBalancer that delegates to other rules depending on the provided load balancing strategy
* in the {@link ZookeeperDependencies.ZookeeperDependency#getLoadBalancerType()}
*
* @author Marcin Grzejszczak, 4financeIT
*/
@Slf4j
public class DependenciesBasedLoadBalancer extends BaseLoadBalancer {
private final Map<String, IRule> ruleCache = new ConcurrentHashMap<>();
private final ZookeeperDependencies zookeeperDependencies;
public DependenciesBasedLoadBalancer(ZookeeperDependencies zookeeperDependencies, ServerList serverList) {
this.zookeeperDependencies = zookeeperDependencies;
setServersList(serverList.getInitialListOfServers());
}
@Override
public Server chooseServer(Object key) {
String keyAsString = (String) key;
ZookeeperDependencies.ZookeeperDependency dependency = zookeeperDependencies.getDependencyForAlias(keyAsString);
if (dependency == null) {
log.debug("No dependency found for alias [{}] - will use the default rule which is [{}]", keyAsString, rule);
return rule.choose(key);
};
cacheEntryIfMissing(keyAsString, dependency);
log.debug("Will try to retrieve dependency for key [{}]. Current cache contents [{}]", keyAsString, ruleCache);
return ruleCache.get(keyAsString).choose(key);
}
private void cacheEntryIfMissing(String keyAsString, ZookeeperDependencies.ZookeeperDependency dependency) {
if (!ruleCache.containsKey(keyAsString)) {
log.debug("Cache doesn't contain entry for [{}]", keyAsString);
ruleCache.put(keyAsString, chooseRuleForLoadBalancerType(dependency.getLoadBalancerType()));
}
}
private IRule chooseRuleForLoadBalancerType(LoadBalancerType type) {
switch (type) {
case ROUND_ROBIN:
return getRoundRobinRule();
case RANDOM:
return getRandomRule();
case STICKY:
return getStickyRule();
default:
throw new IllegalArgumentException("Unknown load balancer type " + type);
}
}
private RoundRobinRule getRoundRobinRule() {
return new RoundRobinRule(this);
}
private IRule getRandomRule() {
RandomRule randomRule = new RandomRule();
randomRule.setLoadBalancer(this);
return randomRule;
}
private IRule getStickyRule() {
StickyRule stickyRule = new StickyRule(getRoundRobinRule());
stickyRule.setLoadBalancer(this);
return stickyRule;
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2013-2015 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
*
* http://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.cloud.zookeeper.discovery.dependency;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.zookeeper.discovery.ConditionalOnRibbonZookeeper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
*
* Provides LoadBalancerClient that at runtime can pick proper load balancing strategy
* basing on the Zookeeper dependencies from properties
*
* @author Marcin Grzejszczak, 4financeIT
*/
@AutoConfigureBefore(RibbonAutoConfiguration.class)
@ConditionalOnRibbonZookeeper
@Configuration
@Slf4j
public class DependencyRibbonAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnDependenciesPassed
public LoadBalancerClient loadBalancerClient(SpringClientFactory springClientFactory) {
return new RibbonLoadBalancerClient(springClientFactory) {
@Override
protected Server getServer(String serviceId) {
ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
return loadBalancer == null ? null : chooseServerByServiceIdOrDefault(loadBalancer, serviceId);
}
private Server chooseServerByServiceIdOrDefault(ILoadBalancer loadBalancer, String serviceId) {
log.debug("Dependencies are set - will try to load balance via provided load balancer [{}] for key [{}]", loadBalancer, serviceId);
Server server = loadBalancer.chooseServer(serviceId);
log.debug("Retrieved server [{}] via load balancer", server);
return server != null ? server : loadBalancer.chooseServer("default");
}
};
}
}

View File

@@ -0,0 +1,64 @@
package org.springframework.cloud.zookeeper.discovery.dependency;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* Load balancing rule that returns always the same instance.
*
* Ported from {@link org.apache.curator.x.discovery.strategies.StickyStrategy}
*
* author: Marcin Grzejszczak, 4financeIT
*/
@Slf4j
public class StickyRule extends AbstractLoadBalancerRule {
private final IRule masterStrategy;
private final AtomicReference<Server> ourInstance = new AtomicReference<>(null);
private final AtomicInteger instanceNumber = new AtomicInteger(-1);
public StickyRule(IRule masterStrategy) {
this.masterStrategy = masterStrategy;
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
@Override
public Server choose(Object key) {
final List<Server> instances = getLoadBalancer().getServerList(true);
log.debug("Instances taken from load balancer {}", instances);
Server localOurInstance = ourInstance.get();
log.debug("Current saved instance [{}]", localOurInstance);
if (!instances.contains(localOurInstance)) {
ourInstance.compareAndSet(localOurInstance, null);
}
if (ourInstance.get() == null) {
Server instance = masterStrategy.choose(key);
if (ourInstance.compareAndSet(null, instance)) {
instanceNumber.incrementAndGet();
}
}
return ourInstance.get();
}
/**
* Each time a new instance is picked, an internal counter is incremented. This way you
* can track when/if the instance changes. The instance can change when the selected instance
* is not in the current list of instances returned by the instance provider
*
* @return instance number
*/
public int getInstanceNumber() {
return instanceNumber.get();
}
}

View File

@@ -56,7 +56,7 @@ public class ZookeeperDependencies {
private String path;
private LoadBalancerType loadBalancerType;
private LoadBalancerType loadBalancerType = LoadBalancerType.ROUND_ROBIN;
private String contentTypeTemplate;
@@ -65,6 +65,7 @@ public class ZookeeperDependencies {
private Map<String, String> headers;
private boolean required;
}
public Collection<ZookeeperDependency> getDependencyConfigurations() {
@@ -75,6 +76,15 @@ public class ZookeeperDependencies {
return !dependencies.isEmpty();
}
public ZookeeperDependency getDependencyForPath(final String path) {
for (Map.Entry<String, ZookeeperDependency> zookeeperDependencyEntry : dependencies.entrySet()) {
if (zookeeperDependencyEntry.getValue().getPath().equals(path)) {
return zookeeperDependencyEntry.getValue();
}
}
return null;
}
public ZookeeperDependency getDependencyForAlias(final String alias) {
for (Map.Entry<String, ZookeeperDependency> zookeeperDependencyEntry : dependencies.entrySet()) {
if (zookeeperDependencyEntry.getKey().equals(alias)) {

View File

@@ -17,11 +17,9 @@ package org.springframework.cloud.zookeeper.discovery.dependency;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.zookeeper.ZookeeperAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
/**
@@ -34,8 +32,7 @@ import org.springframework.context.annotation.Configuration;
*/
@Configuration
@EnableConfigurationProperties
@Conditional(DependenciesPassedCondition.class)
@ConditionalOnProperty(value = "spring.cloud.zookeeper.dependencies.enabled", matchIfMissing = true)
@ConditionalOnDependenciesPassed
@AutoConfigureAfter(ZookeeperAutoConfiguration.class)
public class ZookeeperDependenciesAutoConfiguration {

View File

@@ -15,24 +15,22 @@
*/
package org.springframework.cloud.zookeeper.discovery.watcher;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.zookeeper.discovery.ZookeeperServiceDiscovery;
import org.springframework.cloud.zookeeper.discovery.dependency.DependenciesPassedCondition;
import org.springframework.cloud.zookeeper.discovery.dependency.ConditionalOnDependenciesPassed;
import org.springframework.cloud.zookeeper.discovery.dependency.ZookeeperDependencies;
import org.springframework.cloud.zookeeper.discovery.dependency.ZookeeperDependenciesAutoConfiguration;
import org.springframework.cloud.zookeeper.discovery.watcher.presence.DefaultDependencyPresenceOnStartupVerifier;
import org.springframework.cloud.zookeeper.discovery.watcher.presence.DependencyPresenceOnStartupVerifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
/**
* Provides hooks for observing dependency lifecycle in Zookeeper.
* Needs custom dependencies to be set in order to work.
@@ -43,8 +41,7 @@ import org.springframework.context.annotation.Configuration;
*/
@Configuration
@EnableConfigurationProperties
@Conditional(DependenciesPassedCondition.class)
@ConditionalOnProperty(value = "spring.cloud.zookeeper.dependencies.enabled", matchIfMissing = true)
@ConditionalOnDependenciesPassed
@AutoConfigureAfter(ZookeeperDependenciesAutoConfiguration.class)
public class DependencyWatcherAutoConfiguration {

View File

@@ -1,5 +1,6 @@
# Auto Configuration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.zookeeper.discovery.dependency.DependencyRibbonAutoConfiguration,\
org.springframework.cloud.zookeeper.discovery.RibbonZookeeperAutoConfiguration,\
org.springframework.cloud.zookeeper.discovery.dependency.ZookeeperDependenciesAutoConfiguration,\
org.springframework.cloud.zookeeper.discovery.watcher.DependencyWatcherAutoConfiguration

View File

@@ -13,12 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.zookeeper.discovery
package org.springframework.cloud.zookeeper.common
import com.github.tomakehurst.wiremock.WireMockServer
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import org.apache.curator.framework.CuratorFramework
import org.apache.curator.test.TestingServer
import org.springframework.cloud.zookeeper.ZookeeperProperties
import org.springframework.context.annotation.Bean
@@ -34,15 +32,6 @@ class CommonTestConfig {
return new TestingServer(SocketUtils.findAvailableTcpPort())
}
@Bean(initMethod = "start", destroyMethod = "stop")
TestServiceRegistrar testServiceRegistrar(CuratorFramework curatorFramework) {
return new TestServiceRegistrar(wiremockServer().port(), curatorFramework)
}
@Bean(initMethod = "start", destroyMethod = "shutdown") WireMockServer wiremockServer() {
return new WireMockServer(SocketUtils.findAvailableTcpPort())
}
@Bean ZookeeperProperties zookeeperProperties() {
return new ZookeeperProperties(connectString: "localhost:${testingServer().port}")
}

View File

@@ -0,0 +1,23 @@
package org.springframework.cloud.zookeeper.common
import org.springframework.web.client.RestTemplate
class TestRibbonClient extends TestServiceRestClient {
private final String thisAppName
TestRibbonClient(RestTemplate restTemplate) {
super(restTemplate)
this.thisAppName = 'someName'
}
TestRibbonClient(RestTemplate restTemplate, String thisAppName) {
super(restTemplate)
this.thisAppName = thisAppName
}
String thisHealthCheck() {
return restTemplate.getForObject("http://$thisAppName/health", String)
}
}

View File

@@ -13,13 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.zookeeper.discovery
package org.springframework.cloud.zookeeper.common
import groovy.transform.PackageScope
import groovy.transform.CompileStatic
import org.springframework.web.client.RestTemplate
@PackageScope
@CompileStatic
class TestServiceRestClient {
@@ -29,11 +27,11 @@ class TestServiceRestClient {
this.restTemplate = restTemplate
}
String pingService(String alias) {
return restTemplate.getForObject("http://$alias/ping", String)
String callService(String alias, String endpoint) {
return restTemplate.getForObject("http://$alias/$endpoint", String)
}
String pingOnUrl(String url) {
return new RestTemplate().getForObject("http://$url/ping", String)
String callOnUrl(String url, String endpoint) {
return new RestTemplate().getForObject("http://$url/$endpoint", String)
}
}

View File

@@ -0,0 +1,14 @@
package org.springframework.cloud.zookeeper.discovery
trait PollingUtils {
Closure willPass(Closure closure) {
return {
try {
closure()
} catch (Exception e) {
throw new AssertionError("Exception occurred while evaluating closure", e)
}
}
}
}

View File

@@ -25,12 +25,12 @@ import org.apache.curator.x.discovery.UriSpec
@CompileStatic
class TestServiceRegistrar {
private final int wiremockServerPort
private final int serverPort
private final CuratorFramework curatorFramework
private final ServiceDiscovery serviceDiscovery
TestServiceRegistrar(int wiremockServerPort, CuratorFramework curatorFramework) {
this.wiremockServerPort = wiremockServerPort
TestServiceRegistrar(int serverPort, CuratorFramework curatorFramework) {
this.serverPort = serverPort
this.curatorFramework = curatorFramework
this.serviceDiscovery = serviceDiscovery()
}
@@ -42,7 +42,7 @@ class TestServiceRegistrar {
ServiceInstance serviceInstance() {
return ServiceInstance.builder().uriSpec(new UriSpec("{scheme}://{address}:{port}/"))
.address('localhost')
.port(wiremockServerPort)
.port(serverPort)
.name('testInstance')
.build()
}

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.cloud.zookeeper.discovery
import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.client.WireMock
import groovy.json.JsonSlurper
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
@@ -27,53 +25,47 @@ import org.springframework.cloud.client.ServiceInstance
import org.springframework.cloud.client.discovery.DiscoveryClient
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
import org.springframework.cloud.client.loadbalancer.LoadBalanced
import org.springframework.cloud.zookeeper.common.CommonTestConfig
import org.springframework.cloud.zookeeper.common.TestRibbonClient
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.context.annotation.Profile
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.ContextConfiguration
import org.springframework.web.client.RestTemplate
import spock.lang.Specification
import static com.github.tomakehurst.wiremock.client.WireMock.*
@ContextConfiguration(classes = Config, loader = SpringApplicationContextLoader)
@ActiveProfiles('ribbon')
@WebIntegrationTest(randomPort = true)
class ZookeeperDiscoveryISpec extends Specification {
public static final String TEST_INSTANCE_NAME = 'testInstance'
@Autowired TestRibbonClient testRibbonClient
@Autowired WireMockServer wiremockServer
@Autowired DiscoveryClient discoveryClient
@Autowired ZookeeperServiceDiscovery serviceDiscovery
WireMock wireMock
@Value('${spring.application.name}') String springAppName
def setup() {
wireMock = new WireMock('localhost', wiremockServer.port())
wireMock.register(get(urlEqualTo('/ping')).willReturn(aResponse().withBody('pong')))
}
def 'should find a collaborator via Ribbon'() {
expect:
'pong' == testRibbonClient.pingService(TEST_INSTANCE_NAME)
}
def 'should find the app by its name via Ribbon'() {
given:
def jsonSlurper = new JsonSlurper()
def health = jsonSlurper.parseText(testRibbonClient.thisHealthCheck())
expect:
'UP' == health.status
'UP' == registeredServiceStatusViaServiceName()
}
def 'should find a collaborator via discovery client'() {
given:
List<ServiceInstance> instances = discoveryClient.getInstances(TEST_INSTANCE_NAME)
List<ServiceInstance> instances = discoveryClient.getInstances(springAppName)
ServiceInstance instance = instances.first()
expect:
'pong' == testRibbonClient.pingOnUrl("${instance.host}:${instance.port}")
'UP' == registeredServiceStatus(instance)
}
private String registeredServiceStatusViaServiceName() {
return new JsonSlurper().parseText(testRibbonClient.thisHealthCheck()).status
}
private String registeredServiceStatus(ServiceInstance instance) {
return new JsonSlurper().parseText(testRibbonClient.callOnUrl("${instance.host}:${instance.port}", 'health')).status
}
def 'should properly find local instance'() {
@@ -85,6 +77,7 @@ class ZookeeperDiscoveryISpec extends Specification {
@EnableAutoConfiguration
@Import(CommonTestConfig)
@EnableDiscoveryClient
@Profile('ribbon')
static class Config {
@Bean
@@ -92,21 +85,5 @@ class ZookeeperDiscoveryISpec extends Specification {
@Value('${spring.application.name}') String springAppName) {
return new TestRibbonClient(restTemplate, springAppName)
}
}
static class TestRibbonClient extends TestServiceRestClient {
private final String thisAppName
TestRibbonClient(RestTemplate restTemplate, String thisAppName) {
super(restTemplate)
this.thisAppName = thisAppName
}
String thisHealthCheck() {
return restTemplate.getForObject("http://$thisAppName/health", String)
}
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2012-2015 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
*
* http://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.cloud.zookeeper.discovery.dependency
import org.apache.curator.framework.CuratorFramework
import org.apache.curator.test.TestingServer
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.test.SpringApplicationContextLoader
import org.springframework.boot.test.WebIntegrationTest
import org.springframework.cloud.client.discovery.DiscoveryClient
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient
import org.springframework.cloud.zookeeper.ZookeeperProperties
import org.springframework.cloud.zookeeper.discovery.PollingUtils
import org.springframework.cloud.zookeeper.discovery.TestServiceRegistrar
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.ContextConfiguration
import org.springframework.util.SocketUtils
import spock.lang.Specification
import spock.util.concurrent.PollingConditions
@ContextConfiguration(classes = Config, loader = SpringApplicationContextLoader)
@ActiveProfiles('loadbalancerclient')
@WebIntegrationTest(randomPort = true)
class StickyRuleISpec extends Specification implements PollingUtils {
@Autowired LoadBalancerClient loadBalancerClient
@Autowired DiscoveryClient discoveryClient
PollingConditions conditions
def setup() {
conditions = new PollingConditions()
}
def 'should use sticky load balancing strategy taken from Zookeeper dependencies'() {
expect:
thereAreTwoRegisteredServices()
URI uri = getUriForAlias()
conditions.eventually willPass {
2.times {
assert uri == getUriForAlias()
}
}
}
private boolean thereAreTwoRegisteredServices() {
return discoveryClient.getInstances('someAlias')?.size() == 2
}
private URI getUriForAlias() {
return loadBalancerClient.choose('someAlias')?.uri
}
@Configuration
@EnableAutoConfiguration
@EnableDiscoveryClient
@Profile('loadbalancerclient')
static class Config {
@Bean(destroyMethod = 'close')
TestingServer testingServer() {
return new TestingServer(SocketUtils.findAvailableTcpPort())
}
@Bean ZookeeperProperties zookeeperProperties() {
return new ZookeeperProperties(connectString: "localhost:${testingServer().port}")
}
@Bean(initMethod = "start", destroyMethod = "stop") TestServiceRegistrar serviceOne(CuratorFramework curatorFramework) {
return new TestServiceRegistrar(SocketUtils.findAvailableTcpPort(), curatorFramework)
}
@Bean(initMethod = "start", destroyMethod = "stop") TestServiceRegistrar serviceTwo(CuratorFramework curatorFramework) {
return new TestServiceRegistrar(SocketUtils.findAvailableTcpPort(), curatorFramework)
}
}
}

View File

@@ -0,0 +1,64 @@
package org.springframework.cloud.zookeeper.discovery.dependency
import spock.lang.Specification
import spock.lang.Unroll
class ZookeeperDependenciesSpec extends Specification {
private static final ZookeeperDependencies.ZookeeperDependency EXPECTED_DEPENDENCY = new ZookeeperDependencies.ZookeeperDependency(
'id',
'path',
LoadBalancerType.RANDOM,
'contentTypeTemplate',
'version',
[header: 'value'],
false
)
private static final Map<String, ZookeeperDependencies.ZookeeperDependency> DEPENDENCIES = [
alias: EXPECTED_DEPENDENCY
]
ZookeeperDependencies zookeeperDependencies = new ZookeeperDependencies(dependencies: DEPENDENCIES)
@Unroll
def "should retrieve dependency [#expectedDependency] for path [#path]"() {
expect:
expectedDependency == zookeeperDependencies.getDependencyForPath(path)
where:
path || expectedDependency
'unknownPath' || null
'path' || EXPECTED_DEPENDENCY
}
@Unroll
def "should retrieve dependency [#expectedDependency] for alias [#alias]"() {
expect:
expectedDependency == zookeeperDependencies.getDependencyForAlias(alias)
where:
alias || expectedDependency
'unknownAlias' || null
'alias' || EXPECTED_DEPENDENCY
}
@Unroll
def "should retrieve alias [#expectedAlias] for path [#path]"() {
expect:
expectedAlias == zookeeperDependencies.getAliasForPath(path)
where:
path || expectedAlias
'unknownPath' || ''
'path' || 'alias'
}
@Unroll
def "should retrieve path [#expectedPath] for alias [#alias]"() {
expect:
expectedPath == zookeeperDependencies.getPathForAlias(alias)
where:
alias || expectedPath
'unknownAlias' || ''
'alias' || 'path'
}
}

View File

@@ -13,10 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.zookeeper.discovery
package org.springframework.cloud.zookeeper.discovery.dependency
import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.client.WireMock
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.test.SpringApplicationContextLoader
@@ -25,39 +23,46 @@ import org.springframework.cloud.client.ServiceInstance
import org.springframework.cloud.client.discovery.DiscoveryClient
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
import org.springframework.cloud.client.loadbalancer.LoadBalanced
import org.springframework.cloud.zookeeper.common.CommonTestConfig
import org.springframework.cloud.zookeeper.common.TestRibbonClient
import org.springframework.cloud.zookeeper.discovery.PollingUtils
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Controller
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.ContextConfiguration
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.client.RestTemplate
import spock.lang.Specification
import static com.github.tomakehurst.wiremock.client.WireMock.*
import spock.util.concurrent.PollingConditions
@ContextConfiguration(classes = Config, loader = SpringApplicationContextLoader)
@ActiveProfiles('watcher')
@ActiveProfiles('dependencies')
@WebIntegrationTest(randomPort = true)
class ZookeeperDiscoveryWithDependenciesISpec extends Specification {
class ZookeeperDiscoveryWithDependenciesISpec extends Specification implements PollingUtils {
@Autowired TestRibbonClient testRibbonClient
@Autowired WireMockServer wiremockServer
@Autowired DiscoveryClient discoveryClient
WireMock wireMock
PollingConditions conditions
def setup() {
wireMock = new WireMock('localhost', wiremockServer.port())
wireMock.register(get(urlEqualTo('/ping')).willReturn(aResponse().withBody('pong')))
conditions = new PollingConditions()
}
def 'should find an instance via path when alias is not found'() {
expect:
!discoveryClient.getInstances('some/name/without/alias').empty
conditions.eventually willPass {
assert !discoveryClient.getInstances('nameWithoutAlias').empty
}
}
def 'should find a collaborator via Ribbon by using its alias from dependencies'() {
expect:
'pong' == testRibbonClient.pingService('someAlias')
conditions.eventually willPass {
assert callingServiceAtBeansEndpointIsNotEmpty()
}
}
def 'should find a collaborator via discovery client'() {
@@ -65,13 +70,24 @@ class ZookeeperDiscoveryWithDependenciesISpec extends Specification {
List<ServiceInstance> instances = discoveryClient.getInstances('someAlias')
ServiceInstance instance = instances.first()
expect:
'pong' == testRibbonClient.pingOnUrl("${instance.host}:${instance.port}")
conditions.eventually willPass {
assert callingServiceViaUrlOnBeansEndpointIsNotEmpty(instance)
}
}
private boolean callingServiceAtBeansEndpointIsNotEmpty() {
return !testRibbonClient.callService('someAlias', 'beans').empty
}
private boolean callingServiceViaUrlOnBeansEndpointIsNotEmpty(ServiceInstance instance) {
return !testRibbonClient.callOnUrl("${instance.host}:${instance.port}", 'beans').empty
}
@Configuration
@EnableAutoConfiguration
@Import(CommonTestConfig)
@EnableDiscoveryClient
@Profile('dependencies')
static class Config {
@Bean
@@ -81,10 +97,12 @@ class ZookeeperDiscoveryWithDependenciesISpec extends Specification {
}
static class TestRibbonClient extends TestServiceRestClient {
@Controller
@Profile('dependencies')
class PingController {
TestRibbonClient(RestTemplate restTemplate) {
super(restTemplate)
@RequestMapping('/ping') String ping() {
return 'pong'
}
}
}

View File

@@ -14,7 +14,6 @@
* limitations under the License.
*/
package org.springframework.cloud.zookeeper.discovery.watcher
import org.apache.curator.framework.CuratorFramework
import org.apache.curator.framework.CuratorFrameworkFactory
import org.apache.curator.retry.ExponentialBackoffRetry
@@ -26,11 +25,13 @@ import org.apache.curator.x.discovery.UriSpec
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.test.SpringApplicationContextLoader
import org.springframework.cloud.zookeeper.discovery.PollingUtils
import org.springframework.cloud.zookeeper.discovery.ZookeeperServiceDiscovery
import org.springframework.cloud.zookeeper.discovery.watcher.presence.DependencyPresenceOnStartupVerifier
import org.springframework.cloud.zookeeper.discovery.watcher.presence.LogMissingDependencyChecker
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.ContextConfiguration
@@ -42,12 +43,16 @@ import javax.annotation.PreDestroy
@ContextConfiguration(classes = Config, loader = SpringApplicationContextLoader)
@ActiveProfiles('watcher')
class DefaultDependencyWatcherSpringISpec extends Specification {
class DefaultDependencyWatcherSpringISpec extends Specification implements PollingUtils {
@Autowired AssertableDependencyPresenceOnStartupVerifier dependencyPresenceOnStartupVerifier
@Autowired AssertableDependencyWatcherListener dependencyWatcherListener
@Autowired
ZookeeperServiceDiscovery serviceDiscovery
@Autowired ZookeeperServiceDiscovery serviceDiscovery
PollingConditions conditions
def setup() {
conditions = new PollingConditions()
}
def 'should verify that presence of a dependency has been checked'() {
expect:
@@ -56,15 +61,17 @@ class DefaultDependencyWatcherSpringISpec extends Specification {
def 'should verify that dependency watcher listener is successfully registered and operational'() {
when:
serviceDiscovery.serviceDiscovery.unregisterService(serviceDiscovery.serviceInstance)
serviceDiscovery.serviceDiscovery.unregisterService(serviceDiscovery.serviceInstance)
then:
new PollingConditions().eventually {
dependencyWatcherListener.dependencyState == DependencyState.DISCONNECTED
conditions.eventually willPass {
assert dependencyWatcherListener.dependencyState == DependencyState.DISCONNECTED
}
}
@Configuration
@EnableAutoConfiguration
@Profile('watcher')
static class Config {
@Bean
@@ -79,9 +86,7 @@ class DefaultDependencyWatcherSpringISpec extends Specification {
@Bean
ZookeeperServiceDiscovery zookeeperServiceDiscovery() {
return new MyZookeeperServiceDiscovery(curatorFramework()) {
}
return new MyZookeeperServiceDiscovery(curatorFramework())
}
@Bean(initMethod = 'start', destroyMethod = 'close')

View File

@@ -0,0 +1,20 @@
spring.application.name: nameWithoutAlias
spring.cloud.zookeeper:
dependencies:
someAlias:
id: someId
path: nameWithoutAlias
loadBalancerType: ROUND_ROBIN
contentTypeTemplate: application/vnd.newsletter.$version+json
version: v1
headers:
header1: value1
header2: value2
required: false
testInstance2:
id: someId2
path: somePath2
loadBalancerType: ROUND_ROBIN
contentTypeTemplate: application/vnd.newsletter.$version+json2
version: v1
required: false

View File

@@ -0,0 +1,7 @@
spring.application.name: loadbalancerclient
spring.cloud.zookeeper:
dependencies:
someAlias:
id: someId
path: testInstance
loadBalancerType: STICKY

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<logger name="org.springframework.cloud.zookeeper" level="DEBUG"/>
</configuration>