Skip to Content

The following steps will explain how to introduce resilience to your application using the SAP S/4HANA Cloud SDK. If you want to follow this tutorial, we highly recommend checking out the previous parts of this series. Note: This post is part of a series. For a complete overview visit the SAP S/4HANA Cloud SDK Overview.

Goal of this Blog Post

This blog post covers the following steps:

  1. Explain what resilience is and why you should care about it
  2. Make the call to the OData Service resilient by using Hystrix-based commands
  3. Write Unit Tests for the new Hystrix-based command
  4. Deploy the application on SAP Cloud Platform Cloud Foundry

Resilience

Consider the following situation: you are developing a cloud application to provide a service to your customers. In order to keep your customers happy, you’re of course interested in achieving the highest possible availability of your application.

However, cloud applications, possibly spanned across multiple services, are inherently complex. So we can assume that something, somewhere will fail at some point in time. For example a call to your database might fail and cause one part of your application to fail. If other parts of your application rely on the part that has failed, these parts will fail as well. So a single failure might cascade through the whole system and break it. This is especially critical for multi-tenancy applications, where a single instance of your application serves multiple customers. A typical S/4HANA multi-tenancy application involves many downstream services, such as on-premise S/4HANA ERP systems.

Let’s look at a concrete example: Suppose you have 30 systems your cloud application is dependent on and each system has a “perfect” availability of 99.99%. This means each service is unavailable for 4.32 minutes each month (43200min * (1 – 0.9999) = 4.32min).

Now assume failures are cascading, so one service being unavailable means the whole application becomes unavailable. Given the equation used above, the situation now looks like this:

43200min * (1 – 0.9999^30) = 43200min * (1 – 0.997) = 129.6min

So your multi-tenancy application is unavailable for more than two hours every month for every customer!

In order to avoid such scenarios, we need to equip applications with the ability to deal with failure. If an application can deal with failures, we call it resilient. So resilience is the means by which we achieve availability.

Hystrix

The SAP S/4HANA Cloud SDK builds upon the Hystrix library in order to provide resilience for your cloud applications. Or, to put into the words of the creators of Hystrix: “Hystrix is a latency and fault tolerance library designed to isolate points of access to remote systems, services and 3rd party libraries, stop cascading failure and enable resilience in complex distributed systems where failure is inevitable.”

Hystrix comes with many interlocking mechanisms to protect your application from failures. The most important are timeouts, thread-pools and circuit-breakers.

  • Timeouts: Hystrix allows setting custom timeout durations for every remote service. If the response time of a remote service exceeds the specified timeout duration, the remote service call is considered as failure. This value should be adapted according to the mean response time of the remote service.
  • Thread-pools: By default, every command has a separate thread-pool from which it can requests threads to execute the remote service call in. This has multiple benefits: every command is isolated from your application, so whatever happens in these threads will not affect the performance of your application. Also, the usage of threads allows Hystrix to perform remote service calls asynchronously and concurrently. These threads are non-container-managed, so regardless of how many threads are used by your Hystrix commands, they do not interfere with your runtime container.
  • Circuit breaker: Hystrix uses the circuit breaker pattern to determine whether a remote service is currently available. Breakers are closed by default. If a remote service call fails too many times, Hystrix will open/trip the breaker. This means that any further calls that should be made to the same remote service, are automatically stopped. Hystrix will periodically check if the service is available again, and open the closed breaker again accordingly. For more information on the circuit breaker pattern, check this article by Martin Fowler.

Additionally, Hystrix enables you to simply provide a fallback solution. So if a call fails, for example because the thread-pool is depleted or the circuit breaker is open/tripped, Hystrix will check whether a fallback is implemented and call it automatically. So even if a service is unavailable we can still provide some useful result, e.g. by serving cached data.

If you want to gain a deeper understanding of the inner workings, checkout the Hystrix Wiki.

Make your OData call resilient

Now that we have covered why resilience is important and how Hystrix can help us achieve resilience, it’s finally time to introduce it into our application. In the last tutorial we created a simple servlet that uses the SDK’s OData abstractions to retrieve costcenters from an ERP system. In order to make this call resilient, we have to wrap it in an ErpCommand. So first we will create the following class:

./application/src/main/java/com/sap/cloud/sdk/tutorial/GetCostCentersCommand.java

package com.sap.cloud.sdk.tutorial;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import java.util.List;
import java.util.Collections;
 
import com.sap.cloud.sdk.odatav2.connectivity.ODataQueryBuilder;
import com.sap.cloud.sdk.s4hana.connectivity.ErpCommand;
import com.sap.cloud.sdk.s4hana.connectivity.ErpConfigContext;
import com.sap.cloud.sdk.s4hana.connectivity.ErpEndpoint;
import com.sap.cloud.sdk.s4hana.connectivity.exception.QueryExecutionException;
 
public class GetCostCentersCommand extends ErpCommand<List<CostCenterDetails>>
{
    private static final Logger logger = LoggerFactory.getLogger(GetCostCentersCommand.class);
 
    protected GetCostCentersCommand( final ErpConfigContext configContext )
    {
        super(GetCostCentersCommand.class, configContext);
    }
 
    @Override
    protected List<CostCenterDetails> run()
        throws Exception
    {
        final ErpEndpoint erpEndpoint = new ErpEndpoint(this.getConfigContext());
 
        final List<CostCenterDetails> costCenters =
            ODataQueryBuilder
                .withEntity("/sap/opu/odata/sap/FCO_PI_COST_CENTER", "CostCenterCollection")
                .select("CostCenterID", "Status", "CompanyCode", "Category", "CostCenterDescription")
                .build()
                .execute(erpEndpoint)
                .asList(CostCenterDetails.class);
 
        return costCenters;
    }
 
    @Override
    protected List<CostCenterDetails> getFallback() {
        return Collections.emptyList();
    }
}

The GetCostCentersCommand class inherits from ErpCommand, which is the SDK’s abstraction to provide easy to use Hystrix commands. To implement a valid ErpCommand we need to do two things: first, we need to provide a constructor. Here we simply add a constructor that takes an ErpConfigContext as parameter. Second, we need to override the run() method. As you might have noticed already, we can simply use the code we used to call our OData service from the previous blog post, and put it into the run() method. No changes needed!

Additionally, by overriding the getFallback() method, we can provide a fallback if the remote service call should fail. In this case we simply return an empty list. We could also serve static data or check wether we have already cached a response to this call.

Hystrix comes with a default configuration, so you don’t need to perform any configuration on your own. In most cases the default configuration will suffice. However, if you need to change the configuration, you can find more information on this topic in Hystrix wiki.

In the following example we set the thread-pool size to 20 and the timeout to 2000 milliseconds:

protected GetCostCentersCommand( final ErpConfigContext configContext )
{
    super(
        HystrixUtil
            .getDefaultErpCommandSetter(
                GetCostCentersCommand.class,
                HystrixUtil.getDefaultErpCommandProperties().withExecutionTimeoutInMilliseconds(2000))
            .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(20)),
        configContext);
 
}

Now that we have a working command, we need to adapt our CostCenterServlet:

./application/src/main/java/com/sap/cloud/sdk/tutorial/CostCenterServlet.java

package com.sap.cloud.sdk.tutorial;
 
import com.google.gson.Gson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
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 java.io.IOException;
import java.util.List;
 
import com.sap.cloud.sdk.s4hana.connectivity.ErpConfigContext;
import com.sap.cloud.sdk.s4hana.connectivity.ErpDestination;
import com.sap.cloud.sdk.s4hana.connectivity.exception.QueryExecutionException;
import com.sap.cloud.sdk.s4hana.serialization.SapClient;
 
@WebServlet( "/costcenters" )
public class CostCenterServlet extends HttpServlet
{
    private static final long serialVersionUID = 1L;
    private static final Logger logger = LoggerFactory.getLogger(HelloWorldServlet.class);
 
    @Override
    protected void doGet( final HttpServletRequest request, final HttpServletResponse response )
        throws ServletException,
            IOException
    {
        final ErpConfigContext configContext =
            new ErpConfigContext(ErpDestination.getDefaultName(), new SapClient("715"));
 
        final List<CostCenterDetails> result = new GetCostCentersCommand(configContext).execute();
 
        response.setContentType("application/json");
        response.getWriter().write(new Gson().toJson(result));
    }
}

As in the last blog post, the first thing we to do is initializing an ErpConfigContext. But thanks to our new GetCostCentersCommand, we can now simply create a new command, provide it with the ErpConfigContext and execute. As before, we get a list of costcenters as result. But now we can be sure, that our application will not fail if the OData service is temporarily unavailable.

Write Unit Tests for the Hystrix command

There are two things we need to address in order to properly unit test our code: we need to provide our tests with an ERP endpoint and a Hystrix request context.

ERP destination

If you run your application on CloudFoundry, the SDK can simply read the ERP destinations according to the configuration provided when deploying the configuration. However, since the unit tests should run locally, we need a way to provide our tests with an ERP destination.

Luckily, the SDK provides a utility class for such purposes – MockUtil. This class allows us to mock the ERP destinations we’d typically find on CloudFoundry. To provide MockUtil with the necessary information, you’ll need to add two files to your resources directory: systems.json and credentials.json. MockUtil will read these files and provide your tests with the ERP destinations accordingly.

{
  "systems": [
    {
      "alias": "ANY_SYSTEM",
      "uri": "https://any-system.com"
    }
  ],
  "erp": {
    "default": "ErpQueryEndpoint",
    "systems": [
      {
        "alias": "ErpQueryEndpoint",
        "systemId": "ERP",
        "uri": "https://my-erp.com",
        "sapClient": "001"
      }
    ]
  }
}
{
  "credentials": [
    {
      "alias": "ErpQueryEndpoint",
      "username": "myusername",
      "password": "mypassword"
    }
  ]
}

Hystrix request context

Since Hystrix commands run in non-container-managed threads, they cannot use ThreadLocal to access request specific information. Therefore Hystrix provides a solution in the form of request contexts. Usually these are created by a servlet filter whenever a request is being made. Since there are no requests in our unit tests, we need a different way to provide a Hystrix request context.

MockUtil is our friend once again. Using MockUtil’s requestContextExecutor() method we can wrap the execution of the the GetCostCentersCommand in a request context.

Now let’s have a look at the code:

package com.sap.cloud.sdk.tutorial;
 
import org.junit.BeforeClass;
import org.junit.Test;
 
import java.util.Collections;
import java.util.List;
 
import com.sap.cloud.sdk.cloudplatform.servlet.Executable;
import com.sap.cloud.sdk.s4hana.connectivity.ErpConfigContext;
import com.sap.cloud.sdk.s4hana.serialization.SapClient;
import com.sap.cloud.sdk.testutil.MockUtil;
 
import static org.assertj.core.api.Assertions.assertThat;
 
public class GetCostCentersCommandTest {
 
    private static final MockUtil mockUtil = new MockUtil();
 
    @BeforeClass
    public static void beforeClass()
    {
        mockUtil.mockDefaults();
        mockUtil.mockErpDestination();
    }
 
    private List<CostCenterDetails> getCostCenters(final String alias, final SapClient sapClient) {
        final ErpConfigContext configContext =
                new ErpConfigContext(alias, sapClient);
 
        return new GetCostCentersCommand(configContext).execute();
    }
 
    @Test
    public void testWithSuccess() throws Exception {
        mockUtil.requestContextExecutor().execute(new Executable() {
            @Override
            public void execute() throws Exception {
                assertThat(getCostCenters(mockUtil.getErpSystem().getAlias(), mockUtil.getErpSystem().getSapClient())).isNotEmpty();
            }
        });
    }
 
    @Test
    public void testWithFallback() throws Exception {
        mockUtil.requestContextExecutor().execute(new Executable() {
            @Override
            public void execute() throws Exception {
                assertThat(getCostCenters("NoErpSystem", mockUtil.getErpSystem().getSapClient())).isEqualTo(Collections.emptyList());
            }
        });
    }
 
}

We use JUnit’s @BeforeClass annotation to setup our mockUtils and to mock the ERP destinations. Then in the tests (@Test) we do the following: first we create a new request context using mockUtil.requestContextExecutor and provide it with a new Executable. Then we override the Executable’s execute() method, where we finally put the code that we actually want to test together with the corresponding assertions.

For testWithSuccess(), we correctly provide the ERP destination information using mockUtil. For the sake of simplicity we simply assert that the response is not empty.

For testWithFallback(), we intentionally provide a wrong alias in order to make the command fail. Since we implemented a fallback for our command that returns an empty list, we assert that we actually receive an empty list as response.

Deploy the application on SAP Cloud Platform Cloud Foundry

As in the previous tutorials, simply run mvn clean install to test and build your application and then run cf push to deploy your updated application to CloudFoundry!

 

This wraps up the tutorial. Stay tuned for more tutorials on the SAP S/4HANA Cloud SDK on topics like caching and security!

To report this post you need to login first.

Be the first to leave a comment

You must be Logged on to comment or reply to a post.

Leave a Reply