Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dubbo- support tag router feature] Add a new Router implement -- TagRouter #2228

Merged
merged 14 commits into from
Aug 22, 2018

Conversation

lexburner
Copy link
Contributor

@lexburner lexburner commented Aug 11, 2018

What is the purpose of the change

Support tag router feature for dubbo.

some pain points

isolation by feature

  • fact1: some applications changed at the same time in different branch,like A,B,C.
  • fact2: some applications didn't change ,but deploy repeatedly,like D,E.
  • fact3: if a new feature need to be tested, all 5 application need to be deployed.
  • fact4: isolation by feature is not friendly to parallel development.

use tag

isolation by tag

  • fact: changed applications are incremental deployment

some cases

some cases

usage

  • consumer
RpcContext.getContext().setAttachment("request.tag","red");

i suggest you can use filter or SPI to set this attachment, notice that RpcContext is bound to Thread.

  • provider
    @Bean
    public ApplicationConfig applicationConfig() {
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName("provider-book");
        // instruct tag router
        Map<String,String> parameters = new HashMap<>();
        parameters.put(Constants.ROUTER_KEY, "tag");
        applicationConfig.setParameters(parameters);
        return applicationConfig;
    }

Brief changelog

  • add unit property for dubbo.xsd.
  • add TagRouter,TagRouterFactory.
  • add SPI statement for adding TagRouter automatically.
  • fix a small bug(may be not) in org.apache.dubbo.registry.integration.RegistryDirectory#route,exclude runtime routers, avoid to filter some runtime invokers.

Verifying this change

Here is more detail about Unit Test:
org.apache.dubbo.rpc.cluster.router.tag.TagRouterTest
I have verified that the unit test has been correct and passed.

Here is more detail about a Integration Test:
clone the repository https://github.com/lexburner/Dubbo-Sample-TagRouter.git,can see the route tag feature.

Follow this checklist to help us incorporate your contribution quickly and easily:

  • Make sure there is a GITHUB_issue filed for the change (usually before you start working on it). Trivial changes like typos do not require a GITHUB issue. Your pull request should address just this issue, without pulling in other changes - one PR resolves one issue.
  • Format the pull request title like [Dubbo-XXX] Fix UnknownException when host config not exist #XXX. Each commit in the pull request should have a meaningful subject line and body.
  • Write a pull request description that is detailed enough to understand what the pull request does, how, and why.
  • Write necessary unit-test to verify your logic correction, more mock a little better when cross module dependency exist. If the new feature or significant change is committed, please remember to add integration-test in test module.
  • Run mvn clean install -DskipTests & mvn clean test-compile failsafe:integration-test to make sure unit-test and integration-test pass.
  • If this contribution is large, please follow the Software Donation Guide.

@codecov-io
Copy link

codecov-io commented Aug 13, 2018

Codecov Report

❗ No coverage uploaded for pull request base (master@b2ce7b2). Click here to learn what that means.
The diff coverage is 61.36%.

Impacted file tree graph

@@            Coverage Diff            @@
##             master    #2228   +/-   ##
=========================================
  Coverage          ?   54.65%           
  Complexity        ?     5172           
=========================================
  Files             ?      571           
  Lines             ?    25084           
  Branches          ?     4464           
=========================================
  Hits              ?    13709           
  Misses            ?     9334           
  Partials          ?     2041
Impacted Files Coverage Δ Complexity Δ
...c/main/java/org/apache/dubbo/common/Constants.java 88.88% <ø> (ø) 1 <0> (?)
...g/apache/dubbo/config/AbstractInterfaceConfig.java 80.78% <ø> (ø) 81 <0> (?)
...g/apache/dubbo/config/AbstractReferenceConfig.java 93.75% <ø> (ø) 27 <0> (?)
.../dubbo/registry/integration/RegistryDirectory.java 0% <0%> (ø) 0 <0> (?)
...dubbo/rpc/cluster/router/tag/TagRouterFactory.java 100% <100%> (ø) 2 <2> (?)
...org/apache/dubbo/config/AbstractServiceConfig.java 95.58% <33.33%> (ø) 42 <1> (?)
...apache/dubbo/rpc/cluster/router/tag/TagRouter.java 66.66% <66.66%> (ø) 11 <11> (?)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update b2ce7b2...bf3915c. Read the comment docs.

@diecui1202
Copy link

@lexburner Thanks for your contributing. I think this PR is a big feature, so I suggest discussing it on the dev mail list. BTW, pls. solve the conflicts.

@diecui1202 diecui1202 added the status/waiting-for-feedback Need reporters to triage label Aug 17, 2018
@beiwei30
Copy link
Member

very nice feature, will look into it.

import java.util.List;

/**
* @author [email protected]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove author plz

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lack of ASF license in the head:

/*
 * 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.
 */

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, i will add licenses for all new files.

// Tag request
if (!StringUtils.isEmpty(tag)) {
// Select tag invokers first
for (Invoker<T> invoker : invokers) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

three for statement can be combined one and check conditions in it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These three for statements have their own logic, depend on different conditions, can you give me more details, thanks a lot.

Copy link

@hupuxiaojun hupuxiaojun Sep 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lexburner @kimmking i think three for statement can be combined too,my impl:

public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
    // filter
    List<Invoker<T>> result = new ArrayList<>();
    // all invokers that don't have "tag" parameter in url is a normal invoker
    List<Invoker<T>> normalResult = new ArrayList<>();
    try {
        // Dynamic param
        String tag = RpcContext.getContext().getAttachment(REQUEST_TAG_KEY);

        for (Invoker<T> invoker : invokers) {
            if (StringUtils.isEmpty(invoker.getUrl().getParameter(TAG_KEY))) {
                // all invokers that don't have "tag" parameter in url is a normal invoker
                normalResult.add(invoker);
            } else {
                if (invoker.getUrl().getParameter(TAG_KEY).equals(tag)) {
                    result.add(invoker);
                }
            }
        }
        // If no invoker be selected, downgrade to normal invokers
        if (result.isEmpty()) {
            return normalResult;
        }
        return result;
    } catch (Exception e) {
        logger.error("Route by tag error,return all invokers.", e);
    }
    // Downgrade to all invokers
    return invokers;
}

@@ -307,6 +307,12 @@
<xsd:documentation><![CDATA[ The serialization protocol of service. ]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="tag" type="xsd:string">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls. also change dubbo-config-spring/src/main/resources/META-INF/compat/dubbo.xsd

private final URL url;

public static final URL ROUTER_URL =
new URL("tag"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls. change this to one liner, or complete remove it since I notice TagRouter() is redundant.

}
// Normal request
} else {
for (Invoker<T> invoker : invokers) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for normal request, I guess we should include all invokers, no matter the service is published with or without tag, for example: what if all services are published with tag, then is it expected that there's no valid service to invoke?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional, if a service published with tag, we assume it as a new feature, and won't normal request or other feature's request hit it. Only the tag request hit the tag service.

In another case, tag request can hit normal service if (and only if) there is no tag service, i call it downgrade.

I know that no valid service to invoke was sound terrible , but if users want to use TagRouter , i think it is their duty to know what will happen to their projects.

By default, if we do not config like:

Map<String,String> parameters = new HashMap<>();
parameters.put(Constants.ROUTER_KEY, "tag");
applicationConfig.setParameters(parameters);

There will no TagRouter feature exist in our project. Users who don't care about the TagRouter feature are not affected by it.

import org.apache.dubbo.rpc.cluster.RouterFactory;

/**
* @author [email protected]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, remove author and add ASF license header.

import java.util.List;

/**
* @author [email protected]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, remove author info and add ASF license header.

@beiwei30
Copy link
Member

I think it's good to go, I will merge it.

@lexburner pls. add corresponding doc at https://github.com/apache/incubator-dubbo-website, and pls. consider to write a tech blog too. Thx.

@hupuxiaojun
Copy link

hupuxiaojun commented Sep 3, 2018

TagRouter feature is very good, but i think an LoadBalance Wrapper can get same result ,and has smaller changes with source code:

import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.common.logger.Logger;
import com.alibaba.dubbo.common.logger.LoggerFactory;
import com.alibaba.dubbo.common.utils.StringUtils;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.RpcContext;
import com.alibaba.dubbo.rpc.RpcException;
import com.alibaba.dubbo.rpc.cluster.LoadBalance;

import java.util.ArrayList;
import java.util.List;


public class LoadbalanceTagFilterWrapper implements LoadBalance {
    private static final Logger logger = LoggerFactory.getLogger(LoadbalanceTagFilterWrapper.class);
    private LoadBalance loadBalance;

    public LoadbalanceTagFilterWrapper(LoadBalance loadBalance) {
        this.loadBalance = loadBalance;
    }

    @Override
    public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
        List<Invoker<T>> filteredInvokers = filterInvokers(invokers);
        return loadBalance.select(filteredInvokers, url, invocation);
    }

    private <T> List<Invoker<T>> filterInvokers(List<Invoker<T>> invokers) {
        // filter
        List<Invoker<T>> result = new ArrayList<>();
        // all invokers that don't have "tag" parameter in url is a normal invoker
        List<Invoker<T>> normalResult = new ArrayList<>();
        try {
            // Dynamic param
            String tag = RpcContext.getContext().getAttachment(Constants.REQUEST_TAG_KEY);

            for (Invoker<T> invoker : invokers) {
                if (StringUtils.isEmpty(invoker.getUrl().getParameter(Constants.TAG_KEY))) {
                    // all invokers that don't have "tag" parameter in url is a normal invoker
                    normalResult.add(invoker);
                } else {
                    String[] tags = invoker.getUrl().getParameter(Constants.TAG_KEY).split(",");
                    for (String s : tags) {
                        if (s.equals(tag)) {
                            result.add(invoker);
                            break;
                        }
                    }
                }
            }
            // If no invoker be selected, downgrade to normal invokers
            if (result.isEmpty()) {
                return normalResult;
            }
            return result;
        } catch (Exception e) {
            logger.error("Route by tag error,return all invokers.", e);
        }
        // Downgrade to all invokers
        return invokers;
    }
}

@qixiaobo
Copy link
Contributor

TagRouter feature is very good, but i think an LoadBalance Wrapper can get same result ,and has smaller changes with source code:

import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.common.logger.Logger;
import com.alibaba.dubbo.common.logger.LoggerFactory;
import com.alibaba.dubbo.common.utils.StringUtils;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.RpcContext;
import com.alibaba.dubbo.rpc.RpcException;
import com.alibaba.dubbo.rpc.cluster.LoadBalance;

import java.util.ArrayList;
import java.util.List;


public class LoadbalanceTagFilterWrapper implements LoadBalance {
    private static final Logger logger = LoggerFactory.getLogger(LoadbalanceTagFilterWrapper.class);
    private LoadBalance loadBalance;

    public LoadbalanceTagFilterWrapper(LoadBalance loadBalance) {
        this.loadBalance = loadBalance;
    }

    @Override
    public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
        List<Invoker<T>> filteredInvokers = filterInvokers(invokers);
        return loadBalance.select(filteredInvokers, url, invocation);
    }

    private <T> List<Invoker<T>> filterInvokers(List<Invoker<T>> invokers) {
        // filter
        List<Invoker<T>> result = new ArrayList<>();
        // all invokers that don't have "tag" parameter in url is a normal invoker
        List<Invoker<T>> normalResult = new ArrayList<>();
        try {
            // Dynamic param
            String tag = RpcContext.getContext().getAttachment(Constants.REQUEST_TAG_KEY);

            for (Invoker<T> invoker : invokers) {
                if (StringUtils.isEmpty(invoker.getUrl().getParameter(Constants.TAG_KEY))) {
                    // all invokers that don't have "tag" parameter in url is a normal invoker
                    normalResult.add(invoker);
                } else {
                    String[] tags = invoker.getUrl().getParameter(Constants.TAG_KEY).split(",");
                    for (String s : tags) {
                        if (s.equals(tag)) {
                            result.add(invoker);
                            break;
                        }
                    }
                }
            }
            // If no invoker be selected, downgrade to normal invokers
            if (result.isEmpty()) {
                return normalResult;
            }
            return result;
        } catch (Exception e) {
            logger.error("Route by tag error,return all invokers.", e);
        }
        // Downgrade to all invokers
        return invokers;
    }
}

Very interesting , so I should say route and loadBalance maybe a little redundancy of functions;
But in fact, load balance may just do something like aim of making their overall processing more efficient.
So router maybe the better choice in semantic.

@qixiaobo
Copy link
Contributor

How about providing a filter to set tag attachment at same time ?
I think we will invoke not only one service but the attachments will be cleared after invoking provider every time

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status/waiting-for-feedback Need reporters to triage
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants