Add option for re-registration when ephemeral Consul is restarted (#691)

Fixes gh-197
This commit is contained in:
Toshiaki Maki
2021-03-17 00:57:03 +09:00
committed by GitHub
parent 3ce3a99d23
commit 36cc97420a
7 changed files with 247 additions and 20 deletions

View File

@@ -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.

View File

@@ -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)

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

@@ -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);
}
}