All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ratpack.http.client.internal.DefaultHttpClient Maven / Gradle / Ivy

There is a newer version: 2.0.0-rc-1
Show newest version
/*
 * Copyright 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 ratpack.http.client.internal;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelOption;
import io.netty.channel.pool.ChannelHealthChecker;
import io.netty.channel.pool.ChannelPool;
import io.netty.channel.pool.ChannelPoolMap;
import io.netty.channel.pool.SimpleChannelPool;
import io.netty.resolver.AddressResolverGroup;
import ratpack.api.Nullable;
import ratpack.exec.ExecController;
import ratpack.exec.Execution;
import ratpack.exec.Promise;
import ratpack.func.Action;
import ratpack.http.client.*;
import ratpack.util.internal.TransportDetector;

import java.net.URI;
import java.time.Duration;
import java.util.Map;
import java.util.stream.Collectors;

public class DefaultHttpClient implements HttpClientInternal {

  private static final ChannelHealthChecker ALWAYS_UNHEALTHY = channel ->
    channel.eventLoop().newSucceededFuture(Boolean.FALSE);

  final ByteBufAllocator byteBufAllocator;
  final int poolSize;
  final int poolQueueSize;
  final Duration idleTimeout;
  final int maxContentLength;
  final int responseMaxChunkSize;
  final Duration readTimeout;
  final Duration connectTimeout;
  final Action requestInterceptor;
  final Action responseInterceptor;
  final Action errorInterceptor;
  final boolean enableMetricsCollection;
  final AddressResolverGroup resolver;

  @Nullable
  final ProxyInternal proxy;

  private final Cache hostStats = Caffeine.newBuilder()
    .maximumSize(1024)
    .build();

  private final ManagedChannelPoolMap channelPoolMap;

  public DefaultHttpClient(
    ByteBufAllocator byteBufAllocator,
    int poolSize,
    int poolQueueSize,
    Duration idleTimeout,
    int maxContentLength,
    int responseMaxChunkSize,
    Duration readTimeout,
    Duration connectTimeout,
    Action requestInterceptor,
    Action responseInterceptor,
    Action errorInterceptor,
    boolean enableMetricsCollection,
    AddressResolverGroup resolver,
    @Nullable ProxyInternal proxy
  ) {
    this.byteBufAllocator = byteBufAllocator;
    this.poolSize = poolSize;
    this.poolQueueSize = poolQueueSize;
    this.idleTimeout = idleTimeout;
    this.maxContentLength = maxContentLength;
    this.responseMaxChunkSize = responseMaxChunkSize;
    this.readTimeout = readTimeout;
    this.connectTimeout = connectTimeout;
    this.requestInterceptor = requestInterceptor;
    this.responseInterceptor = responseInterceptor;
    this.errorInterceptor = errorInterceptor;
    this.enableMetricsCollection = enableMetricsCollection;
    this.resolver = resolver;
    this.proxy = proxy;

    this.channelPoolMap = isPooling() ? getPoolingChannelManager() : getSimpleChannelManager();
  }

  private ManagedChannelPoolMap getPoolingChannelManager() {
    final HttpChannelPoolMap channelPoolMap = new HttpChannelPoolMap() {
      @Override
      protected ChannelPool newPool(HttpChannelKey key) {
        Bootstrap bootstrap = createBootstrap(key, true);

        InstrumentedChannelPoolHandler channelPoolHandler = getPoolingHandler(key);
        if (enableMetricsCollection) {
          hostStats.put(key.host, channelPoolHandler);
        }
        CleanClosingFixedChannelPool channelPool = new CleanClosingFixedChannelPool(bootstrap, channelPoolHandler, getPoolSize(), getPoolQueueSize());
        key.execController.onClose(() -> {
          remove(key);
          channelPool.closeCleanly();
        });
        return channelPool;
      }
    };

    return channelPoolMap;
  }

  private ManagedChannelPoolMap getSimpleChannelManager() {
    return new ManagedChannelPoolMap() {

      @Override
      public void close() {

      }

      @Override
      public ChannelPool get(HttpChannelKey key) {
        Bootstrap bootstrap = createBootstrap(key, true);
        return new SimpleChannelPool(bootstrap, getSimpleHandler(key), ALWAYS_UNHEALTHY);
      }

      @Override
      public boolean contains(HttpChannelKey key) {
        return false;
      }
    };
  }

  private Bootstrap createBootstrap(HttpChannelKey key, boolean pooling) {
    Bootstrap bootstrap = new Bootstrap()
      .remoteAddress(key.host, key.port)
      .group(key.eventLoop)
      .resolver(resolver)
      .channel(TransportDetector.getSocketChannelImpl())
      .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) key.connectTimeout.toMillis())
      .option(ChannelOption.ALLOCATOR, byteBufAllocator)
      .option(ChannelOption.AUTO_READ, false)
      .option(ChannelOption.SO_KEEPALIVE, pooling);
    return bootstrap;
  }

  private InstrumentedChannelPoolHandler getPoolingHandler(HttpChannelKey key) {
    if (enableMetricsCollection) {
      return new InstrumentedFixedChannelPoolHandler(key, getPoolSize(), getIdleTimeout());
    } else {
      return new NoopFixedChannelPoolHandler(key, getIdleTimeout());
    }
  }

  private InstrumentedChannelPoolHandler getSimpleHandler(HttpChannelKey key) {
    if (enableMetricsCollection) {
      return new InstrumentedSimpleChannelPoolHandler(key);
    } else {
      return new NoopSimpleChannelPoolHandler(key);
    }
  }

  @Override
  public int getPoolSize() {
    return poolSize;
  }

  @Override
  public int getPoolQueueSize() {
    return poolQueueSize;
  }

  @Override
  public Duration getIdleTimeout() {
    return idleTimeout;
  }

  private boolean isPooling() {
    return getPoolSize() > 0;
  }

  @Override
  public ChannelPoolMap getChannelPoolMap() {
    return channelPoolMap;
  }

  @Override
  public Action getRequestInterceptor() {
    return requestInterceptor;
  }

  @Override
  public Action getResponseInterceptor() {
    return responseInterceptor;
  }

  public ByteBufAllocator getByteBufAllocator() {
    return byteBufAllocator;
  }

  public int getMaxContentLength() {
    return maxContentLength;
  }

  @Override
  public int getMaxResponseChunkSize() {
    return responseMaxChunkSize;
  }

  public Duration getReadTimeout() {
    return readTimeout;
  }

  public Duration getConnectTimeout() {
    return connectTimeout;
  }

  @Override
  public Proxy getProxy() {
    return proxy;
  }

  @Override
  public ProxyInternal getProxyInternal() {
    return proxy;
  }

  @Override
  public void close() {
    channelPoolMap.close();
  }

  @Override
  public HttpClient copyWith(Action action) throws Exception {
    HttpClientBuilder builder = new HttpClientBuilder(this);
    action.execute(builder);
    return builder.build();
  }

  @Override
  public Promise get(URI uri, Action action) {
    return request(uri, action);
  }

  @Override
  public Promise post(URI uri, Action action) {
    return request(uri, action.prepend(RequestSpec::post));
  }

  @Override
  public Promise request(URI uri, final Action requestConfigurer) {
    return intercept(
      Promise.async(downstream -> new ContentAggregatingRequestAction(uri, this, 0, false, Execution.current(), requestConfigurer.append(requestInterceptor)).connect(downstream)),
      responseInterceptor,
      errorInterceptor
    );
  }

  @Override
  public Promise requestStream(URI uri, Action requestConfigurer) {
    return intercept(
      Promise.async(downstream -> new ContentStreamingRequestAction(uri, this, 0, false, Execution.current(), requestConfigurer.append(requestInterceptor)).connect(downstream)),
      responseInterceptor,
      errorInterceptor
    );
  }

  private  Promise intercept(Promise promise, Action action, Action errorAction) {
    Promise returnPromise = promise;
    if (errorAction != Action.noop()) {
      returnPromise = promise.wiretap(r -> {
        if (r.isError()) {
          ExecController.require()
            .fork()
            .eventLoop(Execution.current().getEventLoop())
            .start(e ->
              errorAction.execute(r.getThrowable())
            );
        }
      });
    }
    if (action != Action.noop()) {
      returnPromise = returnPromise.next(r ->
        ExecController.require()
          .fork()
          .eventLoop(Execution.current().getEventLoop())
          .start(e ->
            action.execute(r)
          )
      );
    }

    return returnPromise;
  }

  public HttpClientStats getHttpClientStats() {
    return new HttpClientStats(
      hostStats.asMap().entrySet().stream().collect(Collectors.toMap(
        Map.Entry::getKey,
        e -> e.getValue().getHostStats()
      ))
    );
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy