jeudi 29 août 2013

Easy stress tests with Gatling

A few month ago, I needed to compare the response time of several webapp pages before and after upgrading a cache component. I thought the easier and faster way to do that would be to write a shell script doing several http calls to the webapp using wget, and to compute myself the average response time. I did that, it worked and I had my expected conclusion that the new cache version was faster. Of course, I wrote my shell script in a dirty way and it finally finished to the trash.

Then I hear about Gatling, a simple alternative to JMeter to do stress tests. I tested it and indeed it was very easy to use it, and even faster than creating an homemade script.

Today I would like to show you how it is easy to write a stress test with this tool.


Start easily with the recorder



Gatling provides an HTTP recorder tool allowing to generate a Gatling test. It is a good way to generate a first test.

To do that :

1. Download a Gatling archive and extract it
2. Launch the recorder : ./bin/recorder.bat or ./bin/recorder.sh
3. Configure where the test must be generated, for that you can change the fields "package", "class name", and "ouput folder" :

Gatling recorder configuration

4. Configure your web browser to use Gatling as a proxy. As see in the recorder configuration, the http port is 8000 and the https port is 8001.

Web browser configuration
5. Press the start button in the recorder
6. Go to the pages you want to test in the web browser
7. Press the stop & save button in the recorder
8. Open the generated test. In this example the test has been generated under C:\gatling-stress-tests\com\mycompany\stresstests\MyFirstStressTest.scala
9. Delete the useless requests. For example in my stress test I don't want the requests retrieving the static resources or the google analytics requests.
10. Rename the scenario and the http methods giving comprehensive titles : we will find these titles in the test reports.
11. Change the count of users to do 50 simultaneous connections to the webapp

Congratulations you have created your first Gatling test :

import io.gatling.core.Predef._
import io.gatling.core.session.Expression
import io.gatling.http.Predef._
import io.gatling.jdbc.Predef._
import io.gatling.http.Headers.Names._
import io.gatling.http.Headers.Values._
import scala.concurrent.duration._
import bootstrap._
import assertions._

class MyFirstStressTest extends Simulation {
  val httpProtocol = http.baseURL("http://mywebsite.com")
                         .acceptHeader("image/png,image/*;q=0.8,*/*;q=0.5")
                         .acceptEncodingHeader("gzip, deflate")
                         .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3")
                         .connection("keep-alive")
                         .userAgentHeader("Mozilla/5.0 (Windows NT 6.1; WOW64; rv:22.0) Gecko/20100101 Firefox/22.0")
  val headers_1 = Map("""Accept""" -> """text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8""")
  val headers_5 = Map("""Accept""" -> """text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8""",
                      """Content-Type""" -> """application/x-www-form-urlencoded""")
  
  val scn = scenario("my first test")
     .exec(http("home page").get("""/""").headers(headers_1))
     .exec(http("login page").get("""/Account/Login""").headers(headers_1))
     .exec(http("login").post("""/Account/Login""").headers(headers_5)
                        .param("""UserName""", """jsebfranck""")
                        .param("""Password""", """my password""")
     .exec(http("news").get("""/News""").headers(headers_1))

  setUp(scn.inject(atOnce(50 user))).protocols(httpProtocol)
}

As you can see, a Gatling test is a Scala script. Don't worry if you don't know Scala, this script is easy to understand, you don't have to understand all Scala mechanisms :
  • httpProtocol variable defines the global parameters to connect to the website
  • headers_1 and headers_5 defines http headers parameters 
  • scn is the test scenario with the title "my first test"
  • the test contains 4 http calls (3 GET and 1 POST) called "home page", "login page", "login" and "news"
  • setUp allows to launch the test
  • atOnce(50 user) indicates that 50 connections will be done simultaneously

Launch the test

Launch a Gatling test is very easy :

1. Launch the gatling executable : ./bin/gatling.bat or ./bin/gatling.sh
2. Select the MyFirstStressTest test
3. Open the HTML report generated in results folder

Several graphics are generated in the report. But two graphics are particularly interesting. The first one give for each HTTP request :
  • the minimum, maximum and average response time
  • the standard deviation : deviation of the requests response time compared to the average response time. A small standard deviation means that the response time is almost the same for each request
  • the 95 percentile : means that 95% of the requests are under this response time 
  • the 99 percentile : means that 99% of the requests are under this response time



If you click on a request title, for example "news", you get more information specific to the request. Here we can see the response time evolution based on the active sessions.


Conclusion

I hope you understood it is not complicated to use a stress test tool like Gatling, even if it is rare for you to do performance benchmarks. This example is very basic and doesn't show all great features of Gatling. If you want to go further, I encourage you to read the following documentations :

dimanche 25 août 2013

EJB 2 on Jboss 7.1 example

I recently have add the opportunity to migrate an historical JEE web application from JBoss 4 to JBoss 7.1. One of the more complicated point I faced was the migration of the EJB2 components. The old xml configuration was not correct anymore and it was very difficult to find documentation, tutorials or concrete examples, even on JBoss forums. Of course this is understandable because this old version of EJB is not used anymore (hopefully!), but it still exists in several historical web applications.

The goal of this new thread is not to explain all the EJB 2 mechanisms but to share what I really missed during the migration, that is a working example of EJB2 sessions and EJB2 entities on JBoss 7.1. Since JEE 6, it is possible to package the EJB directly in a war archive, so this example will be based on that.

The source code is available here.

EJB 2 session java code

I will create an EJB 2 session called HelloWorld with a simple method returning a String object. Three classes must be created to do that :

  • The EJB interface
package com.jsebfranck.jboss.ejb2.session;

import java.rmi.RemoteException;
import javax.ejb.EJBObject;

public interface HelloWorldEJB extends EJBObject {
 public String helloWorld() throws RemoteException;
}

  • The implementation
package com.jsebfranck.jboss.ejb2.session;

import java.rmi.RemoteException;
import javax.ejb.EJBException;
import javax.ejb.SessionBean;
import javax.ejb.SessionContext;

public class HelloWorldEJBBean implements SessionBean {
 private static final long serialVersionUID = 1L;

 public String helloWorld() throws RemoteException {
  return "Hello world, I am an EJB2 session";
 }
 
 public void ejbActivate() throws EJBException, RemoteException {}
 public void ejbPassivate() throws EJBException, RemoteException {}
 public void ejbRemove() throws EJBException, RemoteException {}
 public void setSessionContext(SessionContext arg0) throws EJBException, RemoteException {}
}

  • And the EJB Home
package com.jsebfranck.jboss.ejb2.session;

import java.rmi.RemoteException;
import javax.ejb.CreateException;
import javax.ejb.EJBHome;

public interface HelloWorldEJBHome extends EJBHome {
 public HelloWorldEJB create() throws RemoteException, CreateException;
}

EJB 2 entity java code

For the EJB 2 entity example, I will create an object called Member with two fields : an id and a login. Three java objects are also needed to do that :

  • The EJBLocalObject
package com.jsebfranck.jboss.ejb2.entity;

import javax.ejb.EJBLocalObject;

public interface Member extends EJBLocalObject {
    public Long getId();
    public String getLogin();
}
  • The EntityBean
package com.jsebfranck.jboss.ejb2.entity;

import javax.ejb.CreateException;
import javax.ejb.EntityBean;
import javax.ejb.EntityContext;

public abstract class MemberBean implements EntityBean {

  private static final long serialVersionUID = 1L;
  private transient EntityContext ctx;

  public MemberBean() {
  }

  public Long ejbCreate(Long id, String login) throws CreateException {
    setId(id);
    setLogin(login);
    return id;
  }

  public void ejbPostCreate(Long id, String login) {
  }

  public abstract Long getId();
  public abstract void setId(Long id);

  public abstract String getLogin();
  public abstract void setLogin(String login);

  public void setEntityContext(EntityContext ctx) {
    this.ctx = ctx;
  }

  public void unsetEntityContext() {
    this.ctx = null;
  }

  public void ejbActivate() {}
  public void ejbPassivate() {}
  public void ejbLoad() {}
  public void ejbStore() {}
  public void ejbRemove() {}
}

  • And the EJBHome
package com.jsebfranck.jboss.ejb2.session;

import java.rmi.RemoteException;

import javax.ejb.CreateException;
import javax.ejb.EJBHome;

public interface HelloWorldEJBHome extends EJBHome {
 public HelloWorldEJB create() throws RemoteException, CreateException;
}

XML configuration


Now we have to declare the EJB entity and the EJB session in the xml configuration. A first file is needed for that, ejb-jar.xml. This file must be put in the /WEB-INF folder of the war archive.

<ejb-jar version="3.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns="http://java.sun.com/xml/ns/javaee" 
xsi:schemalocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd">
  <enterprise-beans>
    <session>
      <ejb-name>HelloWorldEJB</ejb-name>
      <home>com.jsebfranck.jboss.ejb2.session.HelloWorldEJBHome</home>
      <remote>com.jsebfranck.jboss.ejb2.session.HelloWorldEJB</remote>
      <ejb-class>com.jsebfranck.jboss.ejb2.session.HelloWorldEJBBean</ejb-class>
      <session-type>Stateless</session-type>
      <transaction-type>Container</transaction-type>
    </session>
    <entity>
      <ejb-name>MemberEJB</ejb-name>
      <local-home>com.jsebfranck.jboss.ejb2.entity.MemberHome</local-home>
      <local>com.jsebfranck.jboss.ejb2.entity.Member</local>
      <ejb-class>com.jsebfranck.jboss.ejb2.entity.MemberBean</ejb-class>
      <persistence-type>Container</persistence-type>
      <prim-key-class>java.lang.Long</prim-key-class>
      <reentrant>false</reentrant>
      <cmp-version>2.x</cmp-version>
      <abstract-schema-name>member</abstract-schema-name>
      <cmp-field>
        <field-name>id</field-name>
      </cmp-field>      
      <cmp-field>
        <field-name>login</field-name>
      </cmp-field>
      <primkey-field>id</primkey-field>
    </entity>
  </enterprise-beans>
</ejb-jar>

Now let's configure the mapping of the EJB entity. This is done in the jbosscmp-jdbc.xml file which must be put in the META-INF folder of the war archive.

<jbosscmp-jdbc>
  <defaults>
    <datasource>java:jboss/datasources/hsqldbDS</datasource>
  </defaults>
  <enterprise-beans>
    <entity>
      <ejb-name>MemberEJB</ejb-name>
      <row-locking>false</row-locking>
      <table-name>simple</table-name>
      <cmp-field>
        <field-name>id</field-name>
        <column-name>id</column-name>
      </cmp-field>      
      <cmp-field>
        <field-name>login</field-name>
        <column-name>login</column-name>
      </cmp-field>
    </entity>
  </enterprise-beans>
</jbosscmp-jdbc>


JBoss configuration

As you noticed in the jbosscmp-jdbc.xml file, a datasource is required for the entity. This datasource is configured directly in the jboss configuration. As I launch JBoss 7.1 in a standalone mode, I put the following configuration in the $JBOSS_HOME/standalone/configuration/standalone-full.xml file.

In this example, a use a hsql database :

<subsystem xmlns="urn:jboss:domain:datasources:1.0"> <datasources> <datasource enabled="true" jndi-name="java:jboss/datasources/hsqldbDS" jta="true" pool-name="hsqldbDS" use-ccm="true" use-java-context="true"> <connection-url>jdbc:hsqldb:file:/Users/jsebfranck/Documents/database/standaloneHsqldb</connection-url> <driver>hsqldb</driver> <pool> <prefill>false</prefill> <use-strict-min>false</use-strict-min> <flush-strategy>FailingConnectionOnly</flush-strategy> </pool> <security> <user-name>sa</user-name> </security> </datasource>


Deployment in jboss

When you deploy the web application on jboss (standalone.sh --server-config=standalone-full.xml), you can see the JNDI names of both EJB in the logs. This will help us to do our lookups in the client.


19:07:56,689 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-2) JBAS015876: Starting deployment of "WarWithEJB2.war"
19:07:57,069 INFO  [org.jboss.as.ejb3.deployment.processors.EjbJndiBindingsDeploymentUnitProcessor] (MSC service thread 1-4) JNDI bindings for session bean named MemberEJB in deployment unit deployment "WarWithEJB2.war" are as follows:

 java:global/WarWithEJB2/MemberEJB!com.jsebfranck.jboss.ejb2.entity.Member
 java:app/WarWithEJB2/MemberEJB!com.jsebfranck.jboss.ejb2.entity.Member
 java:module/MemberEJB!com.jsebfranck.jboss.ejb2.entity.Member
 java:global/WarWithEJB2/MemberEJB!com.jsebfranck.jboss.ejb2.entity.MemberHome
 java:app/WarWithEJB2/MemberEJB!com.jsebfranck.jboss.ejb2.entity.MemberHome
 java:module/MemberEJB!com.jsebfranck.jboss.ejb2.entity.MemberHome

19:07:57,073 INFO  [org.jboss.as.ejb3.deployment.processors.EjbJndiBindingsDeploymentUnitProcessor] (MSC service thread 1-4) JNDI bindings for session bean named HelloWorldEJB in deployment unit deployment "WarWithEJB2.war" are as follows:

 java:global/WarWithEJB2/HelloWorldEJB!com.jsebfranck.jboss.ejb2.session.HelloWorldEJB
 java:app/WarWithEJB2/HelloWorldEJB!com.jsebfranck.jboss.ejb2.session.HelloWorldEJB
 java:module/HelloWorldEJB!com.jsebfranck.jboss.ejb2.session.HelloWorldEJB
 java:jboss/exported/WarWithEJB2/HelloWorldEJB!com.jsebfranck.jboss.ejb2.session.HelloWorldEJB
 java:global/WarWithEJB2/HelloWorldEJB!com.jsebfranck.jboss.ejb2.session.HelloWorldEJBHome
 java:app/WarWithEJB2/HelloWorldEJB!com.jsebfranck.jboss.ejb2.session.HelloWorldEJBHome
 java:module/HelloWorldEJB!com.jsebfranck.jboss.ejb2.session.HelloWorldEJBHome
 java:jboss/exported/WarWithEJB2/HelloWorldEJB!com.jsebfranck.jboss.ejb2.session.HelloWorldEJBHome

Simple servlet client

We are now able to test our EJBs. The following code calls the helloWorld method of the EJB session, then it creates a new line in the Member table.

package com.jsebfranck.jboss.servlet;

import java.io.IOException;

import javax.naming.InitialContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.jsebfranck.jboss.ejb2.entity.Member;
import com.jsebfranck.jboss.ejb2.entity.MemberHome;
import com.jsebfranck.jboss.ejb2.session.HelloWorldEJB;
import com.jsebfranck.jboss.ejb2.session.HelloWorldEJBHome;

@WebServlet("/Ejb2Servlet")
public class Ejb2Servlet extends HttpServlet {
  private static final long serialVersionUID = 1L;

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    try {
      // EJB 2 session
      HelloWorldEJBHome helloWorldEJBHome = (HelloWorldEJBHome) new InitialContext().lookup("java:global/WarWithEJB2/HelloWorldEJB!com.jsebfranck.jboss.ejb2.session.HelloWorldEJBHome");
      HelloWorldEJB helloWorldEjb = helloWorldEJBHome.create();
      response.getWriter().println("EJB session test : " + helloWorldEjb.helloWorld());

      // EJB 2 entity
      MemberHome memberHome = (MemberHome) new InitialContext().lookup("java:global/WarWithEJB2/MemberEJB!com.jsebfranck.jboss.ejb2.entity.MemberHome");
      Member member = memberHome.create(25L, "jsebfranck");
      response.getWriter().println("EJB entity test : " + member.getId() + " - " + member.getLogin());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}

And we have the prove that this example is working :-)


Conclusion

I hope this example helped you to migrate your EJB2 to a newer version of your application server. Don't hesitate to contact me if you need further details to advance in your migration.