Commit 05a3db3c by Andre Dörnbrack Committed by Ryan Baxter

Fix zuul post retry (#2209)

Modfied RibbonCommandContext to make request entity resettable. fixes gh-892
parent 145b7ce6
...@@ -2685,6 +2685,9 @@ certain Ribbon properties. The properties you can use are ...@@ -2685,6 +2685,9 @@ certain Ribbon properties. The properties you can use are
`client.ribbon.OkToRetryOnAllOperations`. See the https://github.com/Netflix/ribbon/wiki/Getting-Started#the-properties-file-sample-clientproperties[Ribbon documentation] `client.ribbon.OkToRetryOnAllOperations`. See the https://github.com/Netflix/ribbon/wiki/Getting-Started#the-properties-file-sample-clientproperties[Ribbon documentation]
for a description of what there properties do. for a description of what there properties do.
WARNING: Enabling `client.ribbon.OkToRetryOnAllOperations` includes retring POST requests wich can have a impact
on the server's resources due to the buffering of the request's body.
In addition you may want to retry requests when certain status codes are returned in the In addition you may want to retry requests when certain status codes are returned in the
response. You can list the response codes you would like the Ribbon client to retry using the response. You can list the response codes you would like the Ribbon client to retry using the
property `clientName.ribbon.retryableStatusCodes`. For example property `clientName.ribbon.retryableStatusCodes`. For example
......
...@@ -16,6 +16,13 @@ ...@@ -16,6 +16,13 @@
package org.springframework.cloud.netflix.zuul.filters.route; package org.springframework.cloud.netflix.zuul.filters.route;
import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer;
import org.springframework.cloud.netflix.zuul.filters.support.ResettableServletInputStreamWrapper;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StreamUtils;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
...@@ -23,11 +30,6 @@ import java.util.ArrayList; ...@@ -23,11 +30,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
*/ */
...@@ -38,16 +40,16 @@ public class RibbonCommandContext { ...@@ -38,16 +40,16 @@ public class RibbonCommandContext {
private final Boolean retryable; private final Boolean retryable;
private final MultiValueMap<String, String> headers; private final MultiValueMap<String, String> headers;
private final MultiValueMap<String, String> params; private final MultiValueMap<String, String> params;
private final InputStream requestEntity;
private final List<RibbonRequestCustomizer> requestCustomizers; private final List<RibbonRequestCustomizer> requestCustomizers;
private InputStream requestEntity;
private Long contentLength; private Long contentLength;
/** /**
* Kept for backwards compatibility with Spring Cloud Sleuth 1.x versions * Kept for backwards compatibility with Spring Cloud Sleuth 1.x versions
*/ */
@Deprecated @Deprecated
public RibbonCommandContext(String serviceId, String method, String uri, public RibbonCommandContext(String serviceId, String method,
Boolean retryable, MultiValueMap<String, String> headers, String uri, Boolean retryable, MultiValueMap<String, String> headers,
MultiValueMap<String, String> params, InputStream requestEntity) { MultiValueMap<String, String> params, InputStream requestEntity) {
this(serviceId, method, uri, retryable, headers, params, requestEntity, this(serviceId, method, uri, retryable, headers, params, requestEntity,
new ArrayList<RibbonRequestCustomizer>(), null); new ArrayList<RibbonRequestCustomizer>(), null);
...@@ -57,7 +59,8 @@ public class RibbonCommandContext { ...@@ -57,7 +59,8 @@ public class RibbonCommandContext {
Boolean retryable, MultiValueMap<String, String> headers, Boolean retryable, MultiValueMap<String, String> headers,
MultiValueMap<String, String> params, InputStream requestEntity, MultiValueMap<String, String> params, InputStream requestEntity,
List<RibbonRequestCustomizer> requestCustomizers) { List<RibbonRequestCustomizer> requestCustomizers) {
this(serviceId, method, uri, retryable, headers, params, requestEntity, requestCustomizers, null); this(serviceId, method, uri, retryable, headers, params, requestEntity,
requestCustomizers, null);
} }
public RibbonCommandContext(String serviceId, String method, String uri, public RibbonCommandContext(String serviceId, String method, String uri,
...@@ -84,8 +87,7 @@ public class RibbonCommandContext { ...@@ -84,8 +87,7 @@ public class RibbonCommandContext {
public URI uri() { public URI uri() {
try { try {
return new URI(this.uri); return new URI(this.uri);
} } catch (URISyntaxException e) {
catch (URISyntaxException e) {
ReflectionUtils.rethrowRuntimeException(e); ReflectionUtils.rethrowRuntimeException(e);
} }
return null; return null;
...@@ -93,6 +95,7 @@ public class RibbonCommandContext { ...@@ -93,6 +95,7 @@ public class RibbonCommandContext {
/** /**
* Use getMethod() * Use getMethod()
*
* @return * @return
*/ */
@Deprecated @Deprecated
...@@ -125,8 +128,20 @@ public class RibbonCommandContext { ...@@ -125,8 +128,20 @@ public class RibbonCommandContext {
} }
public InputStream getRequestEntity() { public InputStream getRequestEntity() {
if (requestEntity == null) {
return requestEntity;
}
try {
if (!(requestEntity instanceof ResettableServletInputStreamWrapper)) {
requestEntity = new ResettableServletInputStreamWrapper(
StreamUtils.copyToByteArray(requestEntity));
}
requestEntity.reset();
} finally {
return requestEntity; return requestEntity;
} }
}
public List<RibbonRequestCustomizer> getRequestCustomizers() { public List<RibbonRequestCustomizer> getRequestCustomizers() {
return requestCustomizers; return requestCustomizers;
...@@ -142,23 +157,25 @@ public class RibbonCommandContext { ...@@ -142,23 +157,25 @@ public class RibbonCommandContext {
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o)
if (o == null || getClass() != o.getClass()) return false; return true;
if (o == null || getClass() != o.getClass())
return false;
RibbonCommandContext that = (RibbonCommandContext) o; RibbonCommandContext that = (RibbonCommandContext) o;
return Objects.equals(serviceId, that.serviceId) && return Objects.equals(serviceId, that.serviceId) && Objects
Objects.equals(method, that.method) && .equals(method, that.method) && Objects.equals(uri, that.uri)
Objects.equals(uri, that.uri) && && Objects.equals(retryable, that.retryable) && Objects
Objects.equals(retryable, that.retryable) && .equals(headers, that.headers) && Objects
Objects.equals(headers, that.headers) && .equals(params, that.params) && Objects
Objects.equals(params, that.params) && .equals(requestEntity, that.requestEntity) && Objects
Objects.equals(requestEntity, that.requestEntity) && .equals(requestCustomizers, that.requestCustomizers) && Objects
Objects.equals(requestCustomizers, that.requestCustomizers) && .equals(contentLength, that.contentLength);
Objects.equals(contentLength, that.contentLength);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(serviceId, method, uri, retryable, headers, params, requestEntity, requestCustomizers, contentLength); return Objects.hash(serviceId, method, uri, retryable, headers, params,
requestEntity, requestCustomizers, contentLength);
} }
@Override @Override
......
/*
* Copyright 2013-2017 the original author or authors.
*
* 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 org.springframework.cloud.netflix.zuul.filters.support;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
public class ResettableServletInputStreamWrapper extends ServletInputStream {
private final ByteArrayInputStream input;
public ResettableServletInputStreamWrapper(byte[] data) {
this.input = new ByteArrayInputStream(data);
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() throws IOException {
return input.read();
}
@Override
public synchronized void reset() throws IOException {
input.reset();
}
}
package org.springframework.cloud.netflix.zuul.filters.route;
import com.google.common.collect.Lists;
import okhttp3.Request;
import org.junit.Test;
import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer;
import org.springframework.cloud.netflix.zuul.filters.support.ResettableServletInputStreamWrapper;
import org.springframework.http.HttpMethod;
import org.springframework.util.LinkedMultiValueMap;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
/**
* @author Andre Dörnbrack
*/
public class RibbonCommandContextTest {
private static final byte[] TEST_CONTENT = { 42, 42, 42, 42, 42 };
private RibbonCommandContext ribbonCommandContext;
@Test
public void testMultipleReadsOnRequestEntity() throws Exception {
givenRibbonCommandContextIsSetup();
InputStream requestEntity = ribbonCommandContext.getRequestEntity();
assertTrue(requestEntity instanceof ResettableServletInputStreamWrapper);
whenInputStreamIsConsumed(requestEntity);
assertEquals(-1, requestEntity.read());
requestEntity.reset();
assertNotEquals(-1, requestEntity.read());
whenInputStreamIsConsumed(requestEntity);
assertEquals(-1, requestEntity.read());
requestEntity.reset();
assertNotEquals(-1, requestEntity.read());
whenInputStreamIsConsumed(requestEntity);
assertEquals(-1, requestEntity.read());
}
private void whenInputStreamIsConsumed(InputStream requestEntity) throws IOException {
while (requestEntity.read() != -1) {
requestEntity.read();
}
}
private void givenRibbonCommandContextIsSetup() {
LinkedMultiValueMap headers = new LinkedMultiValueMap();
LinkedMultiValueMap params = new LinkedMultiValueMap();
RibbonRequestCustomizer requestCustomizer = new RibbonRequestCustomizer<Request.Builder>() {
@Override
public boolean accepts(Class builderClass) {
return builderClass == Request.Builder.class;
}
@Override
public void customize(Request.Builder builder) {
builder.addHeader("from-customizer", "foo");
}
};
ribbonCommandContext = new RibbonCommandContext("serviceId",
HttpMethod.POST.toString(), "/my/route", true, headers, params,
new ByteArrayInputStream(TEST_CONTENT),
Lists.newArrayList(requestCustomizer));
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment