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

io.gatling.core.session.Session.scala Maven / Gradle / Ivy

There is a newer version: 3.13.1
Show newest version
/*
 * Copyright 2011-2024 GatlingCorp (https://gatling.io)
 *
 * 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.core.session

import java.util.UUID

import scala.reflect.ClassTag

import io.gatling.commons.NotNothing
import io.gatling.commons.stats.{ KO, OK, Status }
import io.gatling.commons.util.TypeCaster
import io.gatling.commons.util.TypeHelper
import io.gatling.commons.validation._
import io.gatling.core.action.Action
import io.gatling.core.session.el.ElMessages
import io.gatling.core.stats.message.ResponseTimings

import com.typesafe.scalalogging.LazyLogging
import io.netty.channel.EventLoop

private[gatling] object SessionPrivateAttributes {
  private val PrivateAttributePrefix = "gatling."

  def isAttributePrivate(attributeName: String): Boolean = attributeName.startsWith(PrivateAttributePrefix)

  def generatePrivateAttribute(base: String): String = PrivateAttributePrefix + base

  def generateRandomUuidPrivateAttribute(): String = PrivateAttributePrefix + UUID.randomUUID()
}

final case class SessionAttribute(session: Session, key: String) {
  def as[T: TypeCaster: ClassTag: NotNothing]: T = session.attributes.get(key) match {
    case Some(value) => TypeHelper.cast[T](key, value)
    case _           => throw new NoSuchElementException(ElMessages.undefinedSessionAttribute(key).message)
  }
  def asOption[T: TypeCaster: ClassTag: NotNothing]: Option[T] = session.attributes.get(key).map(TypeHelper.cast[T](key, _))
  def validate[T: TypeCaster: ClassTag: NotNothing]: Validation[T] = session.attributes.get(key) match {
    case Some(value) => TypeHelper.validate[T](key, value)
    case _           => ElMessages.undefinedSessionAttribute(key)
  }
}

object Session {
  val MarkAsFailedUpdate: Session => Session = _.markAsFailed
  val Identity: Session => Session = identity
  val NothingOnExit: Session => Unit = _ => ()

  private[session] def timestampName(counterName: String) = "timestamp." + counterName

  def apply(
      scenario: String,
      userId: Long,
      eventLoop: EventLoop
  ): Session =
    apply(scenario, userId, Session.NothingOnExit, eventLoop)

  private[core] def apply(
      scenario: String,
      userId: Long,
      onExit: Session => Unit,
      eventLoop: EventLoop
  ): Session =
    Session(
      scenario = scenario,
      userId = userId,
      attributes = Map.empty,
      baseStatus = OK,
      blockStack = Nil,
      onExit = onExit,
      eventLoop: EventLoop
    )
}

/**
 * Session class representing the session passing through a scenario for a given user
 *
 * This session stores all needed data between requests
 *
 * @constructor
 *   creates a new session
 * @param scenario
 *   the name of the current scenario
 * @param userId
 *   the id of the current user
 * @param attributes
 *   the map that stores all values needed
 * @param baseStatus
 *   the status when not in a TryMax blocks hierarchy
 * @param blockStack
 *   the block stack
 * @param onExit
 *   hook to execute once the user reaches the exit
 */
final case class Session(
    scenario: String,
    userId: Long,
    attributes: Map[String, Any],
    baseStatus: Status,
    blockStack: List[Block],
    onExit: Session => Unit,
    eventLoop: EventLoop
) extends LazyLogging {
  import Session._

  def apply(name: String): SessionAttribute = SessionAttribute(this, name)
  def setAll(newAttributes: (String, Any)*): Session = copy(attributes = attributes ++ newAttributes)
  def setAll(newAttributes: Iterable[(String, Any)]): Session = copy(attributes = attributes ++ newAttributes)
  def set(key: String, value: Any): Session = copy(attributes = attributes + (key -> value))
  def remove(key: String): Session = if (contains(key)) copy(attributes = attributes - key) else this
  def removeAll(keys: String*): Session = keys.foldLeft(this)(_ remove _)
  def contains(attributeKey: String): Boolean = attributes.contains(attributeKey)

  def reset: Session = {
    val newAttributes =
      if (blockStack.isEmpty) {
        // not in a block
        attributes.view.filterKeys(SessionPrivateAttributes.isAttributePrivate)
      } else {
        val counterNames: Set[String] = blockStack.view.collect { case loopBlock: LoopBlock => loopBlock.counterName }.to(Set)
        if (counterNames.isEmpty) {
          // no counter based blocks (only groups)
          attributes.view.filterKeys(SessionPrivateAttributes.isAttributePrivate)
        } else {
          val timestampNames: Set[String] = counterNames.map(timestampName)
          attributes.view.filterKeys(key => counterNames.contains(key) || timestampNames.contains(key) || SessionPrivateAttributes.isAttributePrivate(key))
        }
      }
    copy(attributes = newAttributes.to(Map))
  }

  def loopCounterValue(counterName: String): Int = attributes(counterName).asInstanceOf[Int]

  def loopTimestampValue(counterName: String): Long = attributes(timestampName(counterName)).asInstanceOf[Long]

  @SuppressWarnings(Array("org.wartremover.warts.ListAppend"))
  private[gatling] def enterGroup(groupName: String, nowMillis: Long): Session = {
    val groups = blockStack.collectFirst { case g: GroupBlock => g.groups } match {
      case Some(l) => l :+ groupName
      case _       => groupName :: Nil
    }
    copy(blockStack = GroupBlock(groups, nowMillis, 0, OK) :: blockStack)
  }

  private[core] def exitGroup(tail: List[Block]): Session = copy(blockStack = tail)

  def logGroupRequestTimings(startTimestamp: Long, endTimestamp: Long): Session =
    if (blockStack.isEmpty) {
      this
    } else {
      val responseTime = ResponseTimings.responseTime(startTimestamp, endTimestamp)
      copy(blockStack = blockStack.map {
        case g: GroupBlock => g.copy(cumulatedResponseTime = g.cumulatedResponseTime + responseTime)
        case b             => b
      })
    }

  def groups: List[String] =
    blockStack.collectFirst { case g: GroupBlock => g.groups }.getOrElse(Nil)

  private[core] def enterTryMax(counterName: String, loopAction: Action) =
    copy(
      blockStack = TryMaxBlock(counterName, loopAction, OK) :: blockStack,
      attributes = newAttributesWithCounter(counterName, withTimestamp = false, 0L)
    )

  private[core] def exitTryMax(counterName: String, status: Status, tail: List[Block]): Session =
    copy(blockStack = tail).updateStatus(status).removeCounter(counterName)

  def isFailed: Boolean = baseStatus == KO || blockStack.exists {
    case TryMaxBlock(_, _, KO) => true
    case _                     => false
  }

  def status: Status = if (isFailed) KO else OK

  private def failStatusUntilClosestTryMaxBlock: List[Block] = {
    var firstTryMaxBlockNotReached = true
    blockStack.map {
      case tryMaxBlock: TryMaxBlock if firstTryMaxBlockNotReached && tryMaxBlock.status == OK =>
        firstTryMaxBlockNotReached = false
        tryMaxBlock.copy(status = KO)
      case groupBlock: GroupBlock if firstTryMaxBlockNotReached && groupBlock.status == OK =>
        groupBlock.copy(status = KO)
      case b => b
    }
  }

  private def restoreClosestTryMaxBlockStatus: List[Block] = {
    var firstTryMaxBlockNotReached = true
    blockStack.map {
      case tryMaxBlock: TryMaxBlock if firstTryMaxBlockNotReached && tryMaxBlock.status == KO =>
        firstTryMaxBlockNotReached = false
        tryMaxBlock.copy(status = OK)
      case b => b
    }
  }

  private def updateStatus(newStatus: Status): Session =
    if (newStatus == OK) {
      markAsSucceeded
    } else {
      markAsFailed
    }

  private def isWithinTryMax: Boolean = blockStack.exists(_.isInstanceOf[TryMaxBlock])

  def markAsSucceeded: Session =
    if (isWithinTryMax) {
      copy(blockStack = restoreClosestTryMaxBlockStatus)
    } else if (baseStatus == KO) {
      copy(baseStatus = OK)
    } else {
      this
    }

  def markAsFailed: Session =
    if (baseStatus == KO && blockStack.isEmpty) {
      this
    } else {
      val updatedStatus = if (isWithinTryMax) baseStatus else KO
      copy(baseStatus = updatedStatus, blockStack = failStatusUntilClosestTryMaxBlock)
    }

  private def newBlockStack(counterName: String, condition: Expression[Boolean], exitAction: Action, exitASAP: Boolean): List[Block] = {
    val newBlock =
      if (exitASAP) {
        ExitAsapLoopBlock(counterName, condition, exitAction)
      } else {
        ExitOnCompleteLoopBlock(counterName)
      }
    newBlock :: blockStack
  }

  private[core] def enterLoop(counterName: String, condition: Expression[Boolean], exitAction: Action, exitASAP: Boolean): Session =
    copy(
      blockStack = newBlockStack(counterName, condition, exitAction, exitASAP),
      attributes = newAttributesWithCounter(counterName, withTimestamp = false, 0L)
    )

  private[core] def enterTimeBasedLoop(
      counterName: String,
      condition: Expression[Boolean],
      exitAction: Action,
      exitASAP: Boolean,
      nowMillis: Long
  ): Session =
    copy(
      blockStack = newBlockStack(counterName, condition, exitAction, exitASAP),
      attributes = newAttributesWithCounter(counterName, withTimestamp = true, nowMillis)
    )

  private[core] def exitLoop(counterName: String, tail: List[Block]): Session =
    copy(blockStack = tail).removeCounter(counterName)

  private def newAttributesWithCounter(counterName: String, withTimestamp: Boolean, nowMillis: Long): Map[String, Any] = {
    val withCounter = attributes.updated(counterName, 0)
    if (withTimestamp) {
      withCounter.updated(timestampName(counterName), nowMillis)
    } else {
      withCounter
    }
  }

  private[core] def incrementCounter(counterName: String): Session =
    attributes.get(counterName) match {
      case Some(counterValue: Int) => copy(attributes = attributes.updated(counterName, counterValue + 1))
      case _ =>
        logger.error(s"incrementCounter called but attribute for counterName $counterName is missing, please report.")
        this
    }

  private[core] def removeCounter(counterName: String): Session =
    attributes.get(counterName) match {
      case None =>
        logger.error(s"removeCounter called but attribute for counterName $counterName is missing, please report.")
        this
      case _ =>
        copy(attributes = attributes - counterName - timestampName(counterName))
    }

  private[core] def exit(): Unit = onExit(this)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy