com.netflix.prana.http.api.ProxyHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.netflix.prana.http.api.ProxyHandler.java

Source

/*
 * Copyright 2014 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 com.netflix.prana.http.api;

import com.google.common.base.Strings;
import com.netflix.client.config.IClientConfig;
import com.netflix.client.config.IClientConfigKey;
import com.netflix.config.DynamicProperty;
import com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList;
import com.netflix.ribbon.transport.netty.RibbonTransport;
import com.netflix.ribbon.transport.netty.http.LoadBalancingHttpClient;
import io.netty.buffer.ByteBuf;
import io.reactivex.netty.protocol.http.client.HttpClientPipelineConfigurator;
import io.reactivex.netty.protocol.http.client.HttpClientRequest;
import io.reactivex.netty.protocol.http.client.HttpClientResponse;
import io.reactivex.netty.protocol.http.server.HttpServerRequest;
import io.reactivex.netty.protocol.http.server.HttpServerResponse;
import io.reactivex.netty.protocol.http.server.RequestHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rx.Observable;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class ProxyHandler implements RequestHandler<ByteBuf, ByteBuf> {

    private static final ConcurrentHashMap<String, LoadBalancingHttpClient<ByteBuf, ByteBuf>> httpClients = new ConcurrentHashMap<>();

    private final String PROXY_REQ_ACCEPT_ENCODING = DynamicProperty.getInstance("prana.proxy.req.acceptencoding")
            .getString("deflate, gzip");

    private Logger logger = LoggerFactory.getLogger(getClass().getName());

    private final String ERROR_RESPONSE = "<status><status_code>500</status_code><message>Error forwarding request to origin</message></status>";

    @Override
    public Observable<Void> handle(final HttpServerRequest<ByteBuf> serverRequest,
            final HttpServerResponse<ByteBuf> serverResponse) {
        String vip = Utils.forQueryParam(serverRequest.getQueryParameters(), "vip");
        String path = Utils.forQueryParam(serverRequest.getQueryParameters(), "path");
        if (Strings.isNullOrEmpty(vip)) {
            serverResponse.getHeaders().set("Content-Type", "application/xml");
            serverResponse.writeString(ERROR_RESPONSE);
            logger.error("VIP is empty");
            return serverResponse.close();
        }

        if (path == null) {
            path = "";
        }

        final LoadBalancingHttpClient<ByteBuf, ByteBuf> client = getClient(vip);
        final HttpClientRequest<ByteBuf> req = HttpClientRequest.create(serverRequest.getHttpMethod(), path);
        populateRequestHeaders(serverRequest, req);

        final UnicastDisposableCachingSubject<ByteBuf> cachedContent = UnicastDisposableCachingSubject.create();
        /**
         * Why do we retain here?
         * After the onNext on the content returns, RxNetty releases the sent ByteBuf. This ByteBuf is kept out of
         * the scope of the onNext for consumption of the client in the route. The client when eventually writes
         * this ByteBuf over the wire expects the ByteBuf to be usable (i.e. ref count => 1). If this retain() call
         * is removed, the ref count will be 0 after the onNext on the content returns and hence it will be unusable
         * by the client in the route.
         */
        serverRequest.getContent().map(new Func1<ByteBuf, ByteBuf>() {
            @Override
            public ByteBuf call(ByteBuf byteBuf) {
                return byteBuf.retain();
            }
        }).subscribe(cachedContent); // Caches data if arrived before client writes it out, else passes through

        req.withContentSource(cachedContent);

        return client.submit(req).flatMap(new Func1<HttpClientResponse<ByteBuf>, Observable<Void>>() {
            @Override
            public Observable<Void> call(final HttpClientResponse<ByteBuf> response) {
                serverResponse.setStatus(response.getStatus());
                List<Map.Entry<String, String>> headers = response.getHeaders().entries();
                for (Map.Entry<String, String> header : headers) {
                    serverResponse.getHeaders().add(header.getKey(), header.getValue());
                }
                return response.getContent().map(new Func1<ByteBuf, ByteBuf>() {
                    @Override
                    public ByteBuf call(ByteBuf byteBuf) {
                        return byteBuf.retain();
                    }
                }).map(new Func1<ByteBuf, Void>() {
                    @Override
                    public Void call(ByteBuf byteBuf) {
                        serverResponse.write(byteBuf);
                        return null;
                    }
                });
            }
        }).onErrorResumeNext(new Func1<Throwable, Observable<Void>>() {
            @Override
            public Observable<Void> call(Throwable throwable) {
                serverResponse.getHeaders().set("Content-Type", "application/xml");
                serverResponse.writeString(ERROR_RESPONSE);
                return Observable.just(null);
            }
        }).doOnCompleted(new Action0() {
            @Override
            public void call() {
                serverResponse.close();
                cachedContent.dispose(new Action1<ByteBuf>() {
                    @Override
                    public void call(ByteBuf byteBuf) {
                        /**
                         * Why do we release here?
                         *
                         * All ByteBuf which were never consumed are disposed and sent here. This means that the
                         * client in the route never consumed this ByteBuf. Before sending this ByteBuf to the
                         * content subject, we do a retain (see above for reason) expecting the client in the route
                         * to release it when written over the wire. In this case, though, the client never consumed
                         * it and hence never released corresponding to the retain done by us.
                         */
                        if (byteBuf.refCnt() > 1) { // 1 refCount will be from the subject putting into the cache.
                            byteBuf.release();
                        }
                    }
                });
            }
        });
    }

    private void populateRequestHeaders(HttpServerRequest<ByteBuf> serverRequest,
            HttpClientRequest<ByteBuf> request) {
        Set<String> headerNames = serverRequest.getHeaders().names();
        for (String name : headerNames) {
            if (name.contains("content-length")) {
                continue;
            }
            request.getHeaders().add(name, serverRequest.getHeaders().getHeader(name));
        }
        // Normally always request gzipped from the server. But can be overridden with a Dynamic Property.
        if (PROXY_REQ_ACCEPT_ENCODING != null && PROXY_REQ_ACCEPT_ENCODING.length() > 0) {
            request.getHeaders().addHeader("accept-encoding", PROXY_REQ_ACCEPT_ENCODING);
        }
        //TODO Write X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto, X-Forwarded-For in the headers
    }

    private LoadBalancingHttpClient<ByteBuf, ByteBuf> getClient(String vip) {
        LoadBalancingHttpClient<ByteBuf, ByteBuf> client = httpClients.get(vip);
        if (client == null) {
            IClientConfig config = IClientConfig.Builder.newBuilder("prana_backend").withDefaultValues()
                    .withDeploymentContextBasedVipAddresses(vip).build()
                    .set(IClientConfigKey.Keys.MaxTotalConnections, 2000)
                    .set(IClientConfigKey.Keys.MaxConnectionsPerHost, 2000)
                    .set(IClientConfigKey.Keys.OkToRetryOnAllOperations, false)
                    .set(IClientConfigKey.Keys.NIWSServerListClassName,
                            DiscoveryEnabledNIWSServerList.class.getName());

            client = RibbonTransport.newHttpClient(new HttpClientPipelineConfigurator<ByteBuf, ByteBuf>(), config);
            httpClients.putIfAbsent(vip, client);

        }
        return client;
    }

}