/** * Copyright 2016-2017 Sixt GmbH & Co. Autovermietung KG * 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 com.sixt.service.framework.rpc; import com.google.inject.Inject; import com.sixt.service.framework.OrangeContext; import com.sixt.service.framework.ServiceProperties; import com.sixt.service.framework.metrics.GoTimer; import io.opentracing.Span; import io.opentracing.SpanContext; import io.opentracing.Tracer; import io.opentracing.propagation.Format; import io.opentracing.propagation.TextMapInjectAdapter; import io.opentracing.tag.Tags; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.Marker; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import static com.sixt.service.framework.FeatureFlags.shouldExposeErrorsToHttp; import static net.logstash.logback.marker.Markers.append; public class HttpClientWrapper { private static final Logger logger = LoggerFactory.getLogger(HttpClientWrapper.class); protected ServiceProperties serviceProps; protected LoadBalancer loadBalancer; protected HttpClient httpClient; protected RpcClientMetrics rpcClientMetrics; protected RpcClient client; protected Tracer tracer; @Inject public HttpClientWrapper(ServiceProperties serviceProps, HttpClient httpClient, RpcClientMetrics rpcClientMetrics, Tracer tracer) { this.serviceProps = serviceProps; this.httpClient = httpClient; this.rpcClientMetrics = rpcClientMetrics; this.tracer = tracer; } public HttpRequestWrapper createHttpPost(RpcClient client) throws RpcCallException { this.client = client; ServiceEndpoint instance = loadBalancer.getHealthyInstance(); if (instance == null) { throw new RpcCallException(RpcCallException.Category.InternalServerError, "No available instance of " + loadBalancer.getServiceName()). withSource(serviceProps.getServiceName()); } return new HttpRequestWrapper("POST", instance); } private HttpRequestWrapper createHttpPost(HttpRequestWrapper previous, List<ServiceEndpoint> triedEndpoints) throws RpcCallException { ServiceEndpoint instance = loadBalancer.getHealthyInstanceExclude(triedEndpoints); if (instance == null) { throw new RpcCallException(RpcCallException.Category.InternalServerError, "RpcCallException calling " + loadBalancer.getServiceName() + ", no available instance"). withSource(serviceProps.getServiceName()); } //TODO: There may still be a problem where retries are setting chunked encoding // or the content-length gets munged HttpRequestWrapper retval = new HttpRequestWrapper("POST", instance); retval.setHeaders(previous.getHeaders()); retval.setContentProvider(previous.getContentProvider()); return retval; } public void setLoadBalancer(LoadBalancer loadBalancer) { this.loadBalancer = loadBalancer; } public ContentResponse execute(HttpRequestWrapper request, RpcCallExceptionDecoder decoder, OrangeContext orangeContext) throws RpcCallException { ContentResponse retval = null; Span span = null; List<ServiceEndpoint> triedEndpoints = new ArrayList<>(); RpcCallException lastException = null; int lastStatusCode; int tryCount = 0; do { triedEndpoints.add(request.getServiceEndpoint()); GoTimer methodTimer = getMethodTimer(); long startTime = methodTimer.start(); try { Marker logMarker = append("serviceMethod", request.getMethod()) .and(append("serviceEndpoint", request.getServiceEndpoint())); logger.debug(logMarker, "Sending http request to {}", request.getServiceEndpoint()); if (tracer != null) { SpanContext spanContext = null; if (orangeContext != null) { spanContext = orangeContext.getTracingContext(); } if (spanContext != null) { span = tracer.buildSpan(client.getMethodName()).asChildOf(spanContext).start(); } else { span = tracer.buildSpan(client.getMethodName()).start(); } Tags.SPAN_KIND.set(span, Tags.SPAN_KIND_CLIENT); Tags.PEER_SERVICE.set(span, loadBalancer.getServiceName()); if (orangeContext != null) { span.setTag("correlation_id", orangeContext.getCorrelationId()); } tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new TextMapInjectAdapter(request.getHeaders())); } retval = request.newRequest(httpClient).timeout(client.getTimeout(), TimeUnit.MILLISECONDS).send(); logger.debug(logMarker, "Http send completed"); lastStatusCode = retval.getStatus(); } catch (TimeoutException timeout) { lastStatusCode = RpcCallException.Category.RequestTimedOut.getHttpStatus(); lastException = new RpcCallException(RpcCallException.Category.RequestTimedOut, "Http-client timeout"); //TODO: RequestTimedOut should be retried as long as there is time budget left logger.info(getRemoteMethod(), "Caught TimeoutException executing request"); } catch (Exception ex) { lastStatusCode = RpcCallException.Category.InternalServerError.getHttpStatus(); logger.debug(getRemoteMethod(), "Caught exception executing request", ex); } //content.length must always be > 0, because we have an envelope if (responseWasSuccessful(decoder, retval, lastStatusCode)) { if (span != null) { Tags.HTTP_STATUS.set(span, lastStatusCode); span.finish(); } methodTimer.recordSuccess(startTime); request.getServiceEndpoint().requestComplete(true); return retval; } else { if (span != null) { Tags.HTTP_STATUS.set(span, lastStatusCode); Tags.ERROR.set(span, true); span.finish(); } methodTimer.recordFailure(startTime); //4xx errors should not change circuit-breaker state request.getServiceEndpoint().requestComplete(lastStatusCode < 500); if (lastStatusCode != RpcCallException.Category.RequestTimedOut.getHttpStatus()) { lastException = decoder.decodeException(retval); if (lastException != null && !lastException.isRetriable()) { throw lastException; } } if (tryCount < client.getRetries()) { request = createHttpPost(request, triedEndpoints); } } tryCount++; } while (request != null && tryCount <= client.getRetries()); if (lastException == null) { throw new RpcCallException(RpcCallException.Category.fromStatus(lastStatusCode), "Null response in execute").withSource(serviceProps.getServiceName()); } else { throw lastException; } } private boolean responseWasSuccessful(RpcCallExceptionDecoder decoder, ContentResponse response, int lastStatusCode) throws RpcCallException { if (shouldExposeErrorsToHttp(serviceProps)) { return lastStatusCode == 200 && response != null && response.getContent().length > 0; } else if (lastStatusCode != 0 && lastStatusCode != 200) { return false; } else if (response == null || response.getContent() == null) { return false; } RpcCallException exception = decoder.decodeException(response); return (exception == null); } private GoTimer getMethodTimer() { return rpcClientMetrics.getMethodTimer(client.getServiceName(), client.getMethodName()); } private Marker getRemoteMethod() { return append("method", client.getServiceMethodName()); } }