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

com.twitter.finagle.InetResolver.scala Maven / Gradle / Ivy

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

import com.github.benmanes.caffeine.cache.{CacheLoader, Caffeine, LoadingCache}
import com.twitter.cache.caffeine.CaffeineCache
import com.twitter.concurrent.AsyncSemaphore
import com.twitter.conversions.time._
import com.twitter.finagle.stats.{DefaultStatsReceiver, StatsReceiver}
import com.twitter.finagle.util.{DefaultTimer, InetSocketAddressUtil, Updater}
import com.twitter.logging.Logger
import com.twitter.util.{Await, Closable, Var, _}
import java.net.{InetAddress, InetSocketAddress, UnknownHostException}

private[finagle] class DnsResolver(statsReceiver: StatsReceiver)
  extends (String => Future[Seq[InetAddress]]) {

  private[this] val dnsLookupFailures = statsReceiver.counter("dns_lookup_failures")
  private[this] val dnsLookups = statsReceiver.counter("dns_lookups")
  private[this] val log = Logger()

  // Resolve hostnames asynchronously and concurrently.
  private[this] val dnsCond = new AsyncSemaphore(100)
  private[this] val waitersGauge = statsReceiver.addGauge("queue_size") { dnsCond.numWaiters }

  private[this] val Loopback = Future.value(Seq(InetAddress.getLoopbackAddress))

  override def apply(host: String): Future[Seq[InetAddress]] = {
    if (host.isEmpty || host == "localhost") {
      // Avoid using the thread pool to resolve localhost. Ideally we
      // would always do that if hostname is an IP address, but there is
      // no native API to determine if it is the case. localhost can
      // safely be treated specially here, see rfc6761 section 6.3.3.
      Loopback
    } else {
      dnsLookups.incr()
      dnsCond.acquire().flatMap { permit =>
        FuturePool.unboundedPool(InetAddress.getAllByName(host).toSeq)
          .onFailure { e =>
            log.info(s"Failed to resolve $host. Error $e")
            dnsLookupFailures.incr()
          }
          .ensure { permit.release() }
      }
    }
  }
}

/**
 * Resolver for inet scheme.
 */
object InetResolver {
  def apply(): Resolver = apply(DefaultStatsReceiver)
  def apply(unscopedStatsReceiver: StatsReceiver): Resolver = {
    val statsReceiver = unscopedStatsReceiver.scope("inet").scope("dns")
    new InetResolver(new DnsResolver(statsReceiver), statsReceiver, Some(5.seconds))
  }
}

private[finagle] class InetResolver(
  resolveHost: String => Future[Seq[InetAddress]],
  statsReceiver: StatsReceiver,
  pollIntervalOpt: Option[Duration])
  extends Resolver {
  import InetSocketAddressUtil._

  type HostPortMetadata = (String, Int, Addr.Metadata)

  val scheme = "inet"
  private[this] val latencyStat = statsReceiver.stat("lookup_ms")
  private[this] val successes = statsReceiver.counter("successes")
  private[this] val failures = statsReceiver.counter("failures")
  private[this] val log = Logger()
  private[this] val timer = DefaultTimer.twitter

  /**
   * Resolve all hostnames and merge into a final Addr.
   * If all lookups are unknown hosts, returns Addr.Neg.
   * If all lookups fail with unexpected errors, returns Addr.Failed.
   * If any lookup succeeds the final result will be Addr.Bound
   * with the successful results.
   */
  def toAddr(hp: Seq[HostPortMetadata]): Future[Addr] = {
    val elapsed = Stopwatch.start()
    Future.collectToTry(hp.map {
      case (host, port, meta) =>
        resolveHost(host).map { inetAddrs =>
          inetAddrs.map { inetAddr =>
            Address.Inet(new InetSocketAddress(inetAddr, port), meta)
          }
        }
    }).flatMap { seq: Seq[Try[Seq[Address]]] =>
      // Filter out all successes. If there was at least 1 success, consider
      // the entire operation a success
      val results = seq.collect {
        case Return(subset) => subset
      }.flatten

      // Consider any result a success. Ignore partial failures.
      if (results.nonEmpty) {
        successes.incr()
        latencyStat.add(elapsed().inMilliseconds)
        Future.value(Addr.Bound(results.toSet))
      } else {
        // Either no hosts or resolution failed for every host
        failures.incr()
        latencyStat.add(elapsed().inMilliseconds)
        log.info(s"Resolution failed for all hosts in $hp")

        seq.collectFirst {
          case Throw(e) => e
        } match {
          case Some(_: UnknownHostException) => Future.value(Addr.Neg)
          case Some(e) => Future.value(Addr.Failed(e))
          case None => Future.value(Addr.Bound(Set[Address]()))
        }
      }
    }
  }

  def bindHostPortsToAddr(hosts: Seq[HostPortMetadata]): Var[Addr] = {
    Var.async(Addr.Pending: Addr) { u =>
      toAddr(hosts) onSuccess { u() = _ }
      pollIntervalOpt match {
        case Some(pollInterval) =>
          val updater = new Updater[Unit] {
            val one = Seq(())
            // Just perform one update at a time.
            protected def preprocess(elems: Seq[Unit]) = one
            protected def handle(unit: Unit) {
              // This always runs in a thread pool; it's okay to block.
              u() = Await.result(toAddr(hosts))
            }
          }
          timer.schedule(pollInterval.fromNow, pollInterval) {
            FuturePool.unboundedPool(updater(()))
          }
        case None =>
          Closable.nop
      }
    }
  }

  /**
   * Binds to the specified hostnames, and refreshes the DNS information periodically.
   */
  def bind(hosts: String): Var[Addr] = Try(parseHostPorts(hosts)) match {
    case Return(hp) =>
      bindHostPortsToAddr(hp.map { case (host, port) =>
        (host, port, Addr.Metadata.empty)
      })
    case Throw(exc) =>
      Var.value(Addr.Failed(exc))
  }
}

