adriancole
11 years ago
10 changed files with 572 additions and 4 deletions
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
### Version 1.1.0 |
||||
* adds Ribbon integration |
||||
|
||||
### Version 1.0.0 |
||||
|
||||
* Initial open source release |
@ -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; |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
Loading…
Reference in new issue