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

com.twitter.finagle.http2.Http2Transporter.scala Maven / Gradle / Ivy

There is a newer version: 6.39.0
Show newest version
package com.twitter.finagle.http2

import com.twitter.finagle.Http.{param => httpparam}
import com.twitter.finagle.{Stack, Status}
import com.twitter.finagle.client.Transporter
import com.twitter.finagle.http2.transport.{
  Http2ClientDowngrader,
  Http2UpgradingTransport,
  SchemifyingHandler,
  UpgradeRequestHandler
}
import com.twitter.finagle.netty4.Netty4Transporter
import com.twitter.finagle.netty4.http.exp.{initClient, Netty4HttpTransporter}
import com.twitter.finagle.transport.{Transport, TransportProxy}
import com.twitter.logging.Logger
import com.twitter.util.{Future, Throw, Return, Promise}
import io.netty.channel.ChannelPipeline
import io.netty.handler.codec.http._
import io.netty.handler.codec.http2._
import java.net.SocketAddress
import java.util.concurrent.ConcurrentHashMap

private[http2] object Http2Transporter {

  def apply(params: Stack.Params): Transporter[Any, Any] = {
    // current http2 client implementation doesn't support
    // netty-style backpressure
    // https://github.com/netty/netty/issues/3667#issue-69640214
    val underlying = Netty4Transporter[Any, Any](
      init(params),
      params + Netty4Transporter.Backpressure(false)
    )

    val underlyingHttp11 = Netty4HttpTransporter(params)

    new Http2Transporter(underlying, underlyingHttp11)
  }

  // constructing an http2 cleartext transport
  private[http2] def init(params: Stack.Params): ChannelPipeline => Unit =
    { pipeline: ChannelPipeline =>
      val connection = new DefaultHttp2Connection(false /*server*/)

      val maxResponseSize = params[httpparam.MaxResponseSize].size

      // decompresses data frames according to the content-encoding header
      val adapter = new DelegatingDecompressorFrameListener(
        connection,
        // adapts http2 to http 1.1
        new Http2ClientDowngrader(connection)
      )
      val connectionHandler = new HttpToHttp2ConnectionHandlerBuilder()
        .frameListener(adapter)
        .connection(connection)
        .build()

      val maxChunkSize = params[httpparam.MaxChunkSize].size
      val maxHeaderSize = params[httpparam.MaxHeaderSize].size
      val maxInitialLineSize = params[httpparam.MaxInitialLineSize].size

      val sourceCodec = new HttpClientCodec(
        maxInitialLineSize.inBytes.toInt,
        maxHeaderSize.inBytes.toInt,
        maxChunkSize.inBytes.toInt
      )
      val upgradeCodec = new Http2ClientUpgradeCodec(connectionHandler)
      val bufferedWrites = new UpgradeRequestHandler()
      val upgradeHandler = new HttpClientUpgradeHandler(sourceCodec, upgradeCodec, Int.MaxValue)

      pipeline.addLast(sourceCodec, upgradeHandler, bufferedWrites)
      pipeline.addLast("schemifier", new SchemifyingHandler("http"))
      initClient(params)(pipeline)
    }

  def unsafeCast(t: Transport[HttpObject, HttpObject]): Transport[Any, Any] =
    t.map(_.asInstanceOf[HttpObject], _.asInstanceOf[Any])
}

/**
 * This `Transporter` makes `Transports` that speak netty http/1.1, but write
 * http/2 to the wire if they can.
 *
 * The `Transports` this hands out should be used serially, and will upgrade to
 * http/2 if possible.  It caches connections associated with a given socket
 * address, so that you can increase concurrency over a single connection by
 * getting another transport, the same way that serial protocols increase
 * concurrency.  Plainly, these transports have the same semantics as serial
 * netty4 http/1.1 transports, but allows them to be multiplexed under the hood.
 *
 * Since the decision on whether to multiplex a connection or not is made after
 * knowing the result of an upgrade, Http2Transporter is also in charge of
 * upgrading, and caches upgrade results.
 *
 * Since the cleartext upgrade is a request, it's possible to write another
 * request while the upgrade is in progress, which it does over http/1.1, and
 * doesn't attempt to upgrade.
 */
private[http2] class Http2Transporter(
    underlying: Transporter[Any, Any],
    underlyingHttp11: Transporter[Any, Any])
  extends Transporter[Any, Any] {

  private[this] val log = Logger.get()

  import Http2Transporter._

  protected[this] val transporterCache =
    new ConcurrentHashMap[SocketAddress, Future[Option[() => Transport[HttpObject, HttpObject]]]]()

  private[this] def onUpgradeFinished(
    f: Future[Option[() => Transport[HttpObject, HttpObject]]],
    addr: SocketAddress
  ): Future[Transport[Any, Any]] = f.transform {
    case Return(Some(fn)) => Future.value(unsafeCast(fn()))
    case Return(None) =>
      // we didn't upgrade
      underlyingHttp11(addr)
    case Throw(exn) =>
      // kick us out of the cache so we can try to reestablish the connection
      transporterCache.remove(addr, f)
      apply(addr)
  }

  // uses http11 underneath, but marks itself as dead if the upgrade succeeds or
  // the connection fails
  private[this] def onUpgradeInProgress(
    f: Future[Option[_]]
  ): Transport[Any, Any] => Transport[Any, Any] = { http11: Transport[Any, Any] =>
    val ref = new RefTransport(http11)
    f.respond {
      case Return(None) => // we failed the upgrade, so we can keep the connection
      case _ =>
        // we shut down if we pass the upgrade or fail entirely
        // we're assuming here that a well behaved dispatcher will propagate our status to a pool
        // that will close us
        // TODO: if we fail entirely, we should try to reregister on the new connection attempt
        ref.update { trans =>
          new TransportProxy[Any, Any](trans) {
            def write(any: Any): Future[Unit] = trans.write(any)
            def read(): Future[Any] = trans.read()
            override def status: Status = Status.Closed
          }
        }
    }
    ref
  }

  private[this] def upgrade(addr: SocketAddress): Future[Transport[Any, Any]] = {
    val conn: Future[Transport[Any, Any]] = underlying(addr)
    val p = Promise[Option[() => Transport[HttpObject, HttpObject]]]()
    if (transporterCache.putIfAbsent(addr, p) == null) {
      conn.transform {
        case Return(trans) =>
          val ref = new RefTransport(trans)
          ref.update { t =>
            t.onClose.ensure {
              transporterCache.remove(addr, p)
            }
            new Http2UpgradingTransport(t, ref, p)
          }
          Future.value(ref)
        case Throw(e) =>
          p.setException(e)
          Future.exception(e)
      }
    } else apply(addr)
  }

  final def apply(addr: SocketAddress): Future[Transport[Any, Any]] = Option(transporterCache.get(addr)) match {
    case Some(f) =>
      if (f.isDefined) {
        onUpgradeFinished(f, addr)
      } else {
        // fall back to http/1.1 while upgrading
        val conn = underlyingHttp11(addr)
        conn.map(onUpgradeInProgress(f))
      }
    case None =>
      upgrade(addr)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy