All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.alibaba.edas.dubbo.EdasRegistry Maven / Gradle / Ivy

There is a newer version: 2.0.11
Show newest version
package com.alibaba.edas.dubbo;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import com.alibaba.acm.shaded.com.google.common.collect.Lists;
import com.alibaba.cloud.context.cs.AliCloudCsInitializer;
import com.alibaba.cloud.context.cs.CsHandledConfiguration;
import com.alibaba.edas.acm.ConfigService;
import com.alibaba.edas.acm.listener.ConfigChangeListener;

import com.taobao.config.client.Publisher;
import com.taobao.config.client.PublisherRegistrar;
import com.taobao.config.client.PublisherRegistration;
import com.taobao.config.client.Subscriber;
import com.taobao.config.client.SubscriberDataObserver;
import com.taobao.config.client.SubscriberRegistrar;
import com.taobao.config.client.SubscriberRegistration;
import com.taobao.config.client.utils.StringUtils;
import com.taobao.diamond.client.Diamond;
import com.taobao.diamond.client.impl.DiamondEnvRepo;
import org.apache.dubbo.common.Constants;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.utils.UrlUtils;
import org.apache.dubbo.registry.NotifyListener;
import org.apache.dubbo.registry.support.FailbackRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.taobao.diamond.common.Constants.DEFAULT_GROUP;

/**
 * @author edas
 */
public class EdasRegistry extends FailbackRegistry {

    private static final Logger LOGGER = LoggerFactory.getLogger(EdasRegistry.class);

    private final CsHandledConfiguration csHandledConfiguration;

    private final String root;
    private final static String DEFAULT_ROOT = "dubbo";
    private final static String PREFIX = "dubbo://";
    private final static String ACM_SEPARATOR = ":";
    private final static int RETRY_TIMES = 5;
    private final static String EMPTY = "empty_data";
    private final static String TIME_OUT_STRING = System.getProperty("edas.dubbo.subscribe.time");
    private final static long TIME_OUT = StringUtils.isBlank(TIME_OUT_STRING) ? 2500 : Integer.parseInt(
        TIME_OUT_STRING);

    private final ConcurrentHashMap listeners = new ConcurrentHashMap<>();
    private final ConcurrentHashMap diamondListeners = new ConcurrentHashMap<>();
    private final ConcurrentHashMap> subscriberMap = new ConcurrentHashMap<>();

    public EdasRegistry(URL url, CsHandledConfiguration csHandledConfiguration) {
        super(url);
        if (url.isAnyHost()) {
            throw new IllegalStateException("registry address == null");
        }
        String group = url.getParameter(Constants.GROUP_KEY, DEFAULT_ROOT);
        if (!group.startsWith(Constants.PATH_SEPARATOR)) {
            group = Constants.PATH_SEPARATOR + group;
        }
        if (!group.endsWith(Constants.PATH_SEPARATOR)) {
            group = group + Constants.PATH_SEPARATOR;
        }
        this.root = group;
        this.csHandledConfiguration = csHandledConfiguration;
    }

    @Override
    public void doRegister(URL url) {

        if (null == url.getParameter(Constants.CATEGORY_KEY) ||
            url.getParameter(Constants.CATEGORY_KEY).equals(Constants.CONSUMERS_CATEGORY) ||
            url.getParameter(Constants.CATEGORY_KEY).equals(Constants.PROVIDERS_CATEGORY)) {

            //如果是consumer 和 provider 数据,则往 config server 注册
            registryToConfigServer(url);

        } else if (url.getParameter(Constants.CATEGORY_KEY).equals(Constants.ROUTERS_CATEGORY) ||
            url.getParameter(Constants.CATEGORY_KEY).equals(Constants.CONFIGURATORS_CATEGORY)) {

            //如果是 routers 和 configurators 数据,往 diamond 注册
            registryToAcm(url);

        }
    }

    @Override
    public void doUnregister(URL url) {
        if (null == url.getParameter(Constants.CATEGORY_KEY) ||
            url.getParameter(Constants.CATEGORY_KEY).equals(Constants.CONSUMERS_CATEGORY) ||
            url.getParameter(Constants.CATEGORY_KEY).equals(Constants.PROVIDERS_CATEGORY)) {

            //如果是consumer 和 provider 数据,则往 config server 注册
            unRegistryFromConfigServer(url);

        } else if (url.getParameter(Constants.CATEGORY_KEY).equals(Constants.ROUTERS_CATEGORY) ||
            url.getParameter(Constants.CATEGORY_KEY).equals(Constants.CONFIGURATORS_CATEGORY)) {

            //如果是 routers 和 configurators 数据,往 diamond 注册
            unRegistryFromAcm(url);

        }
    }

    @Override
    public void doSubscribe(URL url, final NotifyListener notifyListener) {

        CountDownLatch countDownLatch = new CountDownLatch(1);
        ConfigServerNotify configServerNotify = new ConfigServerNotify(countDownLatch, url,
            Arrays.asList(notifyListener), this);

        listeners.put(url, configServerNotify);

        // 订阅 dubbo 协议的数据
        subscribeDubboData(url, configServerNotify, notifyListener);

        // 当明确地配置了 dubbo 的 group 和 version 时,会尝试订阅 hsf 协议的数据
        if (null != url.getParameter("version") && null != url.getParameter("group")) {
            subscribeHsfData(url, configServerNotify);
        }

        try {
            countDownLatch.await(TIME_OUT, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            LOGGER.error("interrupted exception when waiting subscribe ", e);
        }

    }

    @Override
    public void doUnsubscribe(URL url, NotifyListener notifyListener) {

        unSubscribeFromConfigServer(url);
        unSubscribeFromAcm(url);
    }

    @Override
    public boolean isAvailable() {
        return false;
    }

    @Override
    public List lookup(URL url) {
        return null;
    }

    private void registryToConfigServer(URL url) {

        String dataId = toCategoryPath(url);
        String value = url.toFullString();

        String publisherName = url.getServiceName() + url.getParameter("pid");
        String datumId = getDataTumId(url);

        PublisherRegistration registration = new PublisherRegistration(publisherName, dataId,
            datumId);
        AliCloudCsInitializer.initialize(csHandledConfiguration, registration);
        registration.setGroup(url.getParameter("group", ""));
        Publisher publisher = PublisherRegistrar.register(registration);
        publisher.publish(value);
    }

    /**
     * 方法需要加上synchronized,防止同时更新 ACM 时出现信息覆盖
     * 调用 ACM 接口时,需要调用 CAS 接口,防止数据出现覆盖,最多重试五次
     *
     * @param url
     */
    private synchronized void registryToAcm(URL url) {
        String dataId = toCategoryPath(url);
        dataId = dataId.replace(Constants.PATH_SEPARATOR, ACM_SEPARATOR);
        List configs = Lists.newArrayList();
        for (int i = 0; i < RETRY_TIMES; i++) {
            try {
                String content = ConfigService.getConfig(dataId, DEFAULT_GROUP, 3000);
                String originContent = content;
                if (content == null || StringUtils.isBlank(content)) {
                    //如果之前无数据,则发布一条
                    if (i < 2) {
                        //由于 Diamond 添加配置时不支持 add 的时候使用 CAS,只能是直接发布。
                        //快速发布两条时,存在第一条丢失的风险,所先 sleep 2s,写法很丑陋
                        TimeUnit.SECONDS.sleep(1);
                        continue;
                    }
                    configs.add(url.toFullString());
                    if (Diamond.publishSingle(dataId, DEFAULT_GROUP, configs.toString())) {
                        return;
                    }
                } else if (StringUtils.equals(content, EMPTY)) {
                    //由于 Diamond 内容不支持直接设置为 "" ,也不支持设置成 "   " 这类,只好设置成一个 EMPTY
                    configs.add(url.toFullString());
                    if (Diamond.publishSingleCas(dataId, DEFAULT_GROUP, EMPTY, configs.toString())) {
                        return;
                    }
                } else {
                    //如果之前有数据,先解析成数组,增加后更新数据
                    content = content.substring(1, content.length() - 1);
                    configs = Lists.newArrayList(content.split(", "));
                    if (configs.contains(url.toFullString())) {
                        //如果已经有数据,无需重复添加
                        return;
                    }
                    configs.add(url.toFullString());
                    if (Diamond.publishSingleCas(dataId, DEFAULT_GROUP, originContent, configs.toString())) {
                        return;
                    }
                }

            } catch (Exception e) {
                LOGGER.error("register to acm error,dataId:{},value:{},e:", dataId, url.toFullString(), e);
            }
        }
        LOGGER.error("fail to register to acm after {} retry,dataId:{},value:{},e:", RETRY_TIMES, dataId,
            url.toFullString());
    }

    private void unRegistryFromConfigServer(URL url) {

        //do nothing
    }

    /**
     * 方法需要加上synchronized,防止同时更新 ACM 时出现信息覆盖
     * 调用 ACM 接口时,需要调用 CAS 接口,防止数据出现覆盖
     *
     * @param url
     */
    private synchronized void unRegistryFromAcm(URL url) {
        String dataId = toCategoryPath(url);
        dataId = dataId.replace(Constants.PATH_SEPARATOR, ACM_SEPARATOR);
        List configs;

        for (int i = 0; i < RETRY_TIMES; i++) {
            try {
                String content = ConfigService.getConfig(dataId, DEFAULT_GROUP, 3000);
                String originContent = content;
                if (content == null || StringUtils.isBlank(content) || StringUtils.equals(content, EMPTY)) {
                    //无配置则直接略过
                    return;
                } else {
                    content = content.substring(1, content.length() - 1);
                    configs = Lists.newArrayList(content.split(", "));
                    configs.remove(url.toFullString());
                }

                if (configs.size() == 0) {
                    //因为删除接口没有CAS,且内容不支持直接设置为 "" ,也不支持设置成 "   "这类,只好设置成一个 EMPTY
                    //所以其他地方也得加上 EMPTY 的判断
                    if (Diamond.publishSingleCas(dataId, DEFAULT_GROUP, originContent, EMPTY)) {
                        return;
                    }
                } else {
                    content = configs.toString();
                    if (Diamond.publishSingleCas(dataId, DEFAULT_GROUP, originContent, content)) {
                        return;
                    }
                }
            } catch (Exception e) {
                LOGGER.error("unRegister from acm error,dataId:{},e:", dataId, e);
            }
        }

        LOGGER.error("fail to unregister from acm after {} retry,dataId:{},value:{},e:", RETRY_TIMES, dataId,
            url.toFullString());

    }

    private void subscribeHsfData(URL url, ConfigServerNotify configServerNotify) {
        String service = url.getServiceInterface();
        String version = url.getParameter("version");
        String dataId = service + ":" + version;

        String subscriberName = url.getServiceName() + url.getParameter("pid");
        String datumId = getDataTumId(url);

        SubscriberRegistration registration = new SubscriberRegistration(subscriberName,
            dataId, datumId);
        AliCloudCsInitializer.initialize(csHandledConfiguration, registration);
        registration.setGroup(url.getParameter("group"));
        Subscriber subscriber = SubscriberRegistrar.register(registration);
        subscriber.setDataObserver(configServerNotify);

        List subscribers = subscriberMap.get(url);
        if (subscribers == null) {
            subscribers = new ArrayList<>();
            subscriberMap.put(url, subscribers);
        }
        subscribers.add(subscriber);
    }

    private void subscribeDubboData(URL url, ConfigServerNotify configServerNotify, NotifyListener notifyListener) {

        //不支持通配符查询订阅
        String subscriberNameDubbo = url.getServiceName() + url.getParameter("pid");
        String datumIdDubbo = getDataTumId(url);
        for (String path : toCategoriesPath(url)) {
            int index = path.lastIndexOf(Constants.PATH_SEPARATOR);
            String category = path.substring(index + 1);
            if (category.equals(Constants.PROVIDERS_CATEGORY) || category.equals(Constants.CONSUMERS_CATEGORY)) {
                //providers 和 consumers 的数据,从 configserver 获取
                SubscriberRegistration registration = new SubscriberRegistration(subscriberNameDubbo, path,
                    datumIdDubbo);
                AliCloudCsInitializer.initialize(csHandledConfiguration, registration);
                registration.setGroup(url.getParameter("group", ""));
                Subscriber subscriber = SubscriberRegistrar.register(registration);
                subscriber.setDataObserver(configServerNotify);

                List subscribers = subscriberMap.get(url);
                if (subscribers == null) {
                    subscribers = new ArrayList<>();
                    subscriberMap.put(url, subscribers);
                }
                subscribers.add(subscriber);

            } else {

                //routers 和 configurators的数据,从 diamond 获取
                subscribeFromAcm(url, path, notifyListener);
            }
        }

    }

    private void unSubscribeFromConfigServer(URL url) {
        List subscribers = subscriberMap.get(url);
        if (null == subscribers) {
            return;
        }
        for (Subscriber subscriber : subscribers) {
            SubscriberRegistrar.unregister(subscriber);
        }

    }

    private void unSubscribeFromAcm(URL url) {
        for (String path : toCategoriesPath(url)) {
            //routers 和 configurators的数据,从 diamond 获取
            path = path.replace(Constants.PATH_SEPARATOR, ACM_SEPARATOR);
            ConfigChangeListener configChangeListener = diamondListeners.get(path);
            try {
                DiamondEnvRepo.getDefaultEnv().removeTenantListener(path, DEFAULT_GROUP, configChangeListener);
            } catch (Exception e) {
                LOGGER.error("remove config listener from acm error,path:{},", path, e);
            }
        }

    }

    private void subscribeFromAcm(URL url, String path, NotifyListener notifyListener) {

        path = path.replace(Constants.PATH_SEPARATOR, ACM_SEPARATOR);

        DiamondListener diamondListener = diamondListeners.get(path);
        if (diamondListener == null) {
            diamondListener = new DiamondListener(url, this, Arrays.asList(notifyListener), path);
            diamondListeners.put(path, diamondListener);
        }

        ConfigService.addListener(path, DEFAULT_GROUP, diamondListener);

    }

    class ConfigServerNotify implements SubscriberDataObserver {

        private CountDownLatch countDownLatch;
        private URL url;
        private Collection listeners;
        private EdasRegistry registry;
        private List lastHsfUrls;
        private List lastDubboUrls;

        public ConfigServerNotify(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;

        }

        public ConfigServerNotify(CountDownLatch countDownLatch, URL url, List notifyListeners,
                                  EdasRegistry edasConfigServerRegistry) {
            this.countDownLatch = countDownLatch;
            this.url = url;
            this.listeners = notifyListeners;
            this.registry = edasConfigServerRegistry;
            this.lastDubboUrls = Collections.emptyList();
            this.lastHsfUrls = Collections.emptyList();
        }

        @Override
        public void handleData(String dataId, List data) {//只要data内容有任何变更,都会回调这个方法
            LOGGER.info("New issues from ConfigServer arrived: " + data);

            List result = new ArrayList();

            if (dataId.contains(Constants.PATH_SEPARATOR)) {
                processDubboData(dataId, data);
            } else {
                processHsfData(dataId, data);
            }

            result.addAll(lastDubboUrls);
            result.addAll(lastHsfUrls);

            for (NotifyListener listener : listeners) {
                registry.notify(url, listener, result);
            }
            if (result.size() > 0) {
                countDownLatch.countDown();
            }
        }

        private void processHsfData(String dataId, List data) {
            String consumerService = url.getServiceInterface();
            String providerService = dataId.substring(0, dataId.indexOf(":"));

            if (!Constants.ANY_VALUE.equals(consumerService)) {
                if (!providerService.equals(consumerService)) {
                    return;
                }
            }
            List urls = new ArrayList();
            for (Object obj : data) {
                String str = (String)obj;
                URL u = convertHsfDataToDubboURL(str, dataId);
                urls.add(u);
            }
            this.lastHsfUrls = urls;
        }

        private void processDubboData(String dataId, List data) {
            String consumerService = url.getServiceInterface();
            String servicePath = toServicePath(dataId);
            String providerService = servicePath.startsWith(root) ? servicePath.substring(root.length()) : servicePath;

            if (!Constants.ANY_VALUE.equals(consumerService)) {
                if (!providerService.equals(consumerService)) {
                    return;
                }
            }
            List urls = new ArrayList();
            for (Object obj : data) {
                String str = (String)obj;
                str = PREFIX + str;
                URL u = URL.valueOf(str);
                if (UrlUtils.isMatch(url, u)) {
                    urls.add(u);
                }

            }
            this.lastDubboUrls = urls;
        }

        private String toCategoryName(String categoryPath) {
            int i = categoryPath.lastIndexOf(Constants.PATH_SEPARATOR);
            return i > 0 ? categoryPath.substring(i + 1) : categoryPath;
        }

        private URL convertHsfDataToDubboURL(String data, String dataId) {

            String service = dataId.substring(0, dataId.indexOf(":"));
            String version = dataId.substring(dataId.indexOf(":") + 1);

            String host = data.substring(0, data.indexOf(":"));
            int port = Integer.parseInt(StringUtils.substringBetween(data, ":", "?"));

            Map params = new HashMap<>();
            params.put("side", "provider");
            params.put("interface", service);
            params.put("version", version);
            params.put("group", url.getParameter("group"));
            return new URL("dubbo", host, port, service, params);
        }
    }

    public class DiamondListener extends ConfigChangeListener {

        private final URL url;
        private final EdasRegistry registry;
        private final Collection listeners;
        private final String dataId;

        public DiamondListener(URL url, EdasRegistry registry,
                               Collection listeners,
                               String dataId) {
            this.url = url;
            this.registry = registry;
            this.listeners = listeners;
            this.dataId = dataId;
        }

        private List getEmptyUrls() {
            String subscribeStr = this.dataId.replace(ACM_SEPARATOR, Constants.PATH_SEPARATOR);
            String service = this.url.getServiceInterface();
            String group = this.url.getParameter("group");
            String version = this.url.getParameter("version");
            String category = subscribeStr.substring(subscribeStr.lastIndexOf(Constants.PATH_SEPARATOR) + 1);

            URL emptyUrl = URL.valueOf(Constants.EMPTY_PROTOCOL + "://0.0.0.0/" + service + "?"
                + Constants.CATEGORY_KEY + "=" + category
                + (group == null ? "" : "&" + Constants.GROUP_KEY + "=" + group)
                + (version == null ? "" : "&" + Constants.VERSION_KEY + "=" + version));
            List urls = new ArrayList<>();
            urls.add(emptyUrl);
            return urls;
        }

        @Override
        public void receiveConfigInfo(String configInfo) {
            if (StringUtils.isBlank(configInfo)) {

                List urls = getEmptyUrls();

                for (NotifyListener listener : listeners) {
                    registry.notify(url, listener, urls);
                }
                return;
            }
            String content = configInfo.substring(1, configInfo.length() - 1);
            List configs = Lists.newArrayList(content.split(", "));
            List urls = new ArrayList<>();
            for (String tmp : configs) {
                URL url = URL.valueOf(tmp);
                urls.add(url);
            }

            if (urls.size() == 0) {
                urls = getEmptyUrls();
            }

            for (NotifyListener listener : listeners) {
                registry.notify(url, listener, urls);
            }
        }
    }

    private String toCategoryPath(URL url) {
        return toServicePath(url) + Constants.PATH_SEPARATOR + url.getParameter(Constants.CATEGORY_KEY,
            Constants.DEFAULT_CATEGORY);
    }

    private String toServicePath(String categoryPath) {
        int i;
        if (categoryPath.startsWith(root)) {
            i = categoryPath.indexOf(Constants.PATH_SEPARATOR, root.length());
        } else {
            i = categoryPath.indexOf(Constants.PATH_SEPARATOR);
        }
        return i > 0 ? categoryPath.substring(0, i) : categoryPath;
    }

    private String toServicePath(URL url) {
        return root + url.getServiceInterface();
    }

    private String[] toCategoriesPath(URL url) {
        String[] categories;
        if (Constants.ANY_VALUE.equals(url.getParameter(Constants.CATEGORY_KEY))) {
            categories = new String[] {Constants.PROVIDERS_CATEGORY, Constants.CONSUMERS_CATEGORY,
                Constants.ROUTERS_CATEGORY, Constants.CONFIGURATORS_CATEGORY};
        } else {
            categories = url.getParameter(Constants.CATEGORY_KEY, new String[] {Constants.DEFAULT_CATEGORY});
        }
        String[] paths = new String[categories.length];
        for (int i = 0; i < categories.length; i++) {
            paths[i] = toServicePath(url) + Constants.PATH_SEPARATOR + categories[i];
        }
        return paths;
    }

    private String getDataTumId(URL url) {
        String datumId = System.getProperty("JM.CONTAINER.ID");
        if (StringUtils.isEmpty(datumId)) {
            String appNameId = System.getProperty("project.name");
            if (StringUtils.isNotEmpty(appNameId)) {
                datumId = "ecu:" + appNameId + ":" + url.getHost();
            } else {
                datumId = "ecu:" + url.getHost();
            }
        }
        return datumId;
    }
}