/**
 * InetResolver that caches all successful DNS lookups indefinitely
 * and does not poll for updates.
 *
 * Clients should only use this in scenarios where host -> IP map changes
 * do not occur.
 */
object FixedInetResolver {
  private[this] val log = Logger()

  val scheme = "fixedinet"

  def apply(): InetResolver =
    apply(DefaultStatsReceiver)

  def apply(unscopedStatsReceiver: StatsReceiver): InetResolver =
    apply(unscopedStatsReceiver, 16000)

  def apply(unscopedStatsReceiver: StatsReceiver, maxCacheSize: Long): InetResolver =
    apply(unscopedStatsReceiver, maxCacheSize, Stream.empty, DefaultTimer.twitter)

  /**
   * Uses a [[com.twitter.util.Future]] cache to memoize lookups.
   *
   * @param maxCacheSize Specifies the maximum number of `Futures` that can be cached.
   *                     No maximum size limit if Long.MaxValue.
   * @param backoffs Optionally retry DNS resolution failures using this sequence of
   *                 durations for backoff. Stream.empty means don't retry.
   */
  def apply(
    unscopedStatsReceiver: StatsReceiver,
    maxCacheSize: Long,
    backoffs: Stream[Duration],
    timer: Timer
  ): InetResolver = {
    val statsReceiver = unscopedStatsReceiver.scope("inet").scope("dns")
    new FixedInetResolver(cache(
      new DnsResolver(statsReceiver), maxCacheSize, backoffs, timer), statsReceiver)
  }

  // A size-bounded FutureCache backed by a LoaderCache
  private[finagle] def cache(
    resolveHost: String => Future[Seq[InetAddress]],
    maxCacheSize: Long,
    backoffs: Stream[Duration] = Stream.empty,
    timer: Timer = DefaultTimer.twitter
  ): LoadingCache[String, Future[Seq[InetAddress]]] = {

    val cacheLoader = new CacheLoader[String, Future[Seq[InetAddress]]]() {
      def load(host: String): Future[Seq[InetAddress]] = {
        // Optionally retry failed DNS resolutions with specified backoff.
        def retryingLoad(nextBackoffs: Stream[Duration]): Future[Seq[InetAddress]] = {
          resolveHost(host).rescue { case exc: UnknownHostException =>
            nextBackoffs match {
              case nextBackoff #:: restBackoffs =>
                log.debug(s"Caught UnknownHostException resolving host '$host'. Retrying in $nextBackoff...")
                Future.sleep(nextBackoff)(timer).before(retryingLoad(restBackoffs))
              case Stream.Empty =>
                Future.exception(exc)
            }
          }
        }
        retryingLoad(backoffs)
      }
    }

    var builder = Caffeine
      .newBuilder()
      .recordStats()

    if (maxCacheSize != Long.MaxValue) {
      builder = builder.maximumSize(maxCacheSize)
    }
    builder.build(cacheLoader)
  }
}

/**
 * Uses a [[com.twitter.util.Future]] cache to memoize lookups.
 *
 * @param cache The lookup cache
 */
private[finagle] class FixedInetResolver(
  cache: LoadingCache[String, Future[Seq[InetAddress]]],
  statsReceiver: StatsReceiver)
  extends InetResolver(CaffeineCache.fromLoadingCache(cache), statsReceiver, None) {

  override val scheme = FixedInetResolver.scheme

  private[this] val cacheStatsReceiver = statsReceiver.scope("cache")
  private[this] val cacheGauges = Seq(
    cacheStatsReceiver.addGauge("size") { cache.estimatedSize },
    cacheStatsReceiver.addGauge("evicts") { cache.stats().evictionCount },
    cacheStatsReceiver.addGauge("hit_rate") { cache.stats().hitRate.toFloat })
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy