REST with Asynchronous Jersey and RXJava – Part 3

By | December 17, 2016

In this series of articles I will implement a RESTful web service using Spring Boot and Jersey. I will then modify the service as to use asynchronous server-side request processing and RXJava. Load-tests will be made using Gatling before and after the modifications, in order to determine any improvements.

The first article in this series is available here and the second part here.

In this part, part three, I will examine the performance of the example program using the Gatling load-test developed in part two. Apart from the numbers and the excellent reports generated by Gatling, I will also use Java Mission Control to examine the example program during the load-tests.

Raise the Number of Allowed Files/Ports in the OS

In *nix OSes such as Ubuntu and OS X etc, there is a limitation of the number of files and ports that may be open. Depending on how this limit is set, this may affect the result of load-tests. There is an excellent section in the Gatling documentation on OS Tuning and Open File Limits.

In my case, I have raised the maximum number of open file handles on both the computer on which the external database is run and the computer on which the REST service and the Gatling load-test is run.

Use an External Database

During the course of load-testing the example program in preparation for this article, I noticed that running an embedded database or even an external database on the same computer as the example program while it is being load-tested does consume a significant amount of resources.

After trying several alternatives in my somewhat limited machine park, I came to the conclusion that the best alternative seems like running an external H2 database on my laptop and running the load-tests on my stationary computer.

Install and Run the H2 Database

To install and run the H2 database, I did the following:

  • Download the H2 Database Engine.
    I downloaded version 1.4.193, which at the time of writing this article is the latest version available. I downloaded the All Platforms version that comes in a ZIP archive.
  • Unpack the H2 database to a suitable location.
  • In the bin directory in the H2 database directory, create a file named loadtesth2.sh with the following contents:
    #!/bin/sh
    java -Xmx4096m -jar h2-1.4.193.jar -web -webAllowOthers -webPort 8081 -tcp -tcpAllowOthers -tcpPort 1521 -baseDir ./h2data

    Note that you may have to adapt the size of RAM allocated for the H2 database (4096m) depending on the size of your computer’s RAM. The name of the JAR file will also need to be modified if you have downloaded another version of the H2 database engine.

  • Make the custom H2 start-scrip executable.
    chmod +x loadtesth2.sh
  • Raise the number of open files and ports permitted in the system if this number is low.
    In a *nix OS such as Ubuntu or OS X, the number of permitted files and ports can be examined using the ulimit -n command. To set this for the database, the same command can be used followed by a number, for instance ulimit -n 65536
  • Start the H2 database.
    ./loadtesth2.sh

    You should see output similar to this:

    Web Console server running at http://127.0.1.1:8081 (others can connect)
    TCP server running at tcp://127.0.1.1:1521 (others can connect)

If you do not know which IP your “database server” has, you need to find that out. On Ubuntu, for instance, use the ifconfig command.

The H2 Database Web Console

Note that there was a message about a web console when starting the H2 database. This is a nice feature of the H2 database that allows us to inspect the data in the database using a browser. To examine the external database:

  • Open the URL http://[ip of external database]:8081 in a browser.
  • Use the following connection parameters in the login form that appears in your web browser:
    Driver class: org.h2.Driver
    JDBC URL: jdbc:h2:tcp://[ip of external database]:1521/./dbname
    User Name: sa
    Password: [empty]

    H2 Database web console login screen.

    H2 Database web console login screen.

Modify the Example Program to Use an External Database

The following modifications to the example program are required in order for it to use the external H2 database instead of the embedded HSQLDB database:

  • Modify the application.properties file to have the contents below.
    Note that you need to replace the IP address 192.168.1.68 with the IP address of your “database server”.

    spring.jpa.hibernate.use-new-id-generator-mappings=true
    spring.jpa.show-sql=false
    spring.jpa.hibernate.ddl-auto=create-drop
    
    # H2 DB
    spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
    spring.datasource.url=jdbc:h2:tcp://192.168.1.68:1521/dbname;DB_CLOSE_DELAY=-1
    spring.datasource.driverClassName=org.h2.Driver
    spring.datasource.username=sa
    spring.datasource.password=
  • In the Maven pom.xml of the example program, replace the HSQLDB dependency with this H2 DB dependency:
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.193</version>
        </dependency>

The above configuration in the application.properties file will cause the unit-tests to fail unless the external database is actually available at the datasource URL. To run the tests without the external database in place, just comment out the property spring.datasource.url using a # first on the line in question. Don’t forget to remove the comment when you actually want to connect to the external database!

The example application should now be able to start up and connect to the external database without any errors.

Load-Testing

First of all, I will mention that I am running both the example program and the load-tests on one and the same computer. This will of course affect the result, but nevertheless I hope I will get a hint about any performance improvements.

Running a Load-Test

In order to become acquainted with the current load-testing setup, I will run one load-test on the REST example application.

  • Start the example program.
    As earlier, right-click the RestExampleApplication class and run it.
  • Launch Java Mission Control and attach to the example program’s JVM.

    Attach Java Mission Control to the example program.

  • Run the Gatling load-test in a terminal window.
    mvn gatling:test

Observe the display of Java Mission Control as the program runs. In the picture below I have run two 60-second load-tests with Gatling without restarting the example application.

Monitoring two load-tests of the example program in Java Mission Control.

We can see that the CPU and memory usage is moderate during the load-tests.
Note that the example application uses more memory during the second load-test. My suggestion is to run not only load-tests that focus on a high number of requests per second, but to run load-tests that span over a significantly longer period of time, hours if not days, and in which the load is close to the real load expected on the application in question. The latter type of load-tests are focused on long-term memory use and will hopefully catch problems like memory leaks.

If you take a look in the console of the example program, you may see errors like this one after the load-test has ended:

2016-12-10 11:17:19.276 ERROR 1495 --- [o-8080-exec-222] o.g.j.server.ServerRuntime$Responder     : 
An I/O error has occurred while writing a response message entity to the container output stream.

org.glassfish.jersey.server.internal.process.MappableException: org.apache.catalina.connector.ClientAbortException: 
java.io.IOException: Broken pipe

This type of errors are caused by the server, in my case the REST example program, not having had time to process a request before the connection was closed by the client, Gatling in this example.

In the terminal window in which the Gatling load-test was launched, a text-only report will be printed after a load-test:

================================================================================
---- Global Information --------------------------------------------------------
> request count                                       4645 (OK=4645   KO=0     )
> min response time                                      3 (OK=3      KO=-     )
> max response time                                   1365 (OK=1365   KO=-     )
> mean response time                                   104 (OK=104    KO=-     )
> std deviation                                        238 (OK=238    KO=-     )
> response time 50th percentile                         13 (OK=13     KO=-     )
> response time 75th percentile                         38 (OK=38     KO=-     )
> response time 95th percentile                        762 (OK=762    KO=-     )
> response time 99th percentile                       1037 (OK=1037   KO=-     )
> mean requests/sec                                 76.148 (OK=76.148 KO=-     )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms                                          4442 ( 96%)
> 800 ms < t < 1200 ms                                 198 (  4%)
> t > 1200 ms                                            5 (  0%)
> failed                                                 0 (  0%)
================================================================================

On the first row below Global Information, we can see that a total of 4645 requests were sent out by the Gatling load-test. Some rows down we see that the mean number of requests per second during the load-test were 76.148 requests per second.

Looking at the numbers below the Response Time Distribution, we can see that 4% of the requests had a response time between 800 and 1200 ms.
What happened to the requests that I spoke of earlier that caused broken pipes? As far as I have seen such requests are not counted by Gatling. Probably because Gatling doesn’t have an opportunity to record a response before the load-test ends and thus there is no HTTP status or response time for these requests.

Finally I want to recommend restarting the application to be load-tested before each load-test, unless of course you are interested in looking at the long-term behaviour of your application. The restarts are in order to attempt to establish some kind of predictability in the load-tests and will also ensure that the database is cleared.

Load-Test Baseline

If I increase the parameters that specify the number of simulated users and number of requests per second in the load-test until there are errors in the REST example program console after the load-test and some distribution between the three response time distribution groups, I end up with the following values:

    /* Simulation timing and load parameters. */
    val rampUpTimeSecs = 2
    val testTimeSecs = 60
    val noOfUsers = 200
    val noOfRequestPerSeconds = 600

These values are no absolute science so don’t spend too much time on trying to get them “right”.

The final console output from the load-test run with these parameters on my computer:

================================================================================
---- Global Information --------------------------------------------------------
> request count                                       5311 (OK=5311   KO=0     )
> min response time                                      6 (OK=6      KO=-     )
> max response time                                  13418 (OK=13418  KO=-     )
> mean response time                                  2164 (OK=2164   KO=-     )
> std deviation                                       2446 (OK=2446   KO=-     )
> response time 50th percentile                       1470 (OK=1470   KO=-     )
> response time 75th percentile                       2112 (OK=2112   KO=-     )
> response time 95th percentile                       8671 (OK=8671   KO=-     )
> response time 99th percentile                      12468 (OK=12468  KO=-     )
> mean requests/sec                                 84.302 (OK=84.302 KO=-     )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms                                          1031 ( 19%)
> 800 ms < t < 1200 ms                                 974 ( 18%)
> t > 1200 ms                                         3306 ( 62%)
> failed                                                 0 (  0%)
================================================================================

I will focus my interest to two graphs from the Gatling report; the response time distribution graph and the graph showing the number of responses per second.

The two graphs for the load-test baseline looks like this:

Response time distribution of the baseline load-test of the unmodified version of the REST service.

Response time distribution of the baseline load-test of the unmodified version of the REST service.

Responses per second of the baseline load-test of the unmodified version of the REST service.

Load-Test With Stubbed Database

I know that the database is a bottleneck as far as performance of my example application is concerned, but I did not know the magnitude of impact. In order to determine the impact I created a new base-class for the services which did not use a repository but just returned some data as to simulate instantaneous storage and retrieval. In addition I tweaked the load-test parameters and ended up with the following, after which no additional increase in performance were noted:

    /* Simulation timing and load parameters. */
    val rampUpTimeSecs = 2
    val testTimeSecs = 60
    val noOfUsers = 1000
    val noOfRequestPerSeconds = 20000

The two Gatling graphs that I have chosen to look at look like this for the load-test with the stubbed database:

cResponses per second for the load-test of the REST example application with the stubbed database.

We can see that the response times are very short and that the number of responses per second reach about 20,000 responses per second on my computer.
What is the value of this test with a stubbed database?
Well, it did confirm my suspicion that the database slows my application down significantly. I also learned that there seem to be some kind of upper limit at about 20K requests per second in my environment. Regardless of this, I will perform all the remaining load-tests with a real database since this will cause a certain stress on my service layer, which will have to wait for the slower persistence layer.
Learning whether my application is able to cope with interacting with a slow persistence layer is, as far as I am concerned, one important reason for running the load-tests.

Introduce RXJava

In the quest for performance and beautiful code, the first step will be to use RXJava in the example program. We’ll do this by replacing the abstract base classes of services and resources with RXJava versions.

Add RXJava Dependency

As mentioned in the first article, I have chosen to use version 2. of RXJava. Adding RXJava to a project is as simple as adding one single dependency.

  • In the pom.xml file of the example program, add the RXJava dependency:
            <dependency>
                <groupId>io.reactivex.rxjava2</groupId>
                <artifactId>rxjava</artifactId>
                <version>2.0.1</version>
            </dependency>

New Service Base Class

Instead of modifying the service base class, I have chosen to implement a new version.

  • Implement the AbstractServiceBaseRxJava as shown below:
    package se.ivankrizsan.restexample.services;
    
    import io.reactivex.Observable;
    import org.springframework.transaction.annotation.Transactional;
    import se.ivankrizsan.restexample.domain.LongIdEntity;
    import se.ivankrizsan.restexample.repositories.customisation.JpaRepositoryCustomisations;
    
    import java.util.List;
    
    /**
     * Abstract base class for services that has operations for creating, reading,
     * updating and deleting entities.
     * This implementation uses RxJava.
     *
     * @param <E> Entity type.
     * @author Ivan Krizsan
     */
    @Transactional
    public abstract class AbstractServiceBaseRxJava<E extends LongIdEntity> {
        /* Constant(s): */
    
        /* Instance variable(s): */
        protected JpaRepositoryCustomisations<E> mRepository;
    
        /**
         * Creates a service instance that will use the supplied repository for
         * entity persistence.
         *
         * @param inRepository Entity repository.
         */
        public AbstractServiceBaseRxJava(
            final JpaRepositoryCustomisations<E> inRepository) {
            mRepository = inRepository;
        }
    
        /**
         * Saves the supplied entity.
         *
         * @param inEntity Entity to save.
         * @return Observable that will receive the saved entity,
         * or exception if error occurs.
         */
        public Observable<E> save(final E inEntity) {
            return Observable.create(inSource -> {
                try {
                    final E theSavedEntity = mRepository.save(inEntity);
                    inSource.onNext(theSavedEntity);
                    inSource.onComplete();
                } catch (final Exception theException) {
                    inSource.onError(theException);
                }
            });
        }
    
        /**
         * Updates the supplied entity.
         *
         * @param inEntity Entity to update.
         * @return Observable that will receive the updated entity,
         * or exception if error occurs.
         */
        public Observable<E> update(final E inEntity) {
            return Observable.create(inSource -> {
                try {
                    final E theUpdatedEntity = mRepository.persist(inEntity);
                    inSource.onNext(theUpdatedEntity);
                    inSource.onComplete();
                } catch (final Exception theException) {
                    inSource.onError(theException);
                }
            });
        }
    
        /**
         * Finds the entity having supplied id.
         *
         * @param inEntityId Id of entity to retrieve.
         * @return Observable that will receive the found entity,
         * or exception if error occurs or no entity is found.
         */
        @Transactional(readOnly = true)
        public Observable<E> find(final Long inEntityId) {
            return Observable.create(inSource -> {
                try {
                    final E theEntity = mRepository.findOne(inEntityId);
                    if (theEntity != null) {
                        inSource.onNext(theEntity);
                        inSource.onComplete();
                    } else {
                        inSource.onError(new Error(
                            "Cannot find entity with id " + inEntityId));
                    }
                } catch (final Exception theException) {
                    inSource.onError(theException);
                }
            });
        }
    
        /**
         * Finds all the entities.
         *
         * @return Observable that will receive a list of entities,
         * or exception if error occurs.
         */
        @Transactional(readOnly = true)
        public Observable<List<E>> findAll() {
            return Observable.create(inSource -> {
                try {
                    final List<E> theEntitiesList = mRepository.findAll();
                    inSource.onNext(theEntitiesList);
                    inSource.onComplete();
                } catch (final Exception theException) {
                    inSource.onError(theException);
                }
            });
        }
    
        /**
         * Deletes the entity having supplied id.
         *
         * @param inId Id of entity to delete.
         * @return Observable that will receive completion, or exception if error occurs.
         */
        public Observable delete(final Long inId) {
            return Observable.create(inSource -> {
                try {
                    mRepository.delete(inId);
                    inSource.onComplete();
                } catch (final Exception theException) {
                    inSource.onError(theException);
                }
            });
        }
    
        /**
         * Deletes all entities.
         *
         * @return Observable that will receive completion, or exception if error occurs.
         */
        public Observable deleteAll() {
            return Observable.create(inSource -> {
                try {
                    mRepository.deleteAll();
                    inSource.onComplete();
                } catch (final Exception theException) {
                    inSource.onError(theException);
                }
            });
        }
    }

Note that:

  • The return type of all methods have changed to Observable.
    An observable in the RXJava sense can be likened to a box in which we pack some code. The code won’t be executed until we, at some later point in time, open the box to see what is inside. Observables can be composed and also allow for synchronous or asynchronous execution – please refer to the RXJava wiki for more information.
    Later we will see how an observable is used on the client side.
  • The implementation of the methods have changed.
    To be honest, the amount of boilerplate code has increased. Let’s take a look at the save method before and after RXJava.

        public E save(final E inEntity) {
            final E theSavedEntity = mRepository.save(inEntity);
            return theSavedEntity;
        }
        public Observable<E> save(final E inEntity) {
            return Observable.create(inSource -> {
                try {
                    final E theSavedEntity = mRepository.save(inEntity);
                    inSource.onNext(theSavedEntity);
                    inSource.onComplete();
                } catch (final Exception theException) {
                    inSource.onError(theException);
                }
            });
        }

Concerning the new version of the save method, note that:

  • The saved entity is not returned but instead passed to the observable using the onNext method.
    A RXJava observable is an emitter of signals. Three types of signals can be emitted by an emitter: A normal value is emitted by invoking the onNext method, an error is emitted by invoking the onError method and finally the completion of an emitter is signaled by invoking the onComplete method on the emitter.
    Thus the saved entity is emitted as a normal value from the observable.
  • After the saved entity has been emitted, the onComplete method is invoked on the observable.
    As above, this emits a signal that indicates that the emitter (observable) has completed doing what it is supposed to do and that there will be no further signals from the observable.
  • The RXJava version of the save method does handle exceptions.
    In the old save method, we could rely on regular Java exceptions, with its advantages and drawbacks. In the RXJava version we want to emit an error signal instead of throwing an exception – in the catch block, the onError method is invoked on the observable with an exception as parameter.
  • In the catch block, onComplete is not invoked after having called onError on the observable.
    Calling onError terminates the observable and any further calls on it, such as onComplete, will not emit any signals.

If we go back to the AbstractServiceBaseRxJava class:

  • In the find method, the found entity is verified not to be null before calling onNext on the observable.
    RXJava 2 does not allow emitting null values and thus we need to make sure that there indeed is an entity to emit.

Update Services’ Superclass

Introducing the new service base-class in the concrete service classes is as simple as replacing the superclass.

  • In the CircleService class, replace the superclass (was AbstractServiceBasePlain) with AbstractServiceBaseRxJava.
  • In the RectangleService class, replace the superclass with AbstractServiceBaseRxJava.
  • In the DrawingService class, replace the superclass with AbstractServiceBaseRxJava.
  • Delete the old superclass AbstractServiceBasePlain from the project.

New REST Resource Base Class

Updating the services is not sufficient, the REST resource base-class also must be updated since it is the client of the services. As with the service base class, I will create a new base-class for the REST resources.

  • Create the class RestResourceBaseRxJava implemented as follows:
    package se.ivankrizsan.restexample.restadapter;
    
    
    import se.ivankrizsan.restexample.domain.LongIdEntity;
    import se.ivankrizsan.restexample.services.AbstractServiceBaseRxJava;
    
    import javax.validation.constraints.NotNull;
    import javax.ws.rs.*;
    import javax.ws.rs.core.MediaType;
    import javax.ws.rs.core.Response;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicReference;
    
    /**
     * Abstract base class for REST resources exposing operations on an entity type.
     * All operations will return HTTP status 500 with a plain text body containing an
     * error message if an error occurred during request processing.
     *
     * @param <E> Entity type.
     * @author Ivan Krizsan
     */
    @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
    @Consumes({MediaType.APPLICATION_JSON})
    public abstract class RestResourceBaseRxJava<E extends LongIdEntity> {
        /* Constant(s): */
    
        /* Instance variable(s): */
        protected AbstractServiceBaseRxJava<E> mService;
    
        /**
         * Retrieves all entities.
         *
         * @return HTTP response object with HTTP status 200 if operation succeeded or
         * HTTP error status code and a plain-text error message if an error occurred.
         */
        @GET
        public Response getAll() {
            final AtomicReference<Response> theResponseContainer =
                new AtomicReference<>();
    
            mService.findAll().subscribe(
                inResult ->
                    theResponseContainer.set(
                        Response.ok(entityListToArray(inResult)).build()),
                inError ->
                    theResponseContainer.set(
                        Response. status(500).
                        entity("An error occurred retrieving all entities: "
                            + inError.getMessage()).
                        type(MediaType.TEXT_PLAIN_TYPE).
                        build()));
            return theResponseContainer.get();
        }
    
        /**
         * Deletes the entity with supplied id.
         *
         * @param inEntityId Id of entity to delete.
         * @return HTTP response object with HTTP status 200 if operation succeeded or
         * HTTP error status code and a plain-text error message if an error occurred.
         */
        @DELETE
        @Path("{id}")
        public Response deleteEntityById(@PathParam("id") @NotNull final Long inEntityId) {
            final AtomicReference<Response> theResponseContainer =
                new AtomicReference<>();
    
            mService.delete(inEntityId).subscribe(
                inResult -> theResponseContainer.set(Response.ok().build()),
                inError -> theResponseContainer.set(
                    Response.
                        status(500).
                        entity("An error occurred deleting entity with id " + inEntityId).
                        type(MediaType.TEXT_PLAIN_TYPE).
                        build()),
                () -> theResponseContainer.set(Response.ok().build()));
            return theResponseContainer.get();
        }
    
        /**
         * Deletes all entities.
         * Will return HTTP status 500 if error occurred during request processing.
         *
         * @return HTTP response object with HTTP status 200 if operation succeeded or
         * HTTP error status code and a plain-text error message if an error occurred.
         */
        @DELETE
        public Response deleteAllEntities() {
            final AtomicReference<Response> theResponseContainer =
                new AtomicReference<>();
    
            mService.deleteAll().subscribe(
                inResult -> theResponseContainer.set(Response.ok().build()),
                inError -> theResponseContainer.set(
                    Response.
                        status(500).
                        entity("An error occurred deleting all entities.").
                        type(MediaType.TEXT_PLAIN_TYPE).
                        build()),
                () -> theResponseContainer.set(Response.ok().build()));
            return theResponseContainer.get();
        }
    
        /**
         * Retrieves entity with supplied id.
         *
         * @param inEntityId Id of entity to retrieve.
         * @return HTTP response object with HTTP status 200 if operation succeeded or
         * HTTP error status code and a plain-text error message if an error occurred.
         */
        @GET
        @Path("{id}")
        public Response getEntityById(@PathParam("id") Long inEntityId) {
            final AtomicReference<Response> theResponseContainer =
                new AtomicReference<>();
    
            mService.find(inEntityId).subscribe(
                inResult -> theResponseContainer.set(Response.ok(inResult).build()),
                inError -> theResponseContainer.set(
                    Response.
                        status(500).
                        entity(inError.getMessage()).
                        type(MediaType.TEXT_PLAIN_TYPE).
                        build()));
            return theResponseContainer.get();
        }
    
        /**
         * Updates the entity with supplied id by overwriting it with the supplied entity.
         *
         * @param inEntity Entity data to write.
         * @param inEntityId Id of entity to update.
         * @return HTTP response object with HTTP status 200 if operation succeeded or
         * HTTP error status code and a plain-text error message if an error occurred.
         */
        @PUT
        @Path("{id}")
        public Response updateEntity(final E inEntity,
            @PathParam("id") @NotNull final Long inEntityId) {
            final AtomicReference<Response> theResponseContainer =
                new AtomicReference<>();
    
            inEntity.setId(inEntityId);
            mService.update(inEntity).subscribe(
                inResult -> theResponseContainer.set(Response.ok(inResult).build()),
                inError -> theResponseContainer.set(
                    Response.
                        status(500).
                        entity("An error occurred updating entity with id "
                            + inEntityId + ": " + inError.getMessage()).
                        type(MediaType.TEXT_PLAIN_TYPE).
                        build()));
            return theResponseContainer.get();
        }
    
        /**
         * Creates a new entity using the supplied entity data.
         *
         * @param inEntity Entity data to use when creating new entity.
         * @return HTTP response object with HTTP status 200 containing entity
         * representation if operation succeeded or HTTP error status code and
         * a plain-text error message if an error occurred.
         */
        @POST
        public Response createEntity(final E inEntity) {
            if (inEntity.getId() != null) {
                return Response.status(400).entity(
                    "Id must not be set on new entity").build();
            }
    
            final AtomicReference<Response> theResponseContainer =
                new AtomicReference<>();
    
            mService.save(inEntity).subscribe(
                inResult -> theResponseContainer.set(Response.ok(inResult).build()),
                inError -> theResponseContainer.set(
                    Response.
                        status(500).
                        entity("An error occurred creating a new entity: "
                            + inError.getMessage()).
                        type(MediaType.TEXT_PLAIN_TYPE).
                        build()));
            return theResponseContainer.get();
        }
    
        /**
         * Creates an array containing the entities in the supplied list.
         *
         * @param inEntityList List of entities.
         * @return Array containing the entities from the list.
         */
        protected abstract E[] entityListToArray(final List<E> inEntityList);
    
        public AbstractServiceBaseRxJava<E> getService() {
            return mService;
        }
    
        public void setService(final AbstractServiceBaseRxJava<E> inService) {
            mService = inService;
        }
    }

Note that:

  • The instance variable mService is of the type AbstractServiceBaseRxJava.
  • In for instance the method getAll the previous construct that used the performServiceOperation method has been replaced by an RXJava invocation that subscribes to the observable produced by the service invocation.
    When subscribing to an observable, you may provide up to three callbacks; one for the next value emitted by the observable, one for errors and a final callback for completion of the observable.
  • All the methods that return a Response object use a container of the type AtomicReference<Response> to convey the object to be returned from the RXJava callbacks to the surrounding method.
    One can supposedly use a final variable in the surrounding method and set its value from the RXJava callbacks, but my IDE would not let, so I opted for the AtomicReference<Response> solution.

Update REST Resources’ Superclass

As with the service, the superclass of the concrete REST resource classes also need to be updated to the new REST resource base class that uses RXJava.

  • In the CircleResource class, replace the superclass (was RestResourceBasePlain) with RestResourceBaseRxJava.
  • In the RectangleResource class, replace the superclass with RestResourceBaseRxJava.
  • In the DrawingResource class, replace the superclass with RestResourceBaseRxJava.
  • Delete the old superclass RestResourceBasePlain from the project.

Run Tests

To verify that our modifications hasn’t broken the program, we can now run all the tests. Remember that the tests are TestNG tests – in my IDE there is a special alternative for running all the TestNG tests in a project.

Alternatively you can run all the tests using Maven. In a terminal window, use the command:

mvn test

All tests should pass.

Run the Load-Test

We are now ready to run the load-test on the new version of the REST service.

  • Start the example program.
    As earlier, right-click the RestExampleApplication class and run it.
  • Run the Gatling load-test in a terminal window.
    mvn gatling:test
  • Examine the Gatling load-test report.

My report written to the terminal window looks like this:

================================================================================
---- Global Information --------------------------------------------------------
> request count                                       5340 (OK=5340   KO=0     )
> min response time                                      9 (OK=9      KO=-     )
> max response time                                  13191 (OK=13191  KO=-     )
> mean response time                                  2147 (OK=2147   KO=-     )
> std deviation                                       2434 (OK=2434   KO=-     )
> response time 50th percentile                       1492 (OK=1492   KO=-     )
> response time 75th percentile                       2118 (OK=2118   KO=-     )
> response time 95th percentile                       8751 (OK=8751   KO=-     )
> response time 99th percentile                      12213 (OK=12213  KO=-     )
> mean requests/sec                                 86.129 (OK=86.129 KO=-     )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms                                          1119 ( 21%)
> 800 ms < t < 1200 ms                                 938 ( 18%)
> t > 1200 ms                                         3283 ( 61%)
> failed                                                 0 (  0%)
================================================================================

The response time distribution diagram from the graphical version of the report looks like this:

Response time distribution from load test of the RXJava version of the example program.

Response time distribution from load-test of the RXJava version of the example program.

This is the diagram showing responses per second:

Responses per second from the load-test of the RXJava version of the example program.

RXJava With Stubbed Database

After having written the conclusion, I felt that this article would be more incomplete if I did not run a load-test after having introduced RXJava and with the stubbed database.

Here are the two graphs from the Gatling load-test report from that test:

Response time distribution graph from load-test of example program with RXJava and stubbed database.

Responses per second graph from load-test of example program with RXJava and stubbed database.

Conclusion

If we place the response time distribution graph from the baseline load-test in the background, having a blue colour, and the same graph from the RXJava load-test above it, having a purple colour we can see that there are only minor differences between the two graphs.

Response time distribution graphs from the baseline (blue) and RXJava (purple) load-tests.

One conclusion that can be drawn from this is that the performance after having introduced RXJava is about the same as before and that RXJava has neither introduced any performance degradation but also no performance improvements.
Given the earlier load-test with a stubbed database, I would like to rephrase the conclusion a bit:
Any performance improvement or degradation obtained by introducing RXJava in the example program is negligible in comparison with the performance impact caused by the database.

Having added the above section with load-test of the RXJava version of the example program with a stubbed database, we can indeed see that RXJava does not seem to cause neither performance degradation nor performance improvements.

So what is there to learn from this?
If this article were about performance optimization of the example program, I would have made a big mistake by introducing RXJava instead of looking at the real bottleneck which is the database.

RXJava seems to be a good framework in that it holds up well under load, given the simple scenarios in the example program, and does not introduce any performance degradation.
In the example program, the benefit from introducing RXJava is limited; somewhat better error handling and a higher degree of uniformity, as far as RXJava signal passing is concerned.
I do suspect that the example program is too trivial and/or of a type that does not reap large advantages from the use of RXJava.

In the next part of this series, asynchronous request processing with Jersey (JAX-RS) will be added to the example program and further load-tests run.

Happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *