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

io.gatling.http.fetch.ResourceFetcher.scala Maven / Gradle / Ivy

The newest version!
/**
 * Copyright 2011-2014 eBusiness Information, Groupe Excilys (www.ebusinessinformation.fr)
 *
 * 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 io.gatling.http.fetch

import java.net.URI

import scala.collection.JavaConversions._
import scala.collection.concurrent
import scala.collection.mutable

import com.typesafe.scalalogging.slf4j.StrictLogging
import io.gatling.core.akka.BaseActor
import io.gatling.core.filter.Filters
import io.gatling.core.result.message.{ KO, OK, Status }
import io.gatling.core.session.Session
import io.gatling.core.util.StringHelper._
import io.gatling.core.util.TimeHelper.nowMillis
import io.gatling.http.HeaderNames
import io.gatling.http.action.{ HttpRequestAction, HttpRequestActionBuilder }
import io.gatling.http.ahc.HttpTx
import io.gatling.http.cache.CacheHandling
import io.gatling.http.config.HttpProtocol
import io.gatling.http.response.{ Response, ResponseBuilder }
import jsr166e.ConcurrentHashMapV8

sealed trait ResourceFetched {
  def uri: URI
  def status: Status
  def sessionUpdates: Session => Session
}
case class RegularResourceFetched(uri: URI, status: Status, sessionUpdates: Session => Session) extends ResourceFetched
case class CssResourceFetched(uri: URI, status: Status, sessionUpdates: Session => Session, statusCode: Option[Int], lastModifiedOrEtag: Option[String], content: String) extends ResourceFetched

case class InferredPageResources(expire: String, requests: List[NamedRequest])

object ResourceFetcher extends StrictLogging {

  val cssContentCache: concurrent.Map[URI, List[EmbeddedResource]] = new ConcurrentHashMapV8[URI, List[EmbeddedResource]]
  val inferredResourcesCache: concurrent.Map[(HttpProtocol, URI), InferredPageResources] = new ConcurrentHashMapV8[(HttpProtocol, URI), InferredPageResources]

  val resourceChecks = List(HttpRequestActionBuilder.DefaultHttpCheck)

  def pageResources(htmlDocumentURI: URI, filters: Option[Filters], responseChars: Array[Char]): List[EmbeddedResource] = {
    val htmlInferredResources = HtmlParser.getEmbeddedResources(htmlDocumentURI, responseChars)
    filters match {
      case Some(f) => f.filter(htmlInferredResources)
      case none    => htmlInferredResources
    }
  }

  def cssResources(cssURI: URI, filters: Option[Filters], content: String): List[EmbeddedResource] = {
    val cssInferredResources = cssContentCache.getOrElseUpdate(cssURI, CssParser.extractResources(cssURI, content))
    filters match {
      case Some(f) => f.filter(cssInferredResources)
      case none    => cssInferredResources
    }
  }

  def lastModifiedOrEtag(response: Response, protocol: HttpProtocol): Option[String] =
    if (protocol.requestPart.cache)
      response.header(HeaderNames.LAST_MODIFIED).orElse(response.header(HeaderNames.ETAG))
    else
      None

  def fromPage(response: Response, tx: HttpTx, explicitResources: List[NamedRequest]): Option[() => ResourceFetcher] = {

    val htmlDocumentURI = response.request.getURI
    val protocol = tx.protocol

      def pageResourcesRequests(): List[NamedRequest] =
        pageResources(htmlDocumentURI, protocol.responsePart.htmlResourcesFetchingFilters, response.body.string.unsafeChars)
          .flatMap(_.toRequest(protocol, tx.throttled))

    val inferredResources: List[NamedRequest] = response.statusCode match {
      case Some(200) =>
        lastModifiedOrEtag(response, protocol) match {
          case Some(newLastModifiedOrEtag) =>
            val cacheKey = (protocol, htmlDocumentURI)
            inferredResourcesCache.get(cacheKey) match {
              case Some(InferredPageResources(`newLastModifiedOrEtag`, res)) =>
                //cache entry didn't expire, use it
                res
              case _ =>
                // cache entry missing or expired, update it
                val inferredResources = pageResourcesRequests()
                // FIXME add throttle to cache key?
                inferredResourcesCache.put((protocol, htmlDocumentURI), InferredPageResources(newLastModifiedOrEtag, inferredResources))
                inferredResources
            }

          case None =>
            // don't cache
            pageResourcesRequests()
        }

      case Some(304) =>
        // no content, retrieve from cache if exist
        val cacheKey = (protocol, htmlDocumentURI)
        inferredResourcesCache.get(cacheKey) match {
          case Some(inferredPageResources) => inferredPageResources.requests
          case _ =>
            logger.warn(s"Got a 304 for $htmlDocumentURI but could find cache entry?!")
            Nil
        }

      case _ => Nil
    }

    resourceFetcher(tx, inferredResources, explicitResources)
  }

  def fromCache(htmlDocumentURI: URI, tx: HttpTx, explicitResources: List[NamedRequest]): Option[() => ResourceFetcher] = {
    val cacheKey = (tx.protocol, htmlDocumentURI)
    val inferredResources = inferredResourcesCache.get(cacheKey).map(_.requests).getOrElse(Nil)

    resourceFetcher(tx, inferredResources, explicitResources)
  }

  private def resourceFetcher(tx: HttpTx, inferredResources: List[NamedRequest], explicitResources: List[NamedRequest]) = {

    val uniqueResources = inferredResources.map(res => res.ahcRequest.getURI -> res).toMap ++
      explicitResources.map(res => res.ahcRequest.getURI -> res).toMap

    if (uniqueResources.isEmpty)
      None
    else {
      Some(() => new ResourceFetcher(tx, uniqueResources.values))
    }
  }
}

// FIXME handle crash
class ResourceFetcher(tx: HttpTx, initialResources: Iterable[NamedRequest]) extends BaseActor {

  var session = tx.session
  val alreadySeen: Set[URI] = initialResources.map(_.ahcRequest.getURI).toSet
  val bufferedRequestsByHost = mutable.HashMap.empty[String, List[NamedRequest]].withDefaultValue(Nil)
  val availableTokensByHost = mutable.HashMap.empty[String, Int].withDefaultValue(tx.protocol.enginePart.maxConnectionsPerHost)
  var pendingRequestsCount = initialResources.size
  var globalStatus: Status = OK
  val start = nowMillis

  // start fetching
  fetchOrBufferResources(initialResources)

  def fetchResource(request: NamedRequest) {
    logger.debug(s"Fetching ressource ${request.ahcRequest.getUrl}")

    val resourceTx = tx.copy(
      session = this.session,
      request = request.ahcRequest,
      requestName = request.name,
      checks = ResourceFetcher.resourceChecks,
      responseBuilderFactory = ResponseBuilder.newResponseBuilderFactory(ResourceFetcher.resourceChecks, None, tx.protocol),
      next = self,
      resourceFetching = true)

    HttpRequestAction.startHttpTransaction(resourceTx)
  }

  def handleCachedRequest(request: NamedRequest) {
    logger.info(s"Fetching resource ${request.ahcRequest.getURI} from cache")
    // FIXME check if it's a css this way or use the Content-Type?
    val resourceFetched = if (ResourceFetcher.cssContentCache.contains(request.ahcRequest.getURI))
      CssResourceFetched(request.ahcRequest.getURI, OK, identity, None, None, "")
    else
      RegularResourceFetched(request.ahcRequest.getURI, OK, identity)

    receive(resourceFetched)
  }

  def fetchOrBufferResources(requests: Iterable[NamedRequest]) {

      def sendRequests(host: String, requests: Iterable[NamedRequest]) {
        requests.foreach(fetchResource)
        availableTokensByHost += host -> (availableTokensByHost(host) - requests.size)
      }

      def bufferRequests(host: String, requests: Iterable[NamedRequest]) {
        bufferedRequestsByHost += host -> (bufferedRequestsByHost(host) ::: requests.toList)
      }

    val (nonCachedRequests, cachedRequests) = requests.partition { request =>
      val uri = request.ahcRequest.getURI
      CacheHandling.getExpire(tx.protocol, session, uri) match {
        case None => true
        case Some(expire) if nowMillis > expire =>
          // ugly, side effecting
          session = CacheHandling.clearExpire(session, uri)
          true
        case _ => false
      }
    }

    cachedRequests.foreach(handleCachedRequest)

    nonCachedRequests
      .groupBy(_.ahcRequest.getURI.getHost)
      .foreach {
        case (host, reqs) =>
          val availableTokens = availableTokensByHost(host)
          val (immediateRequests, bufferedRequests) = reqs.splitAt(availableTokens)
          sendRequests(host, immediateRequests)
          bufferRequests(host, bufferedRequests)
      }
  }

  def done(status: Status) {
    logger.debug("All resources were fetched")
    tx.next ! session
    context.stop(self)
  }

  def resourceFetched(uri: URI, status: Status) {

      def releaseToken(host: String, bufferedRequests: List[NamedRequest]) {
        bufferedRequests match {
          case Nil =>
            // nothing to send for this host
            availableTokensByHost += host -> (availableTokensByHost(host) + 1)

          case request :: tail =>
            bufferedRequestsByHost += host -> tail
            val uri = request.ahcRequest.getURI
            CacheHandling.getExpire(tx.protocol, session, uri) match {
              case None =>
                // recycle token, fetch a buffered resource
                fetchResource(request)

              case Some(expire) if nowMillis > expire =>
                // expire reached
                session = CacheHandling.clearExpire(session, uri)
                fetchResource(request)

              case _ =>
                handleCachedRequest(request)
                releaseToken(host, tail)
            }
        }
      }

    logger.debug(s"Resource $uri was fetched")
    pendingRequestsCount -= 1

    if (status == KO)
      globalStatus = KO

    if (pendingRequestsCount == 0)
      done(globalStatus)
    else {
      val requests = bufferedRequestsByHost.get(uri.getHost) match {
        case Some(reqs) => reqs
        case _          => Nil
      }
      releaseToken(uri.getHost, requests)
    }
  }

  def cssFetched(uri: URI, status: Status, statusCode: Option[Int], lastModifiedOrEtag: Option[String], content: String) {

    val protocol = tx.protocol

    if (status == OK) {
      // this css might contain some resources

      val rawCssResources: List[NamedRequest] = statusCode match {
        case Some(200) =>
          // try to get from cache
          lastModifiedOrEtag match {
            case Some(newLastModifiedOrEtag) =>
              val cacheKey = (protocol, uri)
              ResourceFetcher.inferredResourcesCache.get(cacheKey) match {
                case Some(InferredPageResources(`newLastModifiedOrEtag`, inferredResources)) =>
                  //cache entry didn't expire, use it
                  inferredResources
                case _ =>
                  // cache entry missing or expired, update it
                  ResourceFetcher.cssContentCache.remove(protocol -> uri)
                  val inferredResources = ResourceFetcher.cssResources(uri, protocol.responsePart.htmlResourcesFetchingFilters, content).flatMap(_.toRequest(protocol, tx.throttled))
                  ResourceFetcher.inferredResourcesCache.put((protocol, uri), InferredPageResources(newLastModifiedOrEtag, inferredResources))
                  inferredResources
              }

            case None =>
              // don't cache
              ResourceFetcher.cssResources(uri, protocol.responsePart.htmlResourcesFetchingFilters, content).flatMap(_.toRequest(protocol, tx.throttled))
          }

        case Some(304) =>
          // no content, retrieve from cache if exist
          val cacheKey = (protocol, uri)
          ResourceFetcher.inferredResourcesCache.get(cacheKey) match {
            case Some(inferredPageResources) => inferredPageResources.requests
            case _ =>
              logger.warn(s"Got a 304 for $uri but could find cache entry?!")
              Nil
          }
        case _ => Nil
      }

      val filtered = rawCssResources.filterNot(res => alreadySeen.contains(res.ahcRequest.getURI))

      pendingRequestsCount += filtered.size
      fetchOrBufferResources(filtered)
    }
  }

  def receive: Receive = {
    case RegularResourceFetched(uri, status, sessionUpdates) =>
      session = sessionUpdates(session)
      resourceFetched(uri, status)

    case CssResourceFetched(uri, status, sessionUpdates, statusCode, lastModifiedOrEtag, content) =>
      session = sessionUpdates(session)
      cssFetched(uri, status, statusCode, lastModifiedOrEtag, content)
      resourceFetched(uri, status)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy