Add option for re-registration when ephemeral Consul is restarted (#691)
Fixes gh-197
This commit is contained in:
@@ -34,7 +34,8 @@
|
||||
|spring.cloud.consul.discovery.health-check-url | | Custom health check url to override default.
|
||||
|spring.cloud.consul.discovery.heartbeat.enabled | `false` |
|
||||
|spring.cloud.consul.discovery.heartbeat.interval-ratio | |
|
||||
|spring.cloud.consul.discovery.heartbeat.ttl | `30s` |
|
||||
|spring.cloud.consul.discovery.heartbeat.ttl | `30s` |
|
||||
|spring.cloud.consul.discovery.heartbeat.reregister-service-on-failure | `false` | Enables service re-registration when the heartbeat fails.
|
||||
|spring.cloud.consul.discovery.hostname | | Hostname to use when accessing server.
|
||||
|spring.cloud.consul.discovery.include-hostname-in-instance-id | `false` | Whether hostname is included into the default instance id when registering service.
|
||||
|spring.cloud.consul.discovery.instance-group | | Service instance group.
|
||||
|
||||
@@ -51,6 +51,8 @@ public class HeartbeatProperties {
|
||||
@DecimalMax("0.9")
|
||||
private double intervalRatio = 2.0 / 3.0;
|
||||
|
||||
private boolean reregisterServiceOnFailure = false;
|
||||
|
||||
/**
|
||||
* @return the computed heartbeat interval
|
||||
*/
|
||||
@@ -90,6 +92,14 @@ public class HeartbeatProperties {
|
||||
this.intervalRatio = intervalRatio;
|
||||
}
|
||||
|
||||
public boolean isReregisterServiceOnFailure() {
|
||||
return this.reregisterServiceOnFailure;
|
||||
}
|
||||
|
||||
public void setReregisterServiceOnFailure(boolean reregisterServiceOnFailure) {
|
||||
this.reregisterServiceOnFailure = reregisterServiceOnFailure;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringCreator(this).append("enabled", this.enabled).append("ttl", this.ttl)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2013-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.cloud.consul.discovery;
|
||||
|
||||
import com.ecwid.consul.v1.OperationException;
|
||||
|
||||
/**
|
||||
* Predicate on whether to re-register service.
|
||||
*
|
||||
* @author Toshiaki Maki
|
||||
*/
|
||||
public interface ReregistrationPredicate {
|
||||
|
||||
/**
|
||||
* test if the exception is eligible for re-registration.
|
||||
* @param e OperationException
|
||||
* @return if the exception is eligible for re-registration
|
||||
*/
|
||||
boolean isEligible(OperationException e);
|
||||
|
||||
/**
|
||||
* Default implementation that performs re-registration when the status code is 500.
|
||||
*/
|
||||
ReregistrationPredicate DEFAULT = e -> e.getStatusCode() == 500;
|
||||
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
import com.ecwid.consul.v1.ConsulClient;
|
||||
import com.ecwid.consul.v1.OperationException;
|
||||
import com.ecwid.consul.v1.agent.model.NewService;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -42,18 +43,27 @@ public class TtlScheduler {
|
||||
|
||||
private final TaskScheduler scheduler = new ConcurrentTaskScheduler(Executors.newSingleThreadScheduledExecutor());
|
||||
|
||||
private HeartbeatProperties configuration;
|
||||
private final HeartbeatProperties heartbeatProperties;
|
||||
|
||||
private ConsulClient client;
|
||||
private final ConsulDiscoveryProperties discoveryProperties;
|
||||
|
||||
public TtlScheduler(HeartbeatProperties configuration, ConsulClient client) {
|
||||
this.configuration = configuration;
|
||||
private final ConsulClient client;
|
||||
|
||||
private final ReregistrationPredicate reregistrationPredicate;
|
||||
|
||||
private final Map<String, NewService> registeredServices = new ConcurrentHashMap<>();
|
||||
|
||||
public TtlScheduler(HeartbeatProperties heartbeatProperties, ConsulDiscoveryProperties discoveryProperties,
|
||||
ConsulClient client, ReregistrationPredicate reregistrationPredicate) {
|
||||
this.heartbeatProperties = heartbeatProperties;
|
||||
this.discoveryProperties = discoveryProperties;
|
||||
this.client = client;
|
||||
this.reregistrationPredicate = reregistrationPredicate;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void add(final NewService service) {
|
||||
add(service.getId());
|
||||
this.registeredServices.put(service.getId(), service);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,8 +71,8 @@ public class TtlScheduler {
|
||||
* @param instanceId instance id
|
||||
*/
|
||||
public void add(String instanceId) {
|
||||
ScheduledFuture task = this.scheduler.scheduleAtFixedRate(new ConsulHeartbeatTask(instanceId),
|
||||
this.configuration.computeHeartbeatInterval().toMillis());
|
||||
ScheduledFuture task = this.scheduler.scheduleAtFixedRate(new ConsulHeartbeatTask(instanceId, this),
|
||||
this.heartbeatProperties.computeHeartbeatInterval().toMillis());
|
||||
ScheduledFuture previousTask = this.serviceHeartbeats.put(instanceId, task);
|
||||
if (previousTask != null) {
|
||||
previousTask.cancel(true);
|
||||
@@ -75,24 +85,55 @@ public class TtlScheduler {
|
||||
task.cancel(true);
|
||||
}
|
||||
this.serviceHeartbeats.remove(instanceId);
|
||||
this.registeredServices.remove(instanceId);
|
||||
}
|
||||
|
||||
private class ConsulHeartbeatTask implements Runnable {
|
||||
static class ConsulHeartbeatTask implements Runnable {
|
||||
|
||||
private String checkId;
|
||||
private final String serviceId;
|
||||
|
||||
ConsulHeartbeatTask(String serviceId) {
|
||||
this.checkId = serviceId;
|
||||
if (!this.checkId.startsWith("service:")) {
|
||||
this.checkId = "service:" + this.checkId;
|
||||
private final String checkId;
|
||||
|
||||
private final TtlScheduler ttlScheduler;
|
||||
|
||||
ConsulHeartbeatTask(String serviceId, TtlScheduler ttlScheduler) {
|
||||
this.serviceId = serviceId;
|
||||
if (!this.serviceId.startsWith("service:")) {
|
||||
this.checkId = "service:" + this.serviceId;
|
||||
}
|
||||
else {
|
||||
this.checkId = this.serviceId;
|
||||
}
|
||||
this.ttlScheduler = ttlScheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
TtlScheduler.this.client.agentCheckPass(this.checkId);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Sending consul heartbeat for: " + this.checkId);
|
||||
try {
|
||||
this.ttlScheduler.client.agentCheckPass(this.checkId);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Sending consul heartbeat for: " + this.checkId);
|
||||
}
|
||||
}
|
||||
catch (OperationException e) {
|
||||
if (this.ttlScheduler.heartbeatProperties.isReregisterServiceOnFailure()
|
||||
&& this.ttlScheduler.reregistrationPredicate.isEligible(e)) {
|
||||
log.warn(e.getMessage());
|
||||
NewService registeredService = this.ttlScheduler.registeredServices.get(this.serviceId);
|
||||
if (registeredService != null) {
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("Re-register " + registeredService);
|
||||
}
|
||||
this.ttlScheduler.client.agentServiceRegister(registeredService,
|
||||
this.ttlScheduler.discoveryProperties.getAclToken());
|
||||
}
|
||||
else {
|
||||
log.warn("The service to re-register is not found.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ public class ConsulServiceRegistry implements ServiceRegistry<ConsulRegistration
|
||||
NewService service = reg.getService();
|
||||
if (this.heartbeatProperties.isEnabled() && this.ttlScheduler != null && service.getCheck() != null
|
||||
&& service.getCheck().getTtl() != null) {
|
||||
this.ttlScheduler.add(reg.getInstanceId());
|
||||
this.ttlScheduler.add(reg.getService());
|
||||
}
|
||||
}
|
||||
catch (ConsulException e) {
|
||||
|
||||
@@ -25,7 +25,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled;
|
||||
import org.springframework.cloud.consul.ConditionalOnConsulEnabled;
|
||||
import org.springframework.cloud.consul.discovery.ConsulDiscoveryClientConfiguration;
|
||||
import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties;
|
||||
import org.springframework.cloud.consul.discovery.HeartbeatProperties;
|
||||
import org.springframework.cloud.consul.discovery.ReregistrationPredicate;
|
||||
import org.springframework.cloud.consul.discovery.TtlScheduler;
|
||||
import org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistryAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -52,8 +54,16 @@ public class ConsulHeartbeatAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public TtlScheduler ttlScheduler(HeartbeatProperties heartbeatProperties, ConsulClient consulClient) {
|
||||
return new TtlScheduler(heartbeatProperties, consulClient);
|
||||
public TtlScheduler ttlScheduler(HeartbeatProperties heartbeatProperties,
|
||||
ConsulDiscoveryProperties discoveryProperties, ConsulClient consulClient,
|
||||
ReregistrationPredicate reRegistrationPredicate) {
|
||||
return new TtlScheduler(heartbeatProperties, discoveryProperties, consulClient, reRegistrationPredicate);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ReregistrationPredicate reRegistrationPredicate() {
|
||||
return ReregistrationPredicate.DEFAULT;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2013-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.cloud.consul.discovery;
|
||||
|
||||
import com.ecwid.consul.v1.ConsulClient;
|
||||
import com.ecwid.consul.v1.OperationException;
|
||||
import com.ecwid.consul.v1.agent.model.NewService;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.cloud.commons.util.InetUtils;
|
||||
import org.springframework.cloud.commons.util.InetUtilsProperties;
|
||||
import org.springframework.cloud.consul.discovery.TtlScheduler.ConsulHeartbeatTask;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Test for ConsulHeartbeatTask
|
||||
*
|
||||
* @author Toshiaki Maki
|
||||
*/
|
||||
public class ConsulHeartbeatTaskTests {
|
||||
|
||||
String serviceId = "service-A";
|
||||
|
||||
HeartbeatProperties heartbeatProperties;
|
||||
|
||||
ConsulDiscoveryProperties discoveryProperties;
|
||||
|
||||
ConsulClient consulClient;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.heartbeatProperties = new HeartbeatProperties();
|
||||
this.discoveryProperties = new ConsulDiscoveryProperties(new InetUtils(new InetUtilsProperties()));
|
||||
this.consulClient = mock(ConsulClient.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enableReRegistration() {
|
||||
TtlScheduler ttlScheduler = new TtlScheduler(heartbeatProperties, discoveryProperties, consulClient,
|
||||
ReregistrationPredicate.DEFAULT);
|
||||
ConsulHeartbeatTask consulHeartbeatTask = new ConsulHeartbeatTask(serviceId, ttlScheduler);
|
||||
heartbeatProperties.setReregisterServiceOnFailure(true);
|
||||
NewService service = new NewService();
|
||||
service.setId(serviceId);
|
||||
ttlScheduler.add(service);
|
||||
given(consulClient.agentCheckPass("service:" + serviceId)).willThrow(new OperationException(500,
|
||||
"Internal Server Error", "CheckID \"service:service-A\" does not have associated TTL"));
|
||||
consulHeartbeatTask.run();
|
||||
ArgumentCaptor<NewService> serviceCaptor = ArgumentCaptor.forClass(NewService.class);
|
||||
ArgumentCaptor<String> tokenCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(consulClient, atLeastOnce()).agentServiceRegister(serviceCaptor.capture(), tokenCaptor.capture());
|
||||
assertThat(serviceCaptor.getValue()).isSameAs(service);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void notEligibleForReRegistration() {
|
||||
TtlScheduler ttlScheduler = new TtlScheduler(heartbeatProperties, discoveryProperties, consulClient,
|
||||
ReregistrationPredicate.DEFAULT);
|
||||
ConsulHeartbeatTask consulHeartbeatTask = new ConsulHeartbeatTask(serviceId, ttlScheduler);
|
||||
heartbeatProperties.setReregisterServiceOnFailure(true);
|
||||
NewService service = new NewService();
|
||||
service.setId(serviceId);
|
||||
ttlScheduler.add(service);
|
||||
OperationException operationException = new OperationException(400, "Internal Server Error",
|
||||
"CheckID \"service:service-A\" does not have associated TTL");
|
||||
given(consulClient.agentCheckPass("service:" + serviceId)).willThrow(operationException);
|
||||
assertThatThrownBy(consulHeartbeatTask::run).isSameAs(operationException);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enableReRegistrationWithCustomPredicate() {
|
||||
TtlScheduler ttlScheduler = new TtlScheduler(heartbeatProperties, discoveryProperties, consulClient,
|
||||
e -> e.getStatusContent().endsWith("does not have associated TTL"));
|
||||
ConsulHeartbeatTask consulHeartbeatTask = new ConsulHeartbeatTask(serviceId, ttlScheduler);
|
||||
heartbeatProperties.setReregisterServiceOnFailure(true);
|
||||
NewService service = new NewService();
|
||||
service.setId(serviceId);
|
||||
ttlScheduler.add(service);
|
||||
given(consulClient.agentCheckPass("service:" + serviceId)).willThrow(new OperationException(400,
|
||||
"Internal Server Error", "CheckID \"service:service-A\" does not have associated TTL"));
|
||||
consulHeartbeatTask.run();
|
||||
ArgumentCaptor<NewService> serviceCaptor = ArgumentCaptor.forClass(NewService.class);
|
||||
ArgumentCaptor<String> tokenCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(consulClient, atLeastOnce()).agentServiceRegister(serviceCaptor.capture(), tokenCaptor.capture());
|
||||
assertThat(serviceCaptor.getValue()).isSameAs(service);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disableReRegistration() {
|
||||
TtlScheduler ttlScheduler = new TtlScheduler(heartbeatProperties, discoveryProperties, consulClient,
|
||||
ReregistrationPredicate.DEFAULT);
|
||||
ConsulHeartbeatTask consulHeartbeatTask = new ConsulHeartbeatTask(serviceId, ttlScheduler);
|
||||
heartbeatProperties.setReregisterServiceOnFailure(false);
|
||||
NewService service = new NewService();
|
||||
service.setId(serviceId);
|
||||
ttlScheduler.add(service);
|
||||
OperationException operationException = new OperationException(500, "Internal Server Error",
|
||||
"CheckID \"service:service-A\" does not have associated TTL");
|
||||
given(consulClient.agentCheckPass("service:" + serviceId)).willThrow(operationException);
|
||||
assertThatThrownBy(consulHeartbeatTask::run).isSameAs(operationException);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user