We have a performance issue in our application: Login is very slow and problematic when too many are initiated too fast.
Unfortunately, that makes it hard to get to the rest of the application for performance testing.
What I would like to do is log in one user at a time, as fast as the application allows, and no faster … and no slower.
One way of doing that would be to add some kind of single threaded gating to a segment of my execution chain. A semaphore or something. So, when the Login begins, we “lock” that part of the scenario, so that other users have to wait. When the login process completes, it “unlocks” the block, and the next user to get CPU time can enter the section.
I imagine that there is probably a way in Scala or Java to accomplish that. But I don’t know if that’s really the right way of accomplishing what I want to accomplish. How would you go about accomplishing this kind of thing, if it were you?
So, something is not right. Maybe you can help me understand what I did wrong. My code compiles, but at startup I get an exception:
Exception in thread "main" java.lang.ExceptionInInitializerError at com.cigna.compass.scenarios.CompassOnlyActions.<init>(CompassOnlyActions.scala:14) at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:526) at java.lang.Class.newInstance(Class.java:374) at io.gatling.core.runner.Runner.run(Runner.scala:37) at io.gatling.app.Gatling.start(Gatling.scala:236) at io.gatling.app.Gatling$.fromMap(Gatling.scala:55) at io.gatling.app.Gatling$.runGatling(Gatling.scala:80) at io.gatling.app.Gatling$.runGatling(Gatling.scala:59) at io.gatling.app.Gatling$.main(Gatling.scala:51) at io.gatling.app.Gatling.main(Gatling.scala) Caused by: java.lang.NullPointerException at io.gatling.core.structure.Execs$$anonfun$exec$1.apply(Execs.scala:30) at io.gatling.core.structure.Execs$$anonfun$exec$1.apply(Execs.scala:30) at scala.collection.TraversableLike$$anonfun$flatMap$1.apply(TraversableLike.scala:251) at scala.collection.TraversableLike$$anonfun$flatMap$1.apply(TraversableLike.scala:251) at scala.collection.immutable.List.foreach(List.scala:318) at scala.collection.TraversableLike$class.flatMap(TraversableLike.scala:251) at scala.collection.AbstractTraversable.flatMap(Traversable.scala:105) at io.gatling.core.structure.Execs$class.exec(Execs.scala:30) at io.gatling.core.Predef$.exec(Predef.scala:33) at io.gatling.core.structure.Execs$class.exec(Execs.scala:28) at io.gatling.core.Predef$.exec(Predef.scala:33) at com.cigna.compass.Login$.<init>(Login.scala:19) at com.cigna.compass.Login$.<clinit>(Login.scala) ... 13 more
The scenario itself is pretty simple:
`
class CompassOnlyActions extends StandardSimulation {
run(
scenario(“Load Test - Compass Only”)
.exec( Login.everyone ) // THIS IS LINE 14 - Something wrong with Login.everyone?
.during( Config.testDuration ) {
uniformRandomSwitch(
exec( API.profile ),
exec( API.displaymap ),
exec( API.cards )
)
}
.exec( Logout.sequence )
)
}
`
I shared Login.everyone previously, but here it is again:
I got an extra pair of eyes looking at it with me. Why the compiler let this go but the run-time didn’t work is beyond me:
I use Login.sequence in the definition of Login.singleFile, before I have declared Login.sequence. By just moving the definition of Login.sequence up ahead of the definition of Login.singleFile, the error went away.
IDEs usually warn you about forward references.
I agree, this is something that should be a compiler error, since it usually leads to runtime errors.
It is, fortunately, easy to fix : move the definition, make it a def or make it a lazy val.
The bad news is: It doesn’t work reliably. After between 1 and 5 users do the login, they all get caught in the holding pattern. I’ve got debug statements everywhere, there is not a thread that is stuck in the login process.
I tried tweaking the code slightly, and now it is pretty consistently stuck after the first user. Here’s the modified code:
val loginAllowed = new AtomicBoolean( true ) val singleFile = exec( session => session.set( LOGGED_IN, false ) ) .asLongAs( session => ! session( LOGGED_IN ).as[Boolean] ) { asLongAs( session => ! loginAllowed.compareAndSet( true, false ) ) { pause( 1 ) } .exec( Login.sequence ) // is definitely setting LOGGED_IN to true at the end .exec( session => { loginAllowed.set( true ); session } ) }
In the code I provided, there was such a useless double check with this LOGGED_IN flag.
Your problem is that asLongAs default behavior is to exit ASAP, so you’re never setting loginAllowed back to true.
It was originally asked for the during loop (actually, all loops are implemented on top of asLongAs): loop could actually be during “duration condition + loop content duration” at max.
But then, I wonder why direct asLongAs also default to exitASAP = true. I agree it doesn’t feel natural, as people expect it to behave like a while loop.
I think I’m going to change this before Gatling 2.0 goes final, except if someone raises an argument against it.