org.apache.dubbo.registry.consul.ConsulServiceDiscovery Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dubbo Show documentation
Show all versions of dubbo Show documentation
The all in one project of dubbo
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.dubbo.registry.consul;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.logger.Logger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.utils.CollectionUtils;
import org.apache.dubbo.common.utils.NamedThreadFactory;
import org.apache.dubbo.common.utils.StringUtils;
import org.apache.dubbo.event.EventListener;
import org.apache.dubbo.registry.client.AbstractServiceDiscovery;
import org.apache.dubbo.registry.client.DefaultServiceInstance;
import org.apache.dubbo.registry.client.ServiceInstance;
import org.apache.dubbo.registry.client.event.ServiceInstancesChangedEvent;
import org.apache.dubbo.registry.client.event.listener.ServiceInstancesChangedListener;
import com.ecwid.consul.v1.ConsistencyMode;
import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.QueryParams;
import com.ecwid.consul.v1.Response;
import com.ecwid.consul.v1.agent.model.NewService;
import com.ecwid.consul.v1.catalog.CatalogServicesRequest;
import com.ecwid.consul.v1.health.HealthServicesRequest;
import com.ecwid.consul.v1.health.model.HealthService;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static java.util.concurrent.Executors.newCachedThreadPool;
import static org.apache.dubbo.common.constants.CommonConstants.COMMA_SEPARATOR_CHAR;
import static org.apache.dubbo.common.constants.CommonConstants.SEMICOLON_SPLIT_PATTERN;
import static org.apache.dubbo.common.constants.ConsulConstants.DEFAULT_PORT;
import static org.apache.dubbo.common.constants.ConsulConstants.DEFAULT_WATCH_TIMEOUT;
import static org.apache.dubbo.common.constants.ConsulConstants.WATCH_TIMEOUT;
import static org.apache.dubbo.registry.consul.AbstractConsulRegistry.CHECK_PASS_INTERVAL;
import static org.apache.dubbo.registry.consul.AbstractConsulRegistry.DEFAULT_CHECK_PASS_INTERVAL;
import static org.apache.dubbo.registry.consul.AbstractConsulRegistry.DEFAULT_DEREGISTER_TIME;
import static org.apache.dubbo.registry.consul.AbstractConsulRegistry.DEREGISTER_AFTER;
import static org.apache.dubbo.registry.consul.AbstractConsulRegistry.ONE_THOUSAND;
import static org.apache.dubbo.registry.consul.AbstractConsulRegistry.PERIOD_DENOMINATOR;
import static org.apache.dubbo.registry.consul.ConsulParameter.ACL_TOKEN;
import static org.apache.dubbo.registry.consul.ConsulParameter.CONSISTENCY_MODE;
import static org.apache.dubbo.registry.consul.ConsulParameter.DEFAULT_ZONE_METADATA_NAME;
import static org.apache.dubbo.registry.consul.ConsulParameter.INSTANCE_GROUP;
import static org.apache.dubbo.registry.consul.ConsulParameter.INSTANCE_ZONE;
import static org.apache.dubbo.registry.consul.ConsulParameter.TAGS;
import static org.apache.dubbo.common.constants.ConsulConstants.INVALID_PORT;
/**
* 2019-07-31
*/
public class ConsulServiceDiscovery extends AbstractServiceDiscovery implements EventListener {
private static final String QUERY_TAG = "consul_query_tag";
private static final String REGISTER_TAG = "consul_register_tag";
private List registeringTags = new ArrayList<>();
private String tag;
private ConsulClient client;
private ExecutorService notifierExecutor = newCachedThreadPool(
new NamedThreadFactory("dubbo-service-discovery-consul-notifier", true));
private Map notifiers = new ConcurrentHashMap<>();
private TtlScheduler ttlScheduler;
private long checkPassInterval;
private URL url;
private String aclToken;
private List tags;
private ConsistencyMode consistencyMode;
private String defaultZoneMetadataName;
/**
* Service instance zone.
*/
private String instanceZone;
/**
* Service instance group.
*/
private String instanceGroup;
@Override
public void onEvent(ServiceInstancesChangedEvent event) {
}
@Override
public void initialize(URL registryURL) throws Exception {
this.url = registryURL;
String host = url.getHost();
int port = INVALID_PORT != url.getPort() ? url.getPort() : DEFAULT_PORT;
checkPassInterval = url.getParameter(CHECK_PASS_INTERVAL, DEFAULT_CHECK_PASS_INTERVAL);
client = new ConsulClient(host, port);
ttlScheduler = new TtlScheduler(checkPassInterval, client);
this.tag = registryURL.getParameter(QUERY_TAG);
this.registeringTags.addAll(getRegisteringTags(url));
this.aclToken = ACL_TOKEN.getValue(registryURL);
this.tags = getTags(registryURL);
this.consistencyMode = getConsistencyMode(registryURL);
this.defaultZoneMetadataName = DEFAULT_ZONE_METADATA_NAME.getValue(registryURL);
this.instanceZone = INSTANCE_ZONE.getValue(registryURL);
this.instanceGroup = INSTANCE_GROUP.getValue(registryURL);
}
/**
* Get the {@link ConsistencyMode}
*
* @param registryURL the {@link URL} of registry
* @return non-null, {@link ConsistencyMode#DEFAULT} as default
* @sine 2.7.8
*/
private ConsistencyMode getConsistencyMode(URL registryURL) {
String value = CONSISTENCY_MODE.getValue(registryURL);
if (StringUtils.isNotEmpty(value)) {
return ConsistencyMode.valueOf(value);
}
return ConsistencyMode.DEFAULT;
}
/**
* Get the "tags" from the {@link URL} of registry
*
* @param registryURL the {@link URL} of registry
* @return non-null
* @sine 2.7.8
*/
private List getTags(URL registryURL) {
String value = TAGS.getValue(registryURL);
return StringUtils.splitToList(value, COMMA_SEPARATOR_CHAR);
}
@Override
public URL getUrl() {
return url;
}
private List getRegisteringTags(URL url) {
List tags = new ArrayList<>();
String rawTag = url.getParameter(REGISTER_TAG);
if (StringUtils.isNotEmpty(rawTag)) {
tags.addAll(Arrays.asList(SEMICOLON_SPLIT_PATTERN.split(rawTag)));
}
return tags;
}
@Override
public void destroy() {
notifiers.forEach((_k, notifier) -> {
if (notifier != null) {
notifier.stop();
}
});
notifiers.clear();
notifierExecutor.shutdownNow();
ttlScheduler.stop();
}
@Override
public void doRegister(ServiceInstance serviceInstance) {
NewService consulService = buildService(serviceInstance);
ttlScheduler.add(consulService.getId());
client.agentServiceRegister(consulService, aclToken);
}
@Override
public void addServiceInstancesChangedListener(ServiceInstancesChangedListener listener)
throws NullPointerException, IllegalArgumentException {
Set serviceNames = listener.getServiceNames();
for (String serviceName : serviceNames) {
ConsulNotifier notifier = notifiers.get(serviceName);
if (notifier == null) {
Response> response = getHealthServices(serviceName, -1, buildWatchTimeout());
Long consulIndex = response.getConsulIndex();
notifier = new ConsulNotifier(serviceName, consulIndex);
}
notifierExecutor.execute(notifier);
}
}
@Override
public void doUpdate(ServiceInstance serviceInstance) {
// TODO
// client.catalogRegister(buildCatalogService(serviceInstance));
}
@Override
public void unregister(ServiceInstance serviceInstance) throws RuntimeException {
String id = buildId(serviceInstance);
ttlScheduler.remove(id);
client.agentServiceDeregister(id, aclToken);
}
@Override
public Set getServices() {
CatalogServicesRequest request = CatalogServicesRequest.newBuilder()
.setQueryParams(QueryParams.DEFAULT)
.setToken(aclToken)
.build();
return this.client.getCatalogServices(request).getValue().keySet();
}
@Override
public List getInstances(String serviceName) throws NullPointerException {
Response> response = getHealthServices(serviceName, -1, buildWatchTimeout());
Long consulIndex = response.getConsulIndex();
ConsulNotifier notifier = notifiers.get(serviceName);
if (notifier == null) {
notifier = new ConsulNotifier(serviceName, consulIndex);
notifiers.put(serviceName, notifier);
}
return convert(response.getValue());
}
private List convert(List services) {
return services.stream()
.map(HealthService::getService)
.map(service -> {
ServiceInstance instance = new DefaultServiceInstance(
service.getId(),
service.getService(),
service.getAddress(),
service.getPort());
instance.getMetadata().putAll(getMetadata(service));
return instance;
})
.collect(Collectors.toList());
}
private Response> getHealthServices(String service, long index, int watchTimeout) {
HealthServicesRequest request = HealthServicesRequest.newBuilder()
.setTag(tag)
.setQueryParams(new QueryParams(watchTimeout, index))
.setPassing(true)
.build();
return client.getHealthServices(service, request);
}
private Map getMetadata(HealthService.Service service) {
Map metadata = service.getMeta();
metadata = decodeMetadata(metadata);
if (CollectionUtils.isEmptyMap(metadata)) {
metadata = getScCompatibleMetadata(service.getTags());
}
return metadata;
}
private Map getScCompatibleMetadata(List tags) {
LinkedHashMap metadata = new LinkedHashMap<>();
if (tags != null) {
for (String tag : tags) {
String[] parts = StringUtils.delimitedListToStringArray(tag, "=");
switch (parts.length) {
case 0:
break;
case 1:
metadata.put(parts[0], parts[0]);
break;
case 2:
metadata.put(parts[0], parts[1]);
break;
default:
String[] end = Arrays.copyOfRange(parts, 1, parts.length);
metadata.put(parts[0], StringUtils.arrayToDelimitedString(end, "="));
break;
}
}
}
return metadata;
}
private NewService buildService(ServiceInstance serviceInstance) {
NewService service = new NewService();
service.setAddress(serviceInstance.getHost());
service.setPort(serviceInstance.getPort());
service.setId(buildId(serviceInstance));
service.setName(serviceInstance.getServiceName());
service.setCheck(buildCheck(serviceInstance));
service.setTags(buildTags(serviceInstance));
return service;
}
private String buildId(ServiceInstance serviceInstance) {
return Integer.toHexString(serviceInstance.hashCode());
}
private List buildTags(ServiceInstance serviceInstance) {
List tags = new LinkedList<>(this.tags);
if (StringUtils.isNotEmpty(instanceZone)) {
tags.add(defaultZoneMetadataName + "=" + instanceZone);
}
if (StringUtils.isNotEmpty(instanceGroup)) {
tags.add("group=" + instanceGroup);
}
Map params = serviceInstance.getMetadata();
params.keySet().stream()
.map(k -> k + "=" + params.get(k))
.forEach(tags::add);
tags.addAll(registeringTags);
return tags;
}
private Map buildMetadata(ServiceInstance serviceInstance) {
Map metadata = new LinkedHashMap<>();
metadata.putAll(getScCompatibleMetadata(registeringTags));
if (CollectionUtils.isNotEmptyMap(serviceInstance.getMetadata())) {
metadata.putAll(serviceInstance.getMetadata());
}
metadata = encodeMetadata(metadata);
return metadata;
}
private Map encodeMetadata(Map metadata) {
if (metadata == null) {
return metadata;
}
Map encoded = new HashMap<>(metadata.size());
metadata.forEach((k, v) -> encoded.put(Base64.getEncoder().encodeToString(k.getBytes()), v));
return encoded;
}
private Map decodeMetadata(Map metadata) {
if (metadata == null) {
return metadata;
}
Map decoded = new HashMap<>(metadata.size());
metadata.forEach((k, v) -> decoded.put(new String(Base64.getDecoder().decode(k)), v));
return decoded;
}
private NewService.Check buildCheck(ServiceInstance serviceInstance) {
NewService.Check check = new NewService.Check();
check.setTtl((checkPassInterval / ONE_THOUSAND) + "s");
String deregister = serviceInstance.getMetadata().get(DEREGISTER_AFTER);
check.setDeregisterCriticalServiceAfter(deregister == null ? DEFAULT_DEREGISTER_TIME : deregister);
return check;
}
private int buildWatchTimeout() {
return url.getParameter(WATCH_TIMEOUT, DEFAULT_WATCH_TIMEOUT) / ONE_THOUSAND;
}
private class ConsulNotifier implements Runnable {
private String serviceName;
private long consulIndex;
private boolean running;
ConsulNotifier(String serviceName, long consulIndex) {
this.serviceName = serviceName;
this.consulIndex = consulIndex;
this.running = true;
}
@Override
public void run() {
while (this.running) {
processService();
}
}
private void processService() {
Response> response = getHealthServices(serviceName, consulIndex, Integer.MAX_VALUE);
Long currentIndex = response.getConsulIndex();
if (currentIndex != null && currentIndex > consulIndex) {
consulIndex = currentIndex;
List services = response.getValue();
List serviceInstances = convert(services);
dispatchServiceInstancesChangedEvent(serviceName, serviceInstances);
}
}
void stop() {
this.running = false;
}
}
private static class TtlScheduler {
private static final Logger logger = LoggerFactory.getLogger(TtlScheduler.class);
private final Map serviceHeartbeats = new ConcurrentHashMap<>();
private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private long checkInterval;
private ConsulClient client;
public TtlScheduler(long checkInterval, ConsulClient client) {
this.checkInterval = checkInterval;
this.client = client;
}
/**
* Add a service to the checks loop.
*
* @param instanceId instance id
*/
public void add(String instanceId) {
ScheduledFuture task = this.scheduler.scheduleAtFixedRate(
new ConsulHeartbeatTask(instanceId),
checkInterval / PERIOD_DENOMINATOR,
checkInterval / PERIOD_DENOMINATOR,
TimeUnit.MILLISECONDS);
ScheduledFuture previousTask = this.serviceHeartbeats.put(instanceId, task);
if (previousTask != null) {
previousTask.cancel(true);
}
}
public void remove(String instanceId) {
ScheduledFuture task = this.serviceHeartbeats.get(instanceId);
if (task != null) {
task.cancel(true);
}
this.serviceHeartbeats.remove(instanceId);
}
private class ConsulHeartbeatTask implements Runnable {
private String checkId;
ConsulHeartbeatTask(String serviceId) {
this.checkId = serviceId;
if (!this.checkId.startsWith("service:")) {
this.checkId = "service:" + this.checkId;
}
}
@Override
public void run() {
TtlScheduler.this.client.agentCheckPass(this.checkId);
if (logger.isDebugEnabled()) {
logger.debug("Sending consul heartbeat for: " + this.checkId);
}
}
}
public void stop() {
scheduler.shutdownNow();
}
}
}