diff --git a/pom.xml b/pom.xml
index 44aaf3a5..5b5717dd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,6 +25,13 @@
HEAD
+
+
+ ecwid
+ http://nexus.ecwid.com/content/groups/public
+
+
+
spring-cloud-consul-core
spring-cloud-consul-config
@@ -111,13 +118,19 @@
com.ecwid.consul
consul-api
- 0.1
+ 1.0.8
javax.servlet
servlet-api
+
+
+
+ com.google.code.gson
+ gson
+ 2.3.1
@@ -165,6 +178,11 @@
ribbon-httpclient
${ribbon.version}
+
+ joda-time
+ joda-time
+ 2.7
+
org.projectlombok
lombok
diff --git a/spring-cloud-consul-core/pom.xml b/spring-cloud-consul-core/pom.xml
index ed025c00..05052729 100644
--- a/spring-cloud-consul-core/pom.xml
+++ b/spring-cloud-consul-core/pom.xml
@@ -33,6 +33,11 @@
com.ecwid.consul
consul-api
+
+
+ com.google.code.gson
+ gson
+
org.projectlombok
lombok
diff --git a/spring-cloud-consul-core/src/main/java/org/springframework/cloud/consul/model/SerfStatusEnum.java b/spring-cloud-consul-core/src/main/java/org/springframework/cloud/consul/model/SerfStatusEnum.java
new file mode 100644
index 00000000..53e741b4
--- /dev/null
+++ b/spring-cloud-consul-core/src/main/java/org/springframework/cloud/consul/model/SerfStatusEnum.java
@@ -0,0 +1,22 @@
+package org.springframework.cloud.consul.model;
+
+/**
+ * Gossip pool (serf) statuses.
+ * Created by nicu on 10.03.2015.
+ */
+public enum SerfStatusEnum {
+ StatusAlive(1),
+ StatusLeaving(2),
+ StatusLeft(3),
+ StatusFailed(4);
+ private final int code;
+
+ SerfStatusEnum(int code) {
+ this.code=code;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+}
diff --git a/spring-cloud-consul-discovery/pom.xml b/spring-cloud-consul-discovery/pom.xml
index 3fb1abdd..e92ddc87 100644
--- a/spring-cloud-consul-discovery/pom.xml
+++ b/spring-cloud-consul-discovery/pom.xml
@@ -47,6 +47,10 @@
spring-boot-starter-test
test
+
+ joda-time
+ joda-time
+
org.springframework.boot
spring-boot-starter-web
diff --git a/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/ConsulDiscoveryClientConfiguration.java b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/ConsulDiscoveryClientConfiguration.java
index 561d93dd..70e2e4e4 100644
--- a/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/ConsulDiscoveryClientConfiguration.java
+++ b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/ConsulDiscoveryClientConfiguration.java
@@ -1,5 +1,7 @@
package org.springframework.cloud.consul.discovery;
+import com.ecwid.consul.v1.ConsulClient;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -8,15 +10,24 @@ import org.springframework.context.annotation.Configuration;
*/
@Configuration
public class ConsulDiscoveryClientConfiguration {
+
+ @Autowired
+ private ConsulClient consulClient;
+
@Bean
public ConsulLifecycle consulLifecycle() {
return new ConsulLifecycle();
}
- /*@Bean
- public ConsulLoadBalancerClient consulLoadBalancerClient() {
- return new ConsulLoadBalancerClient();
- }*/
+ @Bean
+ public TtlScheduler ttlScheduler() {
+ return new TtlScheduler(heartbeatProperties(), consulClient);
+ }
+
+ @Bean
+ public HeartbeatProperties heartbeatProperties() {
+ return new HeartbeatProperties();
+ }
@Bean
public ConsulDiscoveryClient consulDiscoveryClient() {
diff --git a/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/ConsulLifecycle.java b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/ConsulLifecycle.java
index b1909ca0..606e754d 100644
--- a/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/ConsulLifecycle.java
+++ b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/ConsulLifecycle.java
@@ -19,6 +19,12 @@ public class ConsulLifecycle extends AbstractDiscoveryLifecycle {
@Autowired
private ConsulProperties consulProperties;
+ @Autowired
+ private TtlScheduler ttlScheduler;
+
+ @Autowired
+ private HeartbeatProperties ttlConfig;
+
@Override
protected void register() {
NewService service = new NewService();
@@ -30,8 +36,9 @@ public class ConsulLifecycle extends AbstractDiscoveryLifecycle {
Integer port = new Integer(getEnvironment().getProperty("server.port", "8080"));
service.setPort(port);
service.setTags(consulProperties.getTags());
-
- //TODO: add support for Check
+ NewService.Check check = new NewService.Check();
+ check.setTtl(ttlConfig.getTtl());
+ service.setCheck(check);
register(service);
}
@@ -49,6 +56,7 @@ public class ConsulLifecycle extends AbstractDiscoveryLifecycle {
protected void register(NewService service) {
log.info("Registering service with consul: {}", service.toString());
client.agentServiceRegister(service);
+ ttlScheduler.add(service);
}
@Override
@@ -57,7 +65,7 @@ public class ConsulLifecycle extends AbstractDiscoveryLifecycle {
}
@Override
- protected void deregister(){
+ protected void deregister() {
deregister(getContext().getId());
}
@@ -67,6 +75,7 @@ public class ConsulLifecycle extends AbstractDiscoveryLifecycle {
}
private void deregister(String serviceId) {
+ ttlScheduler.remove(serviceId);
client.agentServiceDeregister(serviceId);
}
diff --git a/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/ConsulRibbonClientConfiguration.java b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/ConsulRibbonClientConfiguration.java
index 96913c10..00a264ba 100644
--- a/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/ConsulRibbonClientConfiguration.java
+++ b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/ConsulRibbonClientConfiguration.java
@@ -21,9 +21,12 @@ import static com.netflix.client.config.CommonClientConfigKey.EnableZoneAffinity
import javax.annotation.PostConstruct;
+import com.netflix.loadbalancer.Server;
+import com.netflix.loadbalancer.ServerListFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.cloud.consul.discovery.filters.ServiceCheckServerListFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -69,6 +72,11 @@ public class ConsulRibbonClientConfiguration {
return serverList;
}
+ @Bean
+ public ServerListFilter ribbonServerListFilter() {
+ return new ServiceCheckServerListFilter(client);
+ }
+
@PostConstruct
public void preprocess() {
setProp(this.serviceId, DeploymentContextBasedVipAddresses.key(), this.serviceId);
diff --git a/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/ConsulServer.java b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/ConsulServer.java
index 9d1c2b82..a3b7eeff 100644
--- a/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/ConsulServer.java
+++ b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/ConsulServer.java
@@ -9,9 +9,13 @@ import com.netflix.loadbalancer.Server;
public class ConsulServer extends Server {
private final MetaInfo metaInfo;
+ private final String address;
+ private final String node;
public ConsulServer(final CatalogService service) {
super(service.getNode(), service.getServicePort());
+ address = service.getAddress();
+ node = service.getNode();
metaInfo = new MetaInfo() {
@Override
public String getAppName() {
@@ -39,4 +43,12 @@ public class ConsulServer extends Server {
public MetaInfo getMetaInfo() {
return metaInfo;
}
+
+ public String getAddress() {
+ return address;
+ }
+
+ public String getNode() {
+ return node;
+ }
}
diff --git a/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/HeartbeatProperties.java b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/HeartbeatProperties.java
new file mode 100644
index 00000000..ab0ba03e
--- /dev/null
+++ b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/HeartbeatProperties.java
@@ -0,0 +1,40 @@
+package org.springframework.cloud.consul.discovery;
+
+import javax.annotation.PostConstruct;
+import javax.validation.constraints.DecimalMax;
+import javax.validation.constraints.DecimalMin;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
+import lombok.Data;
+
+import org.joda.time.Period;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(prefix = "consul.heartbeat")
+@Data
+public class HeartbeatProperties {
+ @Min(1)
+ private int ttlValue = 30;
+
+ @NotNull
+ private String ttlUnit = "s";
+
+ @DecimalMin("0.1")
+ @DecimalMax("0.9")
+ private double intervalRatio = 2.0 / 3.0;
+
+ private Period heartbeatInterval;
+
+ @PostConstruct
+ public void computeHeartbeatInterval() {
+ // heartbeat rate at ratio * ttl, but no later than ttl -1s and, (under lesser
+ // priority), no sooner than 1s from now
+ heartbeatInterval = new Period(Math.round(1000 * Math.max(ttlValue - 1,
+ Math.min(ttlValue * intervalRatio, 1))));
+ }
+
+ public String getTtl() {
+ return ttlValue + ttlUnit;
+ }
+}
diff --git a/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/TtlScheduler.java b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/TtlScheduler.java
new file mode 100644
index 00000000..5563b9de
--- /dev/null
+++ b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/TtlScheduler.java
@@ -0,0 +1,58 @@
+package org.springframework.cloud.consul.discovery;
+
+import com.ecwid.consul.v1.ConsulClient;
+import com.ecwid.consul.v1.agent.model.NewService;
+import lombok.extern.slf4j.Slf4j;
+import org.joda.time.DateTime;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Created by nicu on 11.03.2015.
+ */
+@Slf4j
+public class TtlScheduler {
+
+ public static final DateTime EXPIRED_DATE = new DateTime(0);
+ private final Map serviceHeartbeats = new ConcurrentHashMap<>();
+
+ private HeartbeatProperties configuration;
+
+ private ConsulClient client;
+
+ public TtlScheduler(HeartbeatProperties configuration, ConsulClient client) {
+ this.configuration = configuration;
+ this.client = client;
+ }
+
+ /**
+ * Add a service to the checks loop.
+ */
+ public void add(final NewService service) {
+ serviceHeartbeats.put(service.getId(), EXPIRED_DATE);
+ }
+
+ public void remove(String serviceId) {
+ serviceHeartbeats.remove(serviceId);
+ }
+
+ @Scheduled(initialDelay = 0, fixedRateString = "${consul.heartbeat.fixedRate:15000}")
+ private void heartbeatServices() {
+ for (String serviceId : serviceHeartbeats.keySet()) {
+ DateTime latestHeartbeatDoneForService = serviceHeartbeats.get(serviceId);
+ if (latestHeartbeatDoneForService.plus(configuration.getHeartbeatInterval())
+ .isBefore(DateTime.now())) {
+ String checkId = serviceId;
+ if (!checkId.startsWith("service:")) {
+ checkId = "service:"+checkId;
+ }
+ client.agentCheckPass(checkId);
+ log.info("Sending consul heartbeat for: "+serviceId);
+ serviceHeartbeats.put(serviceId, DateTime.now());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/filters/AliveServerListFilter.java b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/filters/AliveServerListFilter.java
new file mode 100644
index 00000000..47e6c91a
--- /dev/null
+++ b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/filters/AliveServerListFilter.java
@@ -0,0 +1,39 @@
+package org.springframework.cloud.consul.discovery.filters;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import com.netflix.loadbalancer.Server;
+import org.springframework.cloud.consul.discovery.ConsulServer;
+
+import com.netflix.loadbalancer.ServerListFilter;
+
+/**
+ * Server filter: returns only alive servers. Each consul agent runs a serf agent which is
+ * a member of the serf gossip pool. The serf status (alive/failed/etc) is reflected in 2
+ * consul APIs: in the agent API and in the catalog API. We prefer the agent API because
+ * it is most up to date (or perhaps we should intersect them and pick members that are
+ * liv in both).
+ * @author nicu marasoiu on 10.03.2015.
+ */
+public class AliveServerListFilter implements ServerListFilter {
+ private FilteringAgentClient filteringAgentClient;
+
+ public AliveServerListFilter(FilteringAgentClient filteringAgentClient) {
+ this.filteringAgentClient = filteringAgentClient;
+ }
+
+ @Override
+ public List getFilteredListOfServers(List servers) {
+ Set liveNodes = filteringAgentClient.getAliveAgentsAddresses();
+ List filteredServers = new ArrayList<>();
+ for (Server server : servers) {
+ ConsulServer consulServer = ConsulServer.class.cast(server);
+ if (liveNodes.contains(consulServer.getAddress())) {
+ filteredServers.add(server);
+ }
+ }
+ return filteredServers;
+ }
+}
diff --git a/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/filters/FilteringAgentClient.java b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/filters/FilteringAgentClient.java
new file mode 100644
index 00000000..cb76c8d1
--- /dev/null
+++ b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/filters/FilteringAgentClient.java
@@ -0,0 +1,41 @@
+package org.springframework.cloud.consul.discovery.filters;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.cloud.consul.model.SerfStatusEnum;
+
+import com.ecwid.consul.v1.ConsulClient;
+import com.ecwid.consul.v1.agent.model.Member;
+
+public class FilteringAgentClient {
+
+ private static final int ALIVE_STATUS = SerfStatusEnum.StatusAlive.getCode();
+
+ private final ConsulClient client;
+
+ public FilteringAgentClient(ConsulClient client) {
+ this.client = client;
+ }
+
+ public List getAliveAgents() {
+ List members = client.getAgentMembers().getValue();
+ List liveMembers = new ArrayList<>(members.size());
+ for (Member peer : members) {
+ if (peer.getStatus() == ALIVE_STATUS) {
+ liveMembers.add(peer);
+ }
+ }
+ return liveMembers;
+ }
+
+ public Set getAliveAgentsAddresses() {
+ Set addresses = new HashSet<>();
+ for (Member server : getAliveAgents()) {
+ addresses.add(server.getAddress());
+ }
+ return addresses;
+ }
+}
diff --git a/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/filters/ServiceCheckServerListFilter.java b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/filters/ServiceCheckServerListFilter.java
new file mode 100644
index 00000000..211e63b7
--- /dev/null
+++ b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/filters/ServiceCheckServerListFilter.java
@@ -0,0 +1,69 @@
+package org.springframework.cloud.consul.discovery.filters;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import com.netflix.loadbalancer.Server;
+import org.springframework.cloud.consul.discovery.ConsulServer;
+
+import com.ecwid.consul.v1.ConsulClient;
+import com.ecwid.consul.v1.QueryParams;
+import com.ecwid.consul.v1.health.model.Check;
+import com.netflix.loadbalancer.ServerListFilter;
+
+/**
+ * Created by nicu on 12.03.2015.
+ */
+public class ServiceCheckServerListFilter implements ServerListFilter {
+
+ private ConsulClient client;
+
+ public ServiceCheckServerListFilter(ConsulClient client) {
+ this.client = client;
+ }
+
+ @Override
+ public List getFilteredListOfServers(List servers) {
+ Set passingServiceIds = getPassingServiceIds(servers);
+ List okServers = new ArrayList<>(servers.size());
+ for (Server server : servers) {
+ String serviceId = server.getMetaInfo().getInstanceId();
+ if (passingServiceIds.contains(serviceId)) {
+ ConsulServer consulServer = ConsulServer.class.cast(server);
+ List nodeChecks = client.getHealthChecksForNode(
+ consulServer.getNode(), QueryParams.DEFAULT).getValue();
+ boolean passingNodeChecks = true;
+ for (Check check : nodeChecks) {
+ if (check.getStatus() != Check.CheckStatus.PASSING) {
+ passingNodeChecks = false;
+ break;
+ }
+ }
+ if (passingNodeChecks) {
+ okServers.add(server);
+ }
+ }
+ }
+ return okServers;
+ }
+
+ private Set getPassingServiceIds(List servers) {
+ Set serviceIds = new HashSet<>(1);
+ for (Server server : servers) {
+ serviceIds.add(server.getMetaInfo().getInstanceId());
+ }
+ for (String serviceId : serviceIds) {
+ List serviceChecks = client.getHealthChecksForService(serviceId,
+ QueryParams.DEFAULT).getValue();
+ for (Check check : serviceChecks) {
+ if (check.getStatus() != Check.CheckStatus.PASSING) {
+ serviceIds.remove(check.getServiceId());
+ }
+ }
+ }
+ return serviceIds;
+ }
+
+}