Update the Session from within a foreach loop

Hello! :slightly_smiling_face:

I am trying to perform a request repeatedly based on the response of the previous one and I can’t seem to get it right.

I have a feeder that generates a List[Map[String, String]] inside the virtual user’s session like the one below (the names have been changed):

requestParams -> List(Map(param1 -> , param2 -> , param3 -> 1234, param4_2777 -> 4, param5 -> false, param6 -> false)), ...)

The scenario conains a foreach("${requestParams}", "param") { ... } that loops over every Map[String,String] in the List and passes the map using Gatling EL directly to a builder like the following

http("Do Action")
  .post("/mypage.html")
  .headers(default)
  .formParam("CSRF_TOKEN", csrfToken)
  .formParam("action", "")
  .formParamMap(requestParams)

The param4_2777 parameter could have any number after it, like 2777 in our example here, and the value of it is what I want to change.

The target flow is as follows:

  1. Make the request with the map initially found in the session (e.g., the one shown above).
  2. If the request fails (this could be detected by a specific string on the response) decrease the number in the param4_2777 parameter by 1and fire the request again. Repeat until the request succeeds or the number is 0.

As a first step, I am trying the simple case of updating the values in the session to no avail:

foreach("${requestParams}", "param") {
  exec(
    exec { session => println(session.update()); session},
    exec { session => {
      val rp = session("requestParams").as[List[Map[String, String]]]
      val newrp = List(rp.head map {
        case ("param4_2777", value) => "param4_2777" -> (value.toInt - 1)
	case x => x
      })
     session.set("requestParams", newrp)
  }}
  )
}

When I run the map to update the value in the Scala REPL console I get the result fine but when I run the test in Gatling I get the following error and the update fails:

14:26:09.664 [ERROR] i.g.c.a.b.SessionHookBuilder$$anon$1 - 'hook-3' crashed with 'j.l.ClassCastException: class java.lang.Boolean cannot be cast to class java.lang.String (java.lang.Boolean and java.lang.String are in module java.base of loader 'bootstrap')', forwarding to the next one

This could be due to the map containing boolean values on some keys as seen in the example above.

Also, updating the variable on which I loop inside the loop, even if it’ll work, doesn’t seem like a good idea overall. Is there a more Gatling way to do such a thing?

This is all part of a purchasing flow where I want to ask for fewer products if the initial number was too high (i.e., not enough products found or not allowed to purchase as many).

Thanks a lot in advance for the help and any information.

Regards,
George

I have a feeder that generates a List[Map[String, String]]
List(Map(param1 → , param2 → , param3 → 1234, param4_2777 → 4, param5 → false, param6 → false)), …)

This is definitely not a Map[String, String] (String keys and String values): you’re mixing String, Ints and Booleans, hence the ClassCastException. This is a Map[String, Any]

Hello! Thanks for the response! :slight_smile:

Sorry, my bad! It should be session("requestParams").as[List[Map[String, Any]]]. That resolves the casting issue. I had it one way in the REPL and another in the IDE!

Regarding the fact that I update the List I am iterating over, is this safe to do in this context?

Hi @gkallergis ,

Personally, I’m used to put in feeder only what can distinct a specific user from another one in the same kind (visitor, customers, leechers, admins, etc.).
Inside a single scenario, I manage my session variable directly.

I don’t think that changing the feeder is a good thing during the simulation.

But your requirement may vary from mine ^^

I’m not sure to fully understand your specific case (param4_2777 is too vague for me)

Here an example in my mind:
you have a product.csv that describe each product on your e-commerce website.
column may be things to check for each (name, summary, description, quantity, etc.)

In the scenario, I will impersonate a customer that want to buy 1-10 items (possibly multiple time the same items)

At beginning of the scenario, I will randomly choose a number between 1 and 10 and put it in a session variable (eg exec(session => session.put("items_amount", Random.nextInt(1, 10)))

Then in a loop, I will feed to have a random element from the products list.

Then I will go to the page for checking the product, then add it to the cart.
(difference between if the product is already in the cart and we want one more, or a fresh new product in the cart)

Then, at the end of the loop, I will check the cart contains the wanted products for this specific session and continue the purchase.

Basically, I will use the feeder for stable data and other session variables for a specific… session.

Does it answer your question ?

Hello Sébastien! :slight_smile:

Thanks for the response.

If understand correctly we take slightly different approaches, but agree on the principle of, as you put it, “use the feeder for stable data and other session variables for a specific… session.”.

In my case, I have a feeder that generates everything a user will need to run their scenario. The scenarios I run have alternative paths of which I control the probability of as well. For example, 50% of users load the login page, log in, and buy stuff, 40% register a new account, then log in and then buy stuff, and 10% simply visit open pages of the site and leave.

A typical feeder-generated entry would contain information to register, reset the password, login, a list of products to buy along with their quantities (from a configurable list of available products), payment information to use when paying, etc.

The feeder (based on configured probabilities) also adds which alternative path in the scenario each user would take as a session variable (i.e., scenarioToRun → 2) as well as other details on the products for example, if they should visit the product directly through a link (simulating a marketing email received) or if they should navigate to it.

Additionally, all feeder-generated data are logged in a file so that I can later import them into the DB and verify the results.

With this, I have built a series of user actions that I re-use across multiple scenarios. For example, loadHomePage(…), addStuffToBasket(…), pay(…), etc. Kind of a “DSL” in my domain where I can put different scenarios together quickly.

The problem I am facing is when instead of simply buying what he was told, the virtual user now needs to decide whether to re-try or not based on an HTTP request response. I need to request the same amount the feeder provided minus 1 and keep doing that until I succeed or I reach 0.

If I am inside a for each loop looping over the (feeder provided) session variable that contains my purchases, if I alter it the data there the change won’t have any effect. It looks like I have to generate the data inside the loop and fire the request.

I hope I didn’t make things more complicated although I have a sense that I did! Sorry in advance! :smiley:

If I reach a conclusion I will post back in case it helps anyone else. :slight_smile:

For example, 50% of users load the login page, log in, and buy stuff, 40% register a new account, then log in and then buy stuff, and 10% simply visit open pages of the site and leave.

In my usual architecture, I will have 3 different scenarii:

  • buyers
  • newbuyers
  • lurkers

And I will play with the Concurrent scenario injection profile to have my percentages.

I will have:

  • an already registered users list accounts.csv for buyers
  • a generative feeder for newbuyers (I don’t expect to see the same newbuyers)
  • a product feeder in random

In another terms, I will split information for users (accounts) and for product.
It may have sense if your site under test constraints some product to some users, to have this list into only one feeder, but generally, it’s not needed.

With this, I have built a series of user actions that I re-use across multiple scenarios. For example, loadHomePage(…), addStuffToBasket(…), pay(…), etc. Kind of a “DSL” in my domain where I can put different scenarios together quickly.

Yeah! It’s a common pattern and we encourage it!

The problem I am facing is when instead of simply buying what he was told, the virtual user now needs to decide whether to re-try or not based on an HTTP request response. I need to request the same amount the feeder provided minus 1 and keep doing that until I succeed or I reach 0.

I don’t get it. The session variables filled by the feeder should not change if you don’t call the feeder feed action again. So you can retry as many time as you want.
You may want to have additional session variable for the current amount to test.

For instance, let’s take a scenario

  // pseudo code because I don't want to open my IDE right now :)

  // FEEDERS ----------------

  // Sessions variables: username, password
  var accounts = csv("accounts.csv")
  // Sessions variables: itemId, itemAmount
  var products = csv("products.csv")


  // Actions ----------------------

  var loadHomePage = ...

  // need username and password session variables
  var login = ...

  // Need itemId and itemAmount session variables
  // Set buyResult to true if succeed to false otherwise
  var buyItem = ...

  // Scenario -------------------
  var buyerScenario = scenario("buyer")
    .feed(accounts)
    .exec(loadHomePage)
    .exec(login)
    .exec(session -> session.set("toPurchase", Random.nextInt(1, 3))
    .repeat("#{toPurchase}").on(
      feed(products) // Here, you get the item to buy with the amount
      .asLongAs(session -> session.getInt("itemAmount") > 0).on(
        buyItem
          .exec(session -> {
             if (session.getBoolean("buyResult")) {
               return session.set("itemAmount", 0);
             } else {
               return session.set("itemAmount", session.getInt("itemAmount") - 1);
             }
      )
    )

So, yeah, you’re right, you cannot (and you shouldn’t) change your feeder values but who cares?

Note: when using Gatling Enterprise, you will be able to have consolidated reports from multiple injectors. But they do not share memory (so, global variable is not possible).

What do I miss to fully answer your question?

I think what I have is close to what you show (if we don’t account for the differences in the way I feed my virtual users with data).

My scenario is like this:

private val configuration = Map(
	"maxProductsAllowed" -> 5,  // How many products to generate for each user as a maximum
	"products" -> Map(
		"1257" -> List("2777") // This is a map of product IDs (key) to a list of items IDs for each product, available to generate purchases for
	)
)

...

private val myScenario = scenario("...")
  .feed(users) // This is the feeder that gives users their data and what they need to buy initially.
  .exec(
    loadHomePage(20, 50, LandingPage.loginPage), // LandingPage is an enum that is used to note which page we expect to land on. It is used inside the actions as part of a CSS check.
    login(15, 39, "${csrf}", "${existingLoginParameters}", LandingPage.productPage),
    foreach("${purchaseParameters}", "purchase") { // Purchase parameters is a List[Map[String, Any]] as shown below.
      exec(
        loadProductListPage(20, 204, LandingPage.productList),
        loadProductPage(15, 39, "${purchase.idProduct}", LandingPage.productPage),
        addProductsToBasket(1, 2, "${csrf}", "${purchase}", LandingPage.basketPage)
      )
    },
    pay(20, 40, "${csrf}", "${paymentParameters}", LandingPage.myOrder),
    logout(0, 0, LandingPage.loginPage)
)

An example of the relevant parameters of the purchaseParameters session variable is as follows:

purchaseParameters -> List(Map(product -> 1257, itemQuantity_2777 -> 4), ...)

The addProductsToBasket(…) would end up passing the ${purchase} parameter directly to a request builder like so:

def addProductsToBasket(csrfToken: Expression[String], purchase: Expression[Map[String, String]]): HttpRequestBuilder = {
http("Add products to basket")
  .post("/addToBasket.html")
  .headers(default)
  .formParam("CSRF_TOKEN", csrfToken)
  .formParam("add", "")
  .formParamMap(purchase) // HERE
}

There are a few calls in between, but they simply control durations, whether we exit on failure, and provide default checks unless overridden so they are not relevant here and thus omitted.

The new version I am after is like this (so far):

def addProductsToBasketWithLinearBackoff(minDuration: Duration, maxDuration: Duration, csrfToken: Expression[String], purchaseParameters: Expression[Map[String, String]], landingPage: LandingPage): ChainBuilder = {
  exec (
    exec { session => println(session); println(); session},
    addProductsToBasket(minDuration, maxDuration, csrfToken, purchaseParameters, landingPage),
    exec { session => println(session); println(); session},
    
    asLongAs(session => session(Check.ENCOUNTERED_ERROR_WHILE_ADDING_TO_BASKET).as[Boolean], "purchaseUpdates", false) {
      exec (
        updatePurchase()
        exec { session => println(session); println(); session}
        addProductsToBasket(minDuration, maxDuration, csrfToken, "${updatedPurchase}", landingPage)
      )
    }
  )
}

def updatePurchase(): ChainBuilder = {
  exec { session => {
    val purchases = session("purchaseParameters").as[List[Map[String, Any]]]
    val currentPurchase = session("purchase").as[Map[String, Any]]
    val purchaseToUpdate = purchases.filter(purchase => purchase("productId").equals(currentPurchase("productId")))
    val purchaseUpdates = session("purchaseUpdates").as[Int]
    
    val updatedPurchase = purchaseToUpdate.head map {
      case ("itemQuantity_2777", value) => "itemQuantity_2777" -> (value.toString.toInt - (purchaseUpdates + 1))
      case x => x
    }
    session.set("updatedPurchase", updatedPurchase).set(Check.ENCOUNTERED_ERROR_WHILE_ADDING_TO_BASKET, false)
  }}
}

The aim of this new add-to-basket call is to first attempt a normal call which will, in case of failure, set a session variable to true (i.e., ENCOUNTERED_ERROR_WHILE_ADDING_TO_BASKET) much like your buyResult one. As long as (see what I did there? :smiley:) this is true, I reduce the count and create a new purchase that I add to the session and call the add-to-basket again but with the new data this time. The ENCOUNTERED_ERROR_WHILE_ADDING_TO_BASKET is initialized to false by the feeder and re-set to false upon every update.

The way I generate the update purchase is hard coded for now but that will change.

The above seems to work to an extent. The first call to add stuff to the basket indeed fails and the ENCOUNTERED_ERROR_WHILE_ADDING_TO_BASKET is indeed set to true. Then the loop begins, and the ENCOUNTERED_ERROR_WHILE_ADDING_TO_BASKET is re-set to false. The first few prints of the session contain on updatedPurchase entry, but once we go into the asLongAs loop it appears and the quantity reduces. The reduction is achieved by using the counter provided by the asLongAs() call.

The only thing left is to shape the logic of reduction to make sure I don’t go below 0 and update more than one items if available. That is more of a Scala question though than a Gatling one so I am not going to bother you with this, too. :smiley:

Although it seems to work, I feel I am missing something in terms of style/architecture, so any feedback on the approach I take is very welcome!

Thanks again very much for your time! I really appreciate your help! :slight_smile: I hope our notes here help someone else in the future, too.

Although it seems to work, I feel I am missing something in terms of style/architecture, so any feedback on the approach I take is very welcome!

As long as it works and fulfill your requirement, it is good enough!

But to your request, here are some personal thoughts:

  • I won’t bother to always give the name of the CSRF_TOKEN (ie parameter csrfToken: Expression[String]) Once and for all I will tell that all requests that can return such token will set it in “CSRF_TOKEN” session variable and each request that need it will directly read the current “#{CSRF_TOKEN}”

  • "${name}" syntax is deprecated, you should now use "#{name}" syntax. You’ll be able to mix scala string interpolation and Gatling Expression Language.

  • I’m disturbed by the usage of function (def) for Gatling value. And your need to pass parameters but I’m not sure to understand your min/max duration parameters either.

  • I find it easy to read your usage of exec with a list of actions, I think I will use it more in the future.

  • I’m very confused with your post("/addToBasket.html") . But it is more about your server than your code (POST something to an html page? Really? If it’s really a form, it should always redirect after a POST, to enable the refresh without performing another request)

  • I personnally have a debugSession value (val debugSession = if (sys.env.get("DEBUG").map(Boolean).orElse(false)) exec { session => session.println(session) ; session } else exec(_))

  • you can pattern match your updatedPurchase if value is an Int

  • you can find a value in a list

etc.

But all of that are matter of taste and you already did a great job!

Thanks a lot for your feedback! It’s always good to learn from someone more experienced and exchange ideas!

Some notes on your points:

  • The CSRF token I read and store in the session and then I pass it in as a parameter using Gatling EL to further user actions in my scenario, but your idea makes more sense and will remove plenty of parameters across all my user actions.
  • The #{name} syntax I was completely unaware of! I do performance testing occasionally so I wouldn’t say I am very up to date with the latest Gatling affairs! I will check it out.
  • The structure I have currently (as far as user actions are concerned) is like this:
    diagram
    Essentially, the HttpRequest level is a set of functions like the addProductsToBasket() I sent in a previous post containing all possible user requests the app can respond to (at least the ones we want to test for). The UserAction level is wrapping those HttpBuilders adding (or not, depending on whether they were passed in) a set of checks with check(checks: HttpCheck*) and the pause(minDuration, maxDuration) to handle user timing. This is where the min/max duration parameters you see in the high-level user action end up. Around this, is a set of default user actions, essentially adding the Sequence of checks and maybe an exitHereIfFailed check. Finally, the ClientSpecificUserActions are extensions of the DefaultUserAction on a per client basis. If there is anything tricky for a specific test we override it there. That’s why I have lots of defs for Gatling purposes. As this matures responsibilities change and some things move around.
  • Thanks! Good to know that this style is at least readable if nothing else! :rofl:
  • The POST is made to such a URL because the custom CMS is handling it like that. There is no actual HTML page under that URL. :smiley:
  • I like that debug idea! I will steal it! Thanks!
  • Pattern matching sounds like a scala thing! I’ll search a bit on that, my Scala knowledge is practically 0!
  • I am assuming you mean to use find instead of filter, right?

One more question, if I may, you mentioned in one of your posts that you use setUp that accepts more than one scenario to run and handle your percentages that way. How do you handle the percentages in that case? Also, if the same feeder is used by two or more scenarios, will I need to worry about anything like concurrency issues and all?

Once again, thanks for all your help, feedback, and kind words. I really appreciate it!

pause(minDuration, maxDuration) to handle user timing

session variable? Where you will be able to mix users that are slow and those super quick in the same scenario ^^

my Scala knowledge is practically 0!

Do you know we recently added supported for Java and Kotlin?

I am assuming you mean to use find instead of filter, right?

Yeah! find stops as soon as the value is found AND return an Option (has a value or not)

How do you handle the percentages in that case?

  val buyersPercentage = 50d / 100 // 50%
  val newBuyersPercentage = 40d / 100 // 40%
  val lurkersPercentage = 10d / 100 // 10%

  val totalUsersPerSecond = 50

  val buyersPerSec = totalUsersPerSecond * buyersPercentage
  val newBuyersPerSec = totalUsersPerSecond * newBuyersPercentage
  val lurkersPerSec = totalUsersPerSecond * lurkersPercentage

  val totalDuration = 120.seconds

  setUp(
    buyersScn.inject(constantUsersPerSec(buyersPerSec).during(totalDuration).randomized),
    newBuyersScn.inject(constantUsersPerSec(newBuyersPerSec).during(totalDuration).randomized),
    lurkerScn.inject(constantUsersPerSec(lurkersPerSec).during(totalDuration).randomized)
  )

Note: for more complex injection profile, you may want to define them once with a userPerSec param.

Also, if the same feeder is used by two or more scenarios, will I need to worry about anything like concurrency issues and all?

No, thanks to Akka actors, only one thread will access to your feeder at a time.
As you create your own feeder, ensure that it’s a FeederBuilder, but it should be good.

session variable? Where you will be able to mix users that are slow and those super quick in the same scenario ^^

The durations themselves are in the scenario as values coming from analytics and other sources. I basically provide a range and then let Gatling randomly select the duration each time.

Do you know we recently added supported for Java and Kotlin?

Yes! I saw that a couple of days ago! If I can get the time I might attempt a port to Java.


Note: for more complex injection profile, you may want to define them once with a userPerSec param.

Oooh! I see what you did there with the percentages. To be honest, I like your way more because it is going to lead to smaller/cleaner and more composable scenarios. Right now I’ve built a command-line tool that runs my tests while dynamically selecting injection profiles, but I think I’ll figure out a way to incorporate this into it.

Thanks a lot, Sébastien!