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

wvlet.airframe.http.finagle.FinagleClient.scala Maven / Gradle / Ivy

There is a newer version: 24.9.0
Show newest version
/*
 * 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 wvlet.airframe.http.finagle

import java.util.concurrent.TimeUnit

import com.twitter.finagle.http.{Request, Response}
import com.twitter.finagle.{Http, Service, http}
import com.twitter.util._
import wvlet.airframe.codec.{MessageCodec, MessageCodecFactory}
import wvlet.airframe.control.Retry.RetryContext
import wvlet.airframe.http.HttpClient.urlEncode
import wvlet.airframe.http._
import wvlet.airframe.http.router.HttpResponseCodec
import wvlet.airframe.json.JSON.{JSONArray, JSONObject}
import wvlet.log.LogSupport

import scala.reflect.runtime.{universe => ru}
import scala.util.control.NonFatal

case class FinagleClientConfig(
    initClient: Http.Client => Http.Client = FinagleClient.defaultInitClient,
    requestFilter: http.Request => http.Request = identity,
    timeout: Duration = Duration(90, TimeUnit.SECONDS),
    retryContext: RetryContext = FinagleClient.defaultRetryContext
) {
  def withInitializer(initClient: Http.Client => Http.Client): FinagleClientConfig = {
    this.copy(initClient = initClient)
  }
  def withRetryContext(retryContext: RetryContext): FinagleClientConfig = {
    this.copy(retryContext = retryContext)
  }
  def withTimeout(timeout: Duration): FinagleClientConfig = {
    this.copy(timeout = timeout)
  }
  def withRequestFilter(requestFilter: http.Request => http.Request): FinagleClientConfig = {
    this.copy(requestFilter = requestFilter)
  }

  def noRetry: FinagleClientConfig = {
    this.copy(retryContext = retryContext.noRetry)
  }

  def withMaxRetry(maxRetry: Int): FinagleClientConfig = {
    this.copy(retryContext = retryContext.withMaxRetry(maxRetry))
  }

  def withBackOff(
      initialIntervalMillis: Int = 100,
      maxIntervalMillis: Int = 15000,
      multiplier: Double = 1.5
  ): FinagleClientConfig = {
    withRetryContext(retryContext.withBackOff(initialIntervalMillis, maxIntervalMillis, multiplier))
  }

  def withJitter(
      initialIntervalMillis: Int = 100,
      maxIntervalMillis: Int = 15000,
      multiplier: Double = 1.5
  ): FinagleClientConfig = {
    withRetryContext(
      retryContext.withJitter(initialIntervalMillis, maxIntervalMillis, multiplier)
    )
  }

  def newClient(hostAndPort: String): FinagleClient = {
    FinagleClient.newClient(hostAndPort, this)
  }

  def newSyncClient(hostAndPort: String): FinagleSyncClient = {
    FinagleClient.newSyncClient(hostAndPort, this)
  }
}

class FinagleClient(address: ServerAddress, config: FinagleClientConfig)
    extends HttpClient[Future, http.Request, http.Response]
    with LogSupport {
  private[this] val client = {
    val retryFilter                = new FinagleRetryFilter(config.retryContext)
    var finagleClient: Http.Client = config.initClient(Http.client)

    address.scheme.map {
      case "https" =>
        // Set TLS for http (443) connection
        finagleClient = finagleClient.withTls(address.host)
      case _ =>
    }
    debug(s"Starting a FinagleClient for ${address}")
    retryFilter andThen finagleClient.newService(address.hostAndPort)
  }

  // Use this method to access the native Finagle HTTP client
  def nativeClient: Service[Request, Response] = client

  override def send(req: Request, requestFilter: Request => Request = identity): Future[Response] = {
    // Apply the common filter in the config first, the apply the additional filter
    val request = requestFilter(config.requestFilter(req))
    // Add HOST header if missing
    if (request.host.isEmpty) {
      request.host = address.hostAndPort
    }
    client.apply(request)
  }

  /**
    * Send the request without applying any requestFilter
    */
  def sendRaw(req: Request): Future[Response] = {
    client.apply(req)
  }

  private def toRawUnsafe(resp: HttpResponse[_]): Response = {
    resp.asInstanceOf[HttpResponse[Response]].toRaw
  }

  override def sendSafe(req: Request, requestFilter: Request => Request = identity): Future[Response] = {
    try {
      send(req, requestFilter).rescue {
        case e: HttpClientException =>
          Future.value(toRawUnsafe(e.response))
      }
    } catch {
      case e: HttpClientException =>
        Future.value(toRawUnsafe(e.response))
      case NonFatal(e) =>
        Future.exception(e)
    }
  }

  def close: Unit = {
    debug(s"Closing FinagleClient for ${address}")
    client.close()
  }

  /**
    * Create a new Request
    */
  protected def newRequest(method: HttpMethod, path: String): Request = {
    Request(toFinagleHttpMethod(method), path)
  }

  /**
    * Await the result of Future[A]. It will throw an exception if some error happens
    */
  override private[http] def awaitF[A](f: Future[A]): A = {
    val r = Await.result(f, config.timeout)
    trace(r)
    r
  }

  private val codecFactory  = MessageCodecFactory.defaultFactoryForJSON
  private val responseCodec = new HttpResponseCodec[Response]

  private def convert[A: ru.TypeTag](response: Future[Response]): Future[A] = {
    if (implicitly[ru.TypeTag[A]] == ru.typeTag[Response]) {
      // Can return the response as is
      response.asInstanceOf[Future[A]]
    } else {
      // Need a conversion
      val codec = MessageCodec.of[A]
      response
        .map { r =>
          val msgpack = responseCodec.toMsgPack(r)
          codec.unpack(msgpack)
        }
    }
  }

  private def toJson[Resource: ru.TypeTag](resource: Resource): String = {
    val resourceCodec = codecFactory.of[Resource]
    // TODO: Support non-json content body
    val json = resourceCodec.toJson(resource)
    json
  }

  override def get[Resource: ru.TypeTag](
      resourcePath: String,
      requestFilter: Request => Request = identity
  ): Future[Resource] = {
    convert[Resource](send(newRequest(HttpMethod.GET, resourcePath), requestFilter))
  }

  override def getResource[ResourceRequest: ru.TypeTag, Resource: ru.TypeTag](
      resourcePath: String,
      resourceRequest: ResourceRequest,
      requestFilter: Request => Request = identity
  ): Future[Resource] = {
    // Read resource as JSON
    val resourceRequestJsonValue = codecFactory.of[ResourceRequest].toJSONObject(resourceRequest)
    val queryParams: Seq[String] =
      resourceRequestJsonValue.v.map {
        case (k, j @ JSONArray(_)) =>
          s"${urlEncode(k)}=${urlEncode(j.toJSON)}" // Flatten the JSON array value
        case (k, j @ JSONObject(_)) =>
          s"${urlEncode(k)}=${urlEncode(j.toJSON)}" // Flatten the JSON object value
        case (k, other) =>
          s"${urlEncode(k)}=${urlEncode(other.toString)}"
      }

    // Build query strings
    val pathWithQueryParam = new StringBuilder
    pathWithQueryParam.append(resourcePath)
    pathWithQueryParam.append("?")
    pathWithQueryParam.append(queryParams.mkString("&"))

    convert[Resource](send(newRequest(HttpMethod.GET, pathWithQueryParam.result()), requestFilter))
  }

  override def list[OperationResponse: ru.TypeTag](
      resourcePath: String,
      requestFilter: Request => Request = identity
  ): Future[OperationResponse] = {
    convert[OperationResponse](send(newRequest(HttpMethod.GET, resourcePath), requestFilter))
  }

  override def post[Resource: ru.TypeTag](
      resourcePath: String,
      resource: Resource,
      requestFilter: Request => Request = identity
  ): Future[Resource] = {
    val r = newRequest(HttpMethod.POST, resourcePath)
    r.setContentTypeJson()
    r.setContentString(toJson(resource))
    convert[Resource](send(r, requestFilter))
  }
  override def postRaw[Resource: ru.TypeTag](
      resourcePath: String,
      resource: Resource,
      requestFilter: Request => Request = identity
  ): Future[http.Response] = {
    postOps[Resource, http.Response](resourcePath, resource, requestFilter)
  }
  override def postOps[Resource: ru.TypeTag, OperationResponse: ru.TypeTag](
      resourcePath: String,
      resource: Resource,
      requestFilter: Request => Request = identity
  ): Future[OperationResponse] = {
    val r = newRequest(HttpMethod.POST, resourcePath)
    r.setContentTypeJson()
    r.setContentString(toJson(resource))
    convert[OperationResponse](send(r, requestFilter))
  }

  override def put[Resource: ru.TypeTag](
      resourcePath: String,
      resource: Resource,
      requestFilter: Request => Request = identity
  ): Future[Resource] = {
    val r = newRequest(HttpMethod.PUT, resourcePath)
    r.setContentTypeJson()
    r.setContentString(toJson(resource))
    convert[Resource](send(r, requestFilter))
  }
  override def putRaw[Resource: ru.TypeTag](
      resourcePath: String,
      resource: Resource,
      requestFilter: Request => Request = identity
  ): Future[http.Response] = {
    putOps[Resource, http.Response](resourcePath, resource, requestFilter)
  }
  override def putOps[Resource: ru.TypeTag, OperationResponse: ru.TypeTag](
      resourcePath: String,
      resource: Resource,
      requestFilter: Request => Request = identity
  ): Future[OperationResponse] = {
    val r = newRequest(HttpMethod.PUT, resourcePath)
    r.setContentTypeJson()
    r.setContentString(toJson(resource))
    convert[OperationResponse](send(r, requestFilter))
  }

  override def delete[OperationResponse: ru.TypeTag](
      resourcePath: String,
      requestFilter: Request => Request = identity
  ): Future[OperationResponse] = {
    convert[OperationResponse](send(newRequest(HttpMethod.DELETE, resourcePath), requestFilter))
  }
  override def deleteRaw(
      resourcePath: String,
      requestFilter: Request => Request = identity
  ): Future[http.Response] = {
    delete[http.Response](resourcePath, requestFilter)
  }
  override def deleteOps[Resource: ru.TypeTag, OperationResponse: ru.TypeTag](
      resourcePath: String,
      resource: Resource,
      requestFilter: Request => Request = identity
  ): Future[OperationResponse] = {
    val r = newRequest(HttpMethod.DELETE, resourcePath)
    r.setContentTypeJson()
    r.setContentString(toJson(resource))
    convert[OperationResponse](send(r, requestFilter))
  }

  override def patch[Resource: ru.TypeTag](
      resourcePath: String,
      resource: Resource,
      requestFilter: Request => Request = identity
  ): Future[Resource] = {
    val r = newRequest(HttpMethod.PATCH, resourcePath)
    r.setContentTypeJson()
    r.setContentString(toJson(resource))
    convert[Resource](send(r, requestFilter))
  }
  override def patchRaw[Resource: ru.TypeTag](
      resourcePath: String,
      resource: Resource,
      requestFilter: Request => Request = identity
  ): Future[http.Response] = {
    patchOps[Resource, http.Response](resourcePath, resource, requestFilter)
  }
  override def patchOps[Resource: ru.TypeTag, OperationResponse: ru.TypeTag](
      resourcePath: String,
      resource: Resource,
      requestFilter: Request => Request = identity
  ): Future[OperationResponse] = {
    val r = newRequest(HttpMethod.PATCH, resourcePath)
    r.setContentTypeJson()
    r.setContentString(toJson(resource))
    convert[OperationResponse](send(r, requestFilter))
  }
}

/**
  *
  */
object FinagleClient extends LogSupport {
  def defaultInitClient: Http.Client => Http.Client = { x: Http.Client =>
    x.withSessionQualifier.noFailureAccrual
  }
  def defaultRetryContext: RetryContext = {
    HttpClient.defaultHttpClientRetry[http.Request, http.Response]
  }
  def newClient(hostAndPort: String, config: FinagleClientConfig = FinagleClientConfig()): FinagleClient = {
    new FinagleClient(address = ServerAddress(hostAndPort), config)
  }
  def newSyncClient(
      hostAndPort: String,
      config: FinagleClientConfig = FinagleClientConfig()
  ): FinagleSyncClient = {
    new FinagleClient(address = ServerAddress(hostAndPort), config).syncClient
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy