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; + } + +}