Commit cc53a1d0 by Johannes Edmeier

Improve handling for hystrix

* Since the hystrix.stream incorrectly handles HEAD requests we had to change the detection for the hystrix-endpoint to a GET request. Issue https://github.com/Netflix/Hystrix/issues/1369 * We need to improve Zuul's SimpleHostRoutingFilter and SendResponseFilter to close the underlying socket when an error occurs when writing the response. I hope that the code can be removen when my PR gets merged. Issue https://github.com/spring-cloud/spring-cloud-netflix/pull/1372 Fixes #290
parent 996c9de3
......@@ -43,7 +43,27 @@ module.config(function ($stateProvider) {
});
module.run(function (ApplicationViews, $sce, $q, $http) {
var isEventSourceAvailable = function (url) {
var deferred = $q.defer();
$http.get(url, {
eventHandlers: {
'progress': function (event) {
deferred.resolve(event.target.status === 200);
event.target.abort();
},
'error': function () {
deferred.resolve(false);
}
}
});
return deferred.promise;
};
ApplicationViews.register({
order: 150,
title: $sce.trustAsHtml('<i class="fa fa-gear fa-fw"></i>Hystrix'),
......@@ -52,11 +72,7 @@ module.run(function (ApplicationViews, $sce, $q, $http) {
if (!application.managementUrl || !application.statusInfo.status || application.statusInfo.status === 'OFFLINE') {
return false;
}
return $http.head('api/applications/' + application.id + '/hystrix.stream').then(function () {
return true;
}).catch(function () {
return false;
});
return isEventSourceAvailable('api/applications/' + application.id + '/hystrix.stream');
}
});
ApplicationViews.register({
......@@ -67,11 +83,7 @@ module.run(function (ApplicationViews, $sce, $q, $http) {
if (!application.managementUrl || !application.statusInfo.status || application.statusInfo.status === 'OFFLINE') {
return false;
}
return $http.head('api/applications/' + application.id + '/turbine.stream').then(function () {
return true;
}).catch(function () {
return false;
});
return isEventSourceAvailable('api/applications/' + application.id + '/turbine.stream');
}
});
});
......@@ -27,6 +27,11 @@
<artifactId>zuul-core</artifactId>
</dependency>
<dependency>
<!-- Remove when https://github.com/spring-cloud/spring-cloud-netflix/pull/1372 is resolved -->
<groupId>com.netflix.netflix-commons</groupId>
<artifactId>netflix-commons-util</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
</dependency>
......
......@@ -26,8 +26,8 @@ import org.springframework.cloud.netflix.zuul.ZuulConfiguration;
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.cloud.netflix.zuul.filters.TraceProxyRequestHelper;
import org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter;
import org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter;
import org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter;
import org.springframework.cloud.netflix.zuul.web.ZuulController;
import org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping;
import org.springframework.context.ApplicationEvent;
......@@ -40,6 +40,7 @@ import de.codecentric.boot.admin.event.RoutesOutdatedEvent;
import de.codecentric.boot.admin.registry.ApplicationRegistry;
import de.codecentric.boot.admin.zuul.ApplicationRouteLocator;
import de.codecentric.boot.admin.zuul.OptionsDispatchingZuulController;
import de.codecentric.boot.admin.zuul.filters.route.SimpleHostRoutingFilter;
@Configuration
@AutoConfigureAfter({ AdminServerWebConfiguration.class })
......@@ -94,6 +95,11 @@ public class RevereseZuulProxyConfiguration extends ZuulConfiguration {
return new SimpleHostRoutingFilter(proxyRequestHelper(), zuulProperties);
}
@Override
public SendResponseFilter sendResponseFilter() {
return new de.codecentric.boot.admin.zuul.filters.post.SendResponseFilter();
}
@Bean
@Override
public ApplicationListener<ApplicationEvent> zuulRefreshRoutesListener() {
......@@ -109,6 +115,7 @@ public class RevereseZuulProxyConfiguration extends ZuulConfiguration {
}
}
private static class ZuulRefreshListener implements ApplicationListener<ApplicationEvent> {
private ZuulHandlerMapping zuulHandlerMapping;
......
/*
* Copyright 2013-2015 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 de.codecentric.boot.admin.zuul.filters.post;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.zip.GZIPInputStream;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ReflectionUtils;
import com.netflix.config.DynamicBooleanProperty;
import com.netflix.config.DynamicIntProperty;
import com.netflix.config.DynamicPropertyFactory;
import com.netflix.util.Pair;
import com.netflix.zuul.constants.ZuulConstants;
import com.netflix.zuul.constants.ZuulHeaders;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.util.HTTPRequestUtils;
/**
* Copy from sprig cloud netflix which is part of a fix for
* https://github.com/spring-cloud/spring-cloud-netflix/pull/1372
*
* @author Johannes Edmeier
* @author Spencer Gibb
*/
public class SendResponseFilter
extends org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter {
private static final Logger log = LoggerFactory.getLogger(SendResponseFilter.class);
private static DynamicBooleanProperty INCLUDE_DEBUG_HEADER = DynamicPropertyFactory
.getInstance()
.getBooleanProperty(ZuulConstants.ZUUL_INCLUDE_DEBUG_HEADER, false);
private static DynamicIntProperty INITIAL_STREAM_BUFFER_SIZE = DynamicPropertyFactory
.getInstance()
.getIntProperty(ZuulConstants.ZUUL_INITIAL_STREAM_BUFFER_SIZE, 1024);
private static DynamicBooleanProperty SET_CONTENT_LENGTH = DynamicPropertyFactory
.getInstance()
.getBooleanProperty(ZuulConstants.ZUUL_SET_CONTENT_LENGTH, false);
@Override
public String filterType() {
return "post";
}
@Override
public int filterOrder() {
return 1000;
}
@Override
public boolean shouldFilter() {
return !RequestContext.getCurrentContext().getZuulResponseHeaders().isEmpty()
|| RequestContext.getCurrentContext().getResponseDataStream() != null
|| RequestContext.getCurrentContext().getResponseBody() != null;
}
@Override
public Object run() {
try {
addResponseHeaders();
writeResponse();
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
private void writeResponse() throws Exception {
RequestContext context = RequestContext.getCurrentContext();
// there is no body to send
if (context.getResponseBody() == null
&& context.getResponseDataStream() == null) {
return;
}
HttpServletResponse servletResponse = context.getResponse();
if (servletResponse.getCharacterEncoding() == null) { // only set if not set
servletResponse.setCharacterEncoding("UTF-8");
}
OutputStream outStream = servletResponse.getOutputStream();
InputStream is = null;
try {
if (RequestContext.getCurrentContext().getResponseBody() != null) {
String body = RequestContext.getCurrentContext().getResponseBody();
writeResponse(
new ByteArrayInputStream(
body.getBytes(servletResponse.getCharacterEncoding())),
outStream);
return;
}
boolean isGzipRequested = false;
final String requestEncoding = context.getRequest()
.getHeader(ZuulHeaders.ACCEPT_ENCODING);
if (requestEncoding != null
&& HTTPRequestUtils.getInstance().isGzipped(requestEncoding)) {
isGzipRequested = true;
}
is = context.getResponseDataStream();
InputStream inputStream = is;
if (is != null) {
if (context.sendZuulResponse()) {
// if origin response is gzipped, and client has not requested gzip,
// decompress stream
// before sending to client
// else, stream gzip directly to client
if (context.getResponseGZipped() && !isGzipRequested) {
// If origin tell it's GZipped but the content is ZERO bytes,
// don't try to uncompress
final Long len = context.getOriginContentLength();
if (len == null || len > 0) {
try {
inputStream = new GZIPInputStream(is);
}
catch (java.util.zip.ZipException ex) {
log.debug(
"gzip expected but not "
+ "received assuming unencoded response "
+ RequestContext.getCurrentContext()
.getRequest().getRequestURL()
.toString());
inputStream = is;
}
}
else {
// Already done : inputStream = is;
}
}
else if (context.getResponseGZipped() && isGzipRequested) {
servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip");
}
writeResponse(inputStream, outStream);
}
}
}
finally {
try {
Object zuulResponse = RequestContext.getCurrentContext().get("zuulResponse");
if (zuulResponse instanceof Closeable) {
((Closeable) zuulResponse).close();
}
outStream.flush();
// The container will close the stream for us
}
catch (IOException ex) {
}
}
}
private void writeResponse(InputStream zin, OutputStream out) throws Exception {
byte[] bytes = new byte[INITIAL_STREAM_BUFFER_SIZE.get()];
int bytesRead = -1;
while ((bytesRead = zin.read(bytes)) != -1) {
out.write(bytes, 0, bytesRead);
out.flush();
// doubles buffer size if previous read filled it
if (bytesRead == bytes.length) {
bytes = new byte[bytes.length * 2];
}
}
}
private void addResponseHeaders() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletResponse servletResponse = context.getResponse();
List<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders();
@SuppressWarnings("unchecked")
List<String> rd = (List<String>) RequestContext.getCurrentContext()
.get("routingDebug");
if (rd != null) {
StringBuilder debugHeader = new StringBuilder();
for (String it : rd) {
debugHeader.append("[[[" + it + "]]]");
}
if (INCLUDE_DEBUG_HEADER.get()) {
servletResponse.addHeader("X-Zuul-Debug-Header", debugHeader.toString());
}
}
if (zuulResponseHeaders != null) {
for (Pair<String, String> it : zuulResponseHeaders) {
servletResponse.addHeader(it.first(), it.second());
}
}
RequestContext ctx = RequestContext.getCurrentContext();
Long contentLength = ctx.getOriginContentLength();
// Only inserts Content-Length if origin provides it and origin response is not
// gzipped
if (SET_CONTENT_LENGTH.get()) {
if (contentLength != null && !ctx.getResponseGZipped()) {
servletResponse.setContentLengthLong(contentLength);
}
}
}
}
/*
* Copyright 2013-2015 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 de.codecentric.boot.admin.zuul.filters.post;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.UndeclaredThrowableException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.util.WebUtils;
import com.netflix.zuul.context.RequestContext;
/**
* Copy from sprig cloud netflix which is part of a fix for
* https://github.com/spring-cloud/spring-cloud-netflix/pull/1372
*
* @author Johannes Edmeier
* @author Spencer Gibb
*/
public class SendResponseFilterTests {
@Before
public void setTestRequestcontext() {
RequestContext context = new RequestContext();
RequestContext.testSetCurrentContext(context);
}
@After
public void reset() {
RequestContext.getCurrentContext().clear();
}
@Test
public void runsNormally() throws Exception {
String characterEncoding = null;
String content = "hello";
runFilter(characterEncoding, content, false);
}
@Test
public void characterEncodingNotOverridden() throws Exception {
String characterEncoding = "UTF-16";
String content = "\u00a5";
runFilter(characterEncoding, content, true);
}
@Test
public void closeResponseOutpusStreamError() throws Exception {
HttpServletResponse response = mock(HttpServletResponse.class);
RequestContext context = new RequestContext();
context.setRequest(new MockHttpServletRequest());
context.setResponse(response);
context.setResponseDataStream(
new ByteArrayInputStream("Hello\n".getBytes("UTF-8")));
CloseableHttpResponse zuulResponse = mock(CloseableHttpResponse.class);
context.set("zuulResponse", zuulResponse);
RequestContext.testSetCurrentContext(context);
SendResponseFilter filter = new SendResponseFilter();
ServletOutputStream zuuloutputstream = mock(ServletOutputStream.class);
doThrow(new IOException("Response to client closed")).when(zuuloutputstream)
.write(isA(byte[].class), anyInt(), anyInt());
when(response.getOutputStream()).thenReturn(zuuloutputstream);
try {
filter.run();
}
catch (UndeclaredThrowableException ex) {
assertThat(ex.getUndeclaredThrowable().getMessage(),
is("Response to client closed"));
}
verify(zuulResponse).close();
}
private void runFilter(String characterEncoding, String content, boolean streamContent) throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
SendResponseFilter filter = createFilter(content, characterEncoding, response, streamContent);
assertTrue("shouldFilter returned false", filter.shouldFilter());
filter.run();
String encoding = RequestContext.getCurrentContext().getResponse().getCharacterEncoding();
String expectedEncoding = characterEncoding != null ? characterEncoding : WebUtils.DEFAULT_CHARACTER_ENCODING;
assertThat("wrong character encoding", encoding, equalTo(expectedEncoding));
assertThat("wrong content", response.getContentAsString(), equalTo(content));
}
private SendResponseFilter createFilter(String content, String characterEncoding, MockHttpServletResponse response, boolean streamContent) throws Exception {
HttpServletRequest request = new MockHttpServletRequest();
RequestContext context = new RequestContext();
context.setRequest(request);
context.setResponse(response);
if (characterEncoding != null) {
response.setCharacterEncoding(characterEncoding);
}
if (streamContent) {
context.setResponseDataStream(new ByteArrayInputStream(content.getBytes(characterEncoding)));
} else {
context.setResponseBody(content);
}
context.addZuulResponseHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(content.length()));
context.set("error.status_code", HttpStatus.NOT_FOUND.value());
RequestContext.testSetCurrentContext(context);
SendResponseFilter filter = new SendResponseFilter();
return filter;
}
}
\ No newline at end of file
/*
* Copyright 2013-2016 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 de.codecentric.boot.admin.zuul.filters.route;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.springframework.boot.test.util.EnvironmentTestUtils.addEnvironment;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.junit.After;
import org.junit.Test;
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Copy from sprig cloud netflix which is part of a fix for
* https://github.com/spring-cloud/spring-cloud-netflix/pull/1372
*
* @author Andreas Kluth
* @author Spencer Gibb
*/
public class SimpleHostRoutingFilterTests {
private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@After
public void clear() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void connectionPropertiesAreApplied() {
addEnvironment(this.context, "zuul.host.maxTotalConnections=100", "zuul.host.maxPerRouteConnections=10");
setupContext();
PoolingHttpClientConnectionManager connMgr = getFilter().newConnectionManager();
assertEquals(100, connMgr.getMaxTotal());
assertEquals(10, connMgr.getDefaultMaxPerRoute());
}
@Test
public void validateSslHostnamesByDefault() {
setupContext();
assertTrue("Hostname verification should be enabled by default",
getFilter().isSslHostnameValidationEnabled());
}
@Test
public void validationOfSslHostnamesCanBeDisabledViaProperty() {
addEnvironment(this.context, "zuul.sslHostnameValidationEnabled=false");
setupContext();
assertFalse("Hostname verification should be disabled via property",
getFilter().isSslHostnameValidationEnabled());
}
@Test
public void defaultPropertiesAreApplied() {
setupContext();
PoolingHttpClientConnectionManager connMgr = getFilter().newConnectionManager();
assertEquals(200, connMgr.getMaxTotal());
assertEquals(20, connMgr.getDefaultMaxPerRoute());
}
private void setupContext() {
this.context.register(PropertyPlaceholderAutoConfiguration.class,
TestConfiguration.class);
this.context.refresh();
}
private SimpleHostRoutingFilter getFilter() {
return this.context.getBean(SimpleHostRoutingFilter.class);
}
@Configuration
@EnableConfigurationProperties(ZuulProperties.class)
protected static class TestConfiguration {
@Bean
SimpleHostRoutingFilter simpleHostRoutingFilter(ZuulProperties zuulProperties) {
return new SimpleHostRoutingFilter(new ProxyRequestHelper(), zuulProperties);
}
}
}
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