Browse Source

Added Ribbon integration

1.x
adriancole 11 years ago
parent
commit
199353ddbe
  1. 6
      CHANGES.md
  2. 10
      README.md
  3. 24
      build.gradle
  4. 30
      feign-ribbon/README.md
  5. 153
      feign-ribbon/src/main/java/feign/ribbon/LBClient.java
  6. 114
      feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
  7. 89
      feign-ribbon/src/main/java/feign/ribbon/RibbonModule.java
  8. 74
      feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java
  9. 74
      feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java
  10. 2
      settings.gradle

6
CHANGES.md

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
### Version 1.1.0
* adds Ribbon integration
### Version 1.0.0
* Initial open source release

10
README.md

@ -65,6 +65,16 @@ CloudDNS cloudDNS = Feign.create().newInstance(new CloudIdentityTarget<CloudDNS @@ -65,6 +65,16 @@ CloudDNS cloudDNS = Feign.create().newInstance(new CloudIdentityTarget<CloudDNS
```
You can find [several examples](https://github.com/Netflix/feign/tree/master/feign-core/src/test/java/feign/examples) in the test tree. Do take time to look at them, as seeing is believing!
### Integrations
Feign intends to work well within Netflix and other Open Source communities. Modules are welcome to integrate with your favorite projects!
### Ribbon
[RibbonModule](https://github.com/Netflix/feign/tree/master/feign-ribbon) overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by [Ribbon](https://github.com/Netflix/ribbon).
Integration requires you to pass your ribbon client name as the host part of the url, for example `myAppProd`.
```java
MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonModule());
```
### Advanced usage and Dagger
#### Dagger
Feign can be directly wired into Dagger which keeps things at compile time and Android friendly. As opposed to exposing builders for config, Feign intends users to embed their config in Dagger.

24
build.gradle

@ -4,14 +4,16 @@ ext.githubProjectName = rootProject.name // Change if github project name is not @@ -4,14 +4,16 @@ ext.githubProjectName = rootProject.name // Change if github project name is not
buildscript {
repositories {
mavenLocal()
mavenCentral() // maven { url 'http://jcenter.bintray.com' }
mavenCentral()
}
apply from: file('gradle/buildscript.gradle'), to: buildscript
}
allprojects {
repositories {
mavenCentral() // maven { url: 'http://jcenter.bintray.com' }
mavenLocal()
mavenCentral()
maven { url 'https://oss.sonatype.org/content/repositories/releases/' }
}
}
@ -42,4 +44,20 @@ project(':feign-core') { @@ -42,4 +44,20 @@ project(':feign-core') {
testCompile 'org.testng:testng:6.8.1'
testCompile 'com.google.mockwebserver:mockwebserver:20130505'
}
}
}
project(':feign-ribbon') {
apply plugin: 'java'
test {
useTestNG()
}
dependencies {
compile project(':feign-core')
compile 'com.netflix.ribbon:ribbon-core:0.2.0'
provided 'com.squareup.dagger:dagger-compiler:1.0.1'
testCompile 'org.testng:testng:6.8.1'
testCompile 'com.google.mockwebserver:mockwebserver:20130505'
}
}

30
feign-ribbon/README.md

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
# Ribbon
This module includes a feign `Target` and `Client` adapter to take advantage of [Ribbon](https://github.com/Netflix/ribbon).
## Conventions
This integration relies on the Feign `Target.url()` being encoded like `https://myAppProd` where `myAppProd` is the ribbon client or loadbalancer name and `myAppProd.ribbon.listOfServers` configuration is set.
### RibbonModule
Adding `RibbonModule` overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by Ribbon.
#### Usage
instead of 
```java
MyService api = Feign.create(MyService.class, "https://myAppProd-1234567890.us-east-1.elb.amazonaws.com");
```
do
```java
MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonModule());
```
### LoadBalancingTarget
Using or extending `LoadBalancingTarget` will enable dynamic url discovery via ribbon including incrementing server request counts.
#### Usage
instead of
```java
MyService api = Feign.create(MyService.class, "https://myAppProd-1234567890.us-east-1.elb.amazonaws.com");
```
do
```java
MyService api = Feign.create(LoadBalancingTarget.create(MyService.class, "https://myAppProd"));
```

153
feign-ribbon/src/main/java/feign/ribbon/LBClient.java

@ -0,0 +1,153 @@ @@ -0,0 +1,153 @@
/*
* Copyright 2013 Netflix, Inc.
*
* 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
*
* 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 feign.ribbon;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.netflix.client.AbstractLoadBalancerAwareClient;
import com.netflix.client.ClientException;
import com.netflix.client.ClientRequest;
import com.netflix.client.IResponse;
import com.netflix.client.config.CommonClientConfigKey;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.util.Pair;
import java.io.IOException;
import java.net.URI;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.core.MultivaluedMap;
import feign.Client;
import feign.Request;
import feign.RequestTemplate;
import feign.Response;
import feign.RetryableException;
import static com.netflix.client.config.CommonClientConfigKey.ConnectTimeout;
import static com.netflix.client.config.CommonClientConfigKey.ReadTimeout;
class LBClient extends AbstractLoadBalancerAwareClient<LBClient.RibbonRequest, LBClient.RibbonResponse> {
private final Client delegate;
private final int connectTimeout;
private final int readTimeout;
LBClient(Client delegate, ILoadBalancer lb, IClientConfig clientConfig) {
this.delegate = delegate;
this.connectTimeout = Integer.valueOf(clientConfig.getProperty(ConnectTimeout).toString());
this.readTimeout = Integer.valueOf(clientConfig.getProperty(ReadTimeout).toString());
setLoadBalancer(lb);
initWithNiwsConfig(clientConfig);
}
@Override
public RibbonResponse execute(RibbonRequest request) throws IOException {
int connectTimeout = config(request, ConnectTimeout, this.connectTimeout);
int readTimeout = config(request, ReadTimeout, this.readTimeout);
Request.Options options = new Request.Options(connectTimeout, readTimeout);
Response response = delegate.execute(request.toRequest(), options);
return new RibbonResponse(request.getUri(), response);
}
@Override protected boolean isCircuitBreakerException(Exception e) {
return e instanceof IOException;
}
@Override protected boolean isRetriableException(Exception e) {
return e instanceof RetryableException;
}
@Override
protected Pair<String, Integer> deriveSchemeAndPortFromPartialUri(RibbonRequest task) {
return new Pair<String, Integer>(URI.create(task.request.url()).getScheme(), task.getUri().getPort());
}
@Override protected int getDefaultPort() {
return 443;
}
static class RibbonRequest extends ClientRequest implements Cloneable {
private final Request request;
RibbonRequest(Request request, URI uri) {
this.request = request;
setUri(uri);
}
Request toRequest() {
return new RequestTemplate()
.method(request.method())
.append(getUri().toASCIIString())
.headers(request.headers())
.body(request.body().orNull()).request();
}
public Object clone() {
return new RibbonRequest(request, getUri());
}
}
static class RibbonResponse implements IResponse {
private final URI uri;
private final Response response;
RibbonResponse(URI uri, Response response) {
this.uri = uri;
this.response = response;
}
@Override public Object getPayload() throws ClientException {
return response.body().orNull();
}
@Override public boolean hasPayload() {
return response.body().isPresent();
}
@Override public boolean isSuccess() {
return response.status() == 200;
}
@Override public URI getRequestedURI() {
return uri;
}
@Override public Map<String, Collection<String>> getHeaders() {
return response.headers().asMap();
}
Response toResponse() {
return response;
}
}
static int config(RibbonRequest request, CommonClientConfigKey key, int defaultValue) {
if (request.getOverrideConfig() != null && request.getOverrideConfig().containsProperty(key))
return Integer.valueOf(request.getOverrideConfig().getProperty(key).toString());
return defaultValue;
}
}

114
feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java

@ -0,0 +1,114 @@ @@ -0,0 +1,114 @@
/*
* Copyright 2013 Netflix, Inc.
*
* 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
*
* 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 feign.ribbon;
import com.google.common.base.Objects;
import com.netflix.loadbalancer.AbstractLoadBalancer;
import com.netflix.loadbalancer.Server;
import java.net.URI;
import feign.Request;
import feign.RequestTemplate;
import feign.Target;
import static com.google.common.base.Objects.equal;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.netflix.client.ClientFactory.getNamedLoadBalancer;
import static java.lang.String.format;
/**
* Basic integration for {@link com.netflix.loadbalancer.ILoadBalancer loadbalancer-aware} targets.
* Using this will enable dynamic url discovery via ribbon including incrementing server request counts.
* <p/>
* Ex.
* <pre>
* MyService api = Feign.create(LoadBalancingTarget.create(MyService.class, "http://myAppProd"))
* </pre>
* Where {@code myAppProd} is the ribbon loadbalancer name and {@code myAppProd.ribbon.listOfServers} configuration
* is set.
*
* @param <T> corresponds to {@link feign.Target#type()}
*/
public class LoadBalancingTarget<T> implements Target<T> {
/**
* creates a target which dynamically derives urls from a {@link com.netflix.loadbalancer.ILoadBalancer loadbalancer}.
*
* @param type corresponds to {@link feign.Target#type()}
* @param schemeName naming convention is {@code https://name} or {@code http://name} where
* name corresponds to {@link com.netflix.client.ClientFactory#getNamedLoadBalancer(String)}
*/
public static <T> LoadBalancingTarget<T> create(Class<T> type, String schemeName) {
URI asUri = URI.create(schemeName);
return new LoadBalancingTarget<T>(type, asUri.getScheme(), asUri.getHost());
}
private final String name;
private final String scheme;
private final Class<T> type;
private final AbstractLoadBalancer lb;
protected LoadBalancingTarget(Class<T> type, String scheme, String name) {
this.type = checkNotNull(type, "type");
this.scheme = checkNotNull(scheme, "scheme");
this.name = checkNotNull(name, "name");
this.lb = AbstractLoadBalancer.class.cast(getNamedLoadBalancer(name()));
}
@Override public Class<T> type() {
return type;
}
@Override public String name() {
return name;
}
@Override public String url() {
return name;
}
/**
* current load balancer for the target.
*/
public AbstractLoadBalancer lb() {
return lb;
}
@Override public Request apply(RequestTemplate input) {
Server currentServer = lb.chooseServer(null);
String url = format("%s://%s", scheme, currentServer.getHostPort());
input.insert(0, url);
try {
return input.request();
} finally {
lb.getLoadBalancerStats().incrementNumRequests(currentServer);
}
}
@Override public int hashCode() {
return Objects.hashCode(type, name);
}
@Override public boolean equals(Object obj) {
if (this == obj)
return true;
if (LoadBalancingTarget.class != obj.getClass())
return false;
LoadBalancingTarget<?> that = LoadBalancingTarget.class.cast(obj);
return equal(this.type, that.type) && equal(this.name, that.name);
}
}

89
feign-ribbon/src/main/java/feign/ribbon/RibbonModule.java

@ -0,0 +1,89 @@ @@ -0,0 +1,89 @@
/*
* Copyright 2013 Netflix, Inc.
*
* 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
*
* 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 feign.ribbon;
import com.google.common.base.Throwables;
import com.netflix.client.ClientException;
import com.netflix.client.ClientFactory;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ILoadBalancer;
import java.io.IOException;
import java.net.URI;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import dagger.Provides;
import feign.Client;
import feign.Request;
import feign.Response;
/**
* Adding this module will override URL resolution of {@link feign.Client Feign's client},
* adding smart routing and resiliency capabilities provided by Ribbon.
* <p/>
* When using this, ensure the {@link feign.Target#url()} is set to as {@code http://clientName}
* or {@code https://clientName}. {@link com.netflix.client.config.IClientConfig#getClientName() clientName}
* will lookup the real url and port of your service dynamically.
* <p/>
* Ex.
* <pre>
* MyService api = Feign.create(MyService.class, "http://myAppProd", new RibbonModule());
* </pre>
* Where {@code myAppProd} is the ribbon client name and {@code myAppProd.ribbon.listOfServers} configuration
* is set.
*/
@dagger.Module(overrides = true, library = true, complete = false)
public class RibbonModule {
@Provides @Named("delegate") Client delegate(Client.Default delegate) {
return delegate;
}
@Provides @Singleton Client httpClient(RibbonClient ribbon) {
return ribbon;
}
@Singleton
static class RibbonClient implements Client {
private final Client delegate;
@Inject
public RibbonClient(@Named("delegate") Client delegate) {
this.delegate = delegate;
}
@Override public Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutSchemeAndPort = URI.create(request.url().replace(asUri.getScheme() + "://" + asUri.getHost(), ""));
LBClient.RibbonRequest ribbonRequest = new LBClient.RibbonRequest(request, uriWithoutSchemeAndPort);
return lbClient(clientName).executeWithLoadBalancer(ribbonRequest).toResponse();
} catch (ClientException e) {
throw Throwables.propagate(e);
}
}
private LBClient lbClient(String clientName) {
IClientConfig config = ClientFactory.getNamedConfig(clientName);
ILoadBalancer lb = ClientFactory.getNamedLoadBalancer(clientName);
return new LBClient(delegate, lb, config);
}
}
}

74
feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java

@ -0,0 +1,74 @@ @@ -0,0 +1,74 @@
/*
* Copyright 2013 Netflix, Inc.
*
* 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
*
* 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 feign.ribbon;
import com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.MockWebServer;
import org.testng.annotations.Test;
import java.io.IOException;
import java.net.URL;
import javax.ws.rs.POST;
import feign.Feign;
import static com.netflix.config.ConfigurationManager.getConfigInstance;
import static org.testng.Assert.assertEquals;
@Test
public class LoadBalancingTargetTest {
static interface TestInterface {
@POST void post();
}
@Test
public void loadBalancingDefaultPolicyRoundRobin() throws IOException, InterruptedException {
String name = "LoadBalancingTargetTest-loadBalancingDefaultPolicyRoundRobin";
String serverListKey = name + ".ribbon.listOfServers";
MockWebServer server1 = new MockWebServer();
server1.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server1.play();
MockWebServer server2 = new MockWebServer();
server2.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server2.play();
getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl("")));
try {
LoadBalancingTarget<TestInterface> target = LoadBalancingTarget.create(TestInterface.class, "http://" + name);
TestInterface api = Feign.create(target);
api.post();
api.post();
assertEquals(server1.getRequestCount(), 1);
assertEquals(server2.getRequestCount(), 1);
// TODO: verify ribbon stats match
// assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat())
} finally {
server1.shutdown();
server2.shutdown();
getConfigInstance().clearProperty(serverListKey);
}
}
static String hostAndPort(URL url) {
return url.getHost() + ":" + url.getPort();
}
}

74
feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java

@ -0,0 +1,74 @@ @@ -0,0 +1,74 @@
/*
* Copyright 2013 Netflix, Inc.
*
* 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
*
* 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 feign.ribbon;
import com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.MockWebServer;
import org.testng.annotations.Test;
import java.io.IOException;
import java.net.URL;
import javax.ws.rs.POST;
import feign.Feign;
import static com.netflix.config.ConfigurationManager.getConfigInstance;
import static org.testng.Assert.assertEquals;
@Test
public class RibbonClientTest {
static interface TestInterface {
@POST void post();
}
@Test
public void loadBalancingDefaultPolicyRoundRobin() throws IOException, InterruptedException {
String client = "RibbonClientTest-loadBalancingDefaultPolicyRoundRobin";
String serverListKey = client + ".ribbon.listOfServers";
MockWebServer server1 = new MockWebServer();
server1.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server1.play();
MockWebServer server2 = new MockWebServer();
server2.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server2.play();
getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl("")));
try {
TestInterface api = Feign.create(TestInterface.class, "http://" + client, new RibbonModule());
api.post();
api.post();
assertEquals(server1.getRequestCount(), 1);
assertEquals(server2.getRequestCount(), 1);
// TODO: verify ribbon stats match
// assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat())
} finally {
server1.shutdown();
server2.shutdown();
getConfigInstance().clearProperty(serverListKey);
}
}
static String hostAndPort(URL url) {
return url.getHost() + ":" + url.getPort();
}
}

2
settings.gradle

@ -1,2 +1,2 @@ @@ -1,2 +1,2 @@
rootProject.name='feign'
include 'feign-core'
include 'feign-core', 'feign-ribbon'

Loading…
Cancel
Save