Skip to content

Commit f5fe710

Browse files
author
adriancole
committed
Added Ribbon integration
1 parent 3440c63 commit f5fe710

File tree

10 files changed

+572
-4
lines changed

10 files changed

+572
-4
lines changed

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
### Version 1.1.0
2+
* adds Ribbon integration
3+
4+
### Version 1.0.0
5+
6+
* Initial open source release

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ CloudDNS cloudDNS = Feign.create().newInstance(new CloudIdentityTarget<CloudDNS
6565
```
6666

6767
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!
68+
69+
### Integrations
70+
Feign intends to work well within Netflix and other Open Source communities. Modules are welcome to integrate with your favorite projects!
71+
### Ribbon
72+
[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).
73+
74+
Integration requires you to pass your ribbon client name as the host part of the url, for example `myAppProd`.
75+
```java
76+
MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonModule());
77+
```
6878
### Advanced usage and Dagger
6979
#### Dagger
7080
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.

build.gradle

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ ext.githubProjectName = rootProject.name // Change if github project name is not
44
buildscript {
55
repositories {
66
mavenLocal()
7-
mavenCentral() // maven { url 'http://jcenter.bintray.com' }
7+
mavenCentral()
88
}
99
apply from: file('gradle/buildscript.gradle'), to: buildscript
1010
}
1111

1212
allprojects {
1313
repositories {
14-
mavenCentral() // maven { url: 'http://jcenter.bintray.com' }
14+
mavenLocal()
15+
mavenCentral()
16+
maven { url 'https://oss.sonatype.org/content/repositories/releases/' }
1517
}
1618
}
1719

@@ -42,4 +44,20 @@ project(':feign-core') {
4244
testCompile 'org.testng:testng:6.8.1'
4345
testCompile 'com.google.mockwebserver:mockwebserver:20130505'
4446
}
45-
}
47+
}
48+
49+
project(':feign-ribbon') {
50+
apply plugin: 'java'
51+
52+
test {
53+
useTestNG()
54+
}
55+
56+
dependencies {
57+
compile project(':feign-core')
58+
compile 'com.netflix.ribbon:ribbon-core:0.2.0'
59+
provided 'com.squareup.dagger:dagger-compiler:1.0.1'
60+
testCompile 'org.testng:testng:6.8.1'
61+
testCompile 'com.google.mockwebserver:mockwebserver:20130505'
62+
}
63+
}

feign-ribbon/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Ribbon
2+
This module includes a feign `Target` and `Client` adapter to take advantage of [Ribbon](https://github.com/Netflix/ribbon).
3+
4+
## Conventions
5+
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.
6+
7+
### RibbonModule
8+
Adding `RibbonModule` overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by Ribbon.
9+
10+
#### Usage
11+
instead of 
12+
```java
13+
MyService api = Feign.create(MyService.class, "https://myAppProd-1234567890.us-east-1.elb.amazonaws.com");
14+
```
15+
do
16+
```java
17+
MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonModule());
18+
```
19+
### LoadBalancingTarget
20+
Using or extending `LoadBalancingTarget` will enable dynamic url discovery via ribbon including incrementing server request counts.
21+
22+
#### Usage
23+
instead of
24+
```java
25+
MyService api = Feign.create(MyService.class, "https://myAppProd-1234567890.us-east-1.elb.amazonaws.com");
26+
```
27+
do
28+
```java
29+
MyService api = Feign.create(LoadBalancingTarget.create(MyService.class, "https://myAppProd"));
30+
```
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Copyright 2013 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package feign.ribbon;
17+
18+
import com.google.common.collect.ImmutableListMultimap;
19+
import com.google.common.collect.ImmutableSet;
20+
import com.google.common.collect.Iterables;
21+
import com.google.common.collect.LinkedListMultimap;
22+
import com.google.common.collect.ListMultimap;
23+
import com.netflix.client.AbstractLoadBalancerAwareClient;
24+
import com.netflix.client.ClientException;
25+
import com.netflix.client.ClientRequest;
26+
import com.netflix.client.IResponse;
27+
import com.netflix.client.config.CommonClientConfigKey;
28+
import com.netflix.client.config.IClientConfig;
29+
import com.netflix.loadbalancer.ILoadBalancer;
30+
import com.netflix.util.Pair;
31+
32+
import java.io.IOException;
33+
import java.net.URI;
34+
import java.util.Collection;
35+
import java.util.List;
36+
import java.util.Map;
37+
import java.util.Set;
38+
39+
import javax.ws.rs.core.MultivaluedMap;
40+
41+
import feign.Client;
42+
import feign.Request;
43+
import feign.RequestTemplate;
44+
import feign.Response;
45+
import feign.RetryableException;
46+
47+
import static com.netflix.client.config.CommonClientConfigKey.ConnectTimeout;
48+
import static com.netflix.client.config.CommonClientConfigKey.ReadTimeout;
49+
50+
class LBClient extends AbstractLoadBalancerAwareClient<LBClient.RibbonRequest, LBClient.RibbonResponse> {
51+
52+
private final Client delegate;
53+
private final int connectTimeout;
54+
private final int readTimeout;
55+
56+
LBClient(Client delegate, ILoadBalancer lb, IClientConfig clientConfig) {
57+
this.delegate = delegate;
58+
this.connectTimeout = Integer.valueOf(clientConfig.getProperty(ConnectTimeout).toString());
59+
this.readTimeout = Integer.valueOf(clientConfig.getProperty(ReadTimeout).toString());
60+
setLoadBalancer(lb);
61+
initWithNiwsConfig(clientConfig);
62+
}
63+
64+
@Override
65+
public RibbonResponse execute(RibbonRequest request) throws IOException {
66+
int connectTimeout = config(request, ConnectTimeout, this.connectTimeout);
67+
int readTimeout = config(request, ReadTimeout, this.readTimeout);
68+
69+
Request.Options options = new Request.Options(connectTimeout, readTimeout);
70+
Response response = delegate.execute(request.toRequest(), options);
71+
return new RibbonResponse(request.getUri(), response);
72+
}
73+
74+
@Override protected boolean isCircuitBreakerException(Exception e) {
75+
return e instanceof IOException;
76+
}
77+
78+
@Override protected boolean isRetriableException(Exception e) {
79+
return e instanceof RetryableException;
80+
}
81+
82+
@Override
83+
protected Pair<String, Integer> deriveSchemeAndPortFromPartialUri(RibbonRequest task) {
84+
return new Pair<String, Integer>(URI.create(task.request.url()).getScheme(), task.getUri().getPort());
85+
}
86+
87+
@Override protected int getDefaultPort() {
88+
return 443;
89+
}
90+
91+
static class RibbonRequest extends ClientRequest implements Cloneable {
92+
93+
private final Request request;
94+
95+
RibbonRequest(Request request, URI uri) {
96+
this.request = request;
97+
setUri(uri);
98+
}
99+
100+
Request toRequest() {
101+
return new RequestTemplate()
102+
.method(request.method())
103+
.append(getUri().toASCIIString())
104+
.headers(request.headers())
105+
.body(request.body().orNull()).request();
106+
}
107+
108+
public Object clone() {
109+
return new RibbonRequest(request, getUri());
110+
}
111+
}
112+
113+
static class RibbonResponse implements IResponse {
114+
115+
private final URI uri;
116+
private final Response response;
117+
118+
RibbonResponse(URI uri, Response response) {
119+
this.uri = uri;
120+
this.response = response;
121+
}
122+
123+
@Override public Object getPayload() throws ClientException {
124+
return response.body().orNull();
125+
}
126+
127+
@Override public boolean hasPayload() {
128+
return response.body().isPresent();
129+
}
130+
131+
@Override public boolean isSuccess() {
132+
return response.status() == 200;
133+
}
134+
135+
@Override public URI getRequestedURI() {
136+
return uri;
137+
}
138+
139+
@Override public Map<String, Collection<String>> getHeaders() {
140+
return response.headers().asMap();
141+
}
142+
143+
Response toResponse() {
144+
return response;
145+
}
146+
}
147+
148+
static int config(RibbonRequest request, CommonClientConfigKey key, int defaultValue) {
149+
if (request.getOverrideConfig() != null && request.getOverrideConfig().containsProperty(key))
150+
return Integer.valueOf(request.getOverrideConfig().getProperty(key).toString());
151+
return defaultValue;
152+
}
153+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright 2013 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package feign.ribbon;
17+
18+
import com.google.common.base.Objects;
19+
import com.netflix.loadbalancer.AbstractLoadBalancer;
20+
import com.netflix.loadbalancer.Server;
21+
22+
import java.net.URI;
23+
24+
import feign.Request;
25+
import feign.RequestTemplate;
26+
import feign.Target;
27+
28+
import static com.google.common.base.Objects.equal;
29+
import static com.google.common.base.Preconditions.checkNotNull;
30+
import static com.netflix.client.ClientFactory.getNamedLoadBalancer;
31+
import static java.lang.String.format;
32+
33+
/**
34+
* Basic integration for {@link com.netflix.loadbalancer.ILoadBalancer loadbalancer-aware} targets.
35+
* Using this will enable dynamic url discovery via ribbon including incrementing server request counts.
36+
* <p/>
37+
* Ex.
38+
* <pre>
39+
* MyService api = Feign.create(LoadBalancingTarget.create(MyService.class, "http://myAppProd"))
40+
* </pre>
41+
* Where {@code myAppProd} is the ribbon loadbalancer name and {@code myAppProd.ribbon.listOfServers} configuration
42+
* is set.
43+
*
44+
* @param <T> corresponds to {@link feign.Target#type()}
45+
*/
46+
public class LoadBalancingTarget<T> implements Target<T> {
47+
48+
/**
49+
* creates a target which dynamically derives urls from a {@link com.netflix.loadbalancer.ILoadBalancer loadbalancer}.
50+
*
51+
* @param type corresponds to {@link feign.Target#type()}
52+
* @param schemeName naming convention is {@code https://name} or {@code http://name} where
53+
* name corresponds to {@link com.netflix.client.ClientFactory#getNamedLoadBalancer(String)}
54+
*/
55+
public static <T> LoadBalancingTarget<T> create(Class<T> type, String schemeName) {
56+
URI asUri = URI.create(schemeName);
57+
return new LoadBalancingTarget<T>(type, asUri.getScheme(), asUri.getHost());
58+
}
59+
60+
private final String name;
61+
private final String scheme;
62+
private final Class<T> type;
63+
private final AbstractLoadBalancer lb;
64+
65+
protected LoadBalancingTarget(Class<T> type, String scheme, String name) {
66+
this.type = checkNotNull(type, "type");
67+
this.scheme = checkNotNull(scheme, "scheme");
68+
this.name = checkNotNull(name, "name");
69+
this.lb = AbstractLoadBalancer.class.cast(getNamedLoadBalancer(name()));
70+
}
71+
72+
@Override public Class<T> type() {
73+
return type;
74+
}
75+
76+
@Override public String name() {
77+
return name;
78+
}
79+
80+
@Override public String url() {
81+
return name;
82+
}
83+
84+
/**
85+
* current load balancer for the target.
86+
*/
87+
public AbstractLoadBalancer lb() {
88+
return lb;
89+
}
90+
91+
@Override public Request apply(RequestTemplate input) {
92+
Server currentServer = lb.chooseServer(null);
93+
String url = format("%s://%s", scheme, currentServer.getHostPort());
94+
input.insert(0, url);
95+
try {
96+
return input.request();
97+
} finally {
98+
lb.getLoadBalancerStats().incrementNumRequests(currentServer);
99+
}
100+
}
101+
102+
@Override public int hashCode() {
103+
return Objects.hashCode(type, name);
104+
}
105+
106+
@Override public boolean equals(Object obj) {
107+
if (this == obj)
108+
return true;
109+
if (LoadBalancingTarget.class != obj.getClass())
110+
return false;
111+
LoadBalancingTarget<?> that = LoadBalancingTarget.class.cast(obj);
112+
return equal(this.type, that.type) && equal(this.name, that.name);
113+
}
114+
}

0 commit comments

Comments
 (0)