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. 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 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 java.util.List;
import java.util.Collections;
 
import com.netflix.hystrix.HystrixThreadPoolProperties;
import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.frameworks.hystrix.HystrixUtil;
import com.sap.cloud.sdk.odatav2.connectivity.ODataQueryBuilder;
import com.sap.cloud.sdk.s4hana.connectivity.ErpCommand;
import com.sap.cloud.sdk.s4hana.connectivity.ErpConfigContext;
 
public class GetCostCentersCommand extends ErpCommand<List<CostCenterDetails>>
{
    private static final Logger logger = CloudLoggerFactory.getLogger(GetCostCentersCommand.class);
 
    protected GetCostCentersCommand( final ErpConfigContext configContext )
    {
        super(GetCostCentersCommand.class, configContext);
    }
 
    @Override
    protected List<CostCenterDetails> run()
        throws Exception
    {
        final List<CostCenterDetails> costCenters =
            ODataQueryBuilder
                .withEntity("/sap/opu/odata/sap/FCO_PI_COST_CENTER", "CostCenterCollection")
                .select("CostCenterID", "Status", "CompanyCode", "Category", "CostCenterDescription")
                .build()
                .execute(getConfigContext())
                .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 10000 milliseconds.

protected GetCostCentersCommand( final ErpConfigContext configContext )
{
    super(
        HystrixUtil
            .getDefaultErpCommandSetter(
                GetCostCentersCommand.class,
                HystrixUtil.getDefaultErpCommandProperties().withExecutionTimeoutInMilliseconds(10000))
            .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 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.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.s4hana.connectivity.ErpConfigContext;

@WebServlet("/costcenters")
public class CostCenterServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;
    private static final Logger logger = CloudLoggerFactory.getLogger(CostCenterServlet.class);
 
    @Override
    protected void doGet( final HttpServletRequest request, final HttpServletResponse response )
        throws ServletException,
            IOException
    {
        final ErpConfigContext configContext = new ErpConfigContext();
 
        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 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 cost centers as result. But now we can be sure, that our application will not fail if the OData service is temporarily unavailable.

Write Tests for the Hystrix command

There are two things we need to address in order to properly 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 tests should run locally, we need a way to supply 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 a systems.json or systems.yml file to your resources directory (i.e., integration-tests/src/test/resources). MockUtil will read these files and provide your tests with the ERP destinations accordingly. Adapt the URL as before.

{
  "erp": {
    "default": "ERP_TEST_SYSTEM",
    "systems": [
      {
        "alias": "ERP_TEST_SYSTEM",
        "uri": "https://URL"
      }
    ]
  }
}

In addition, you may provide a credentials.yml file to reuse your SAP S/4HANA login configuration as described in the Appendix of tutorial Step 4 with SAP S/4HANA Cloud SDK: Calling an OData Service.

To do this create the following credentials.yml file in a safe location (e.g., like storing your ssh keys in ~/.ssh), i.e., not in the source code repository.

/secure/local/path/credentials.yml

---
credentials:
- alias: "ERP_TEST_SYSTEM"
  username: "user"
  password: "pass"

Afterwards you can pass the credentials.yml when running tests. Make sure to pass the absolute path to the file:

mvn test -Dtest.credentials=/secure/local/path/credentials.yml

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 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, to be placed in a file integration-tests/src/test/java/com/sap/cloud/sdk/tutorial/GetCostCentersCommandTest.java:

package com.sap.cloud.sdk.tutorial;

import org.junit.BeforeClass;
import org.junit.Test;

import java.util.Collections;
import java.util.List;
import java.util.Locale;

import com.sap.cloud.sdk.cloudplatform.servlet.Executable;
import com.sap.cloud.sdk.s4hana.connectivity.ErpConfigContext;
import com.sap.cloud.sdk.s4hana.connectivity.ErpDestination;
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 destination, final SapClient sapClient) {
        final ErpConfigContext configContext =
                new ErpConfigContext(destination, sapClient, Locale.ENGLISH);
 
        return new GetCostCentersCommand(configContext).execute();
    }
 
    @Test
    public void testWithSuccess() throws Exception {
        mockUtil.requestContextExecutor().execute(new Executable() {
            @Override
            public void execute() throws Exception {
                assertThat(getCostCenters(ErpDestination.getDefaultName(), 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 default 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 not existing destination 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.

Now we are supplying the ERP system to use during testing as part of the systems.json (or .yml) file. We can use this mechanism for all our tests. Hence, you can adapt the beforeClass() method in the CostCenterServiceTest from step 4: replace the previous implementation with the same two statements as above in the GetCostCentersCommandTest:

@BeforeClass
public static void beforeClass() throws URISyntaxException
{
    mockUtil.mockDefaults();
    mockUtil.mockErpDestination();
}

Simply run mvn clean install as in the previous tutorials to test and build your application. Consider the following before deploying to Cloud Foundry.

Deploy the application on SAP Cloud Platform Cloud Foundry

The SDK integrates the circuit breakers and other resilience features of Hystrix with the SAP Cloud Platform, specifically with the tenant and user information. For example, circuit breakers are maintained per tenant to ensure that they are properly isolated. As of now, our application is not multitenant as far as user authentication is concerned – we will come to this in the next steps of the tutorial series (see Step 7 for Cloud Foundry and Step 8 on Neo).

When running such a non-multitenant application with caching, the required tenant and user information needs to be supplied separately, or be mocked. The (local) Neo environment  handles this out-of-the-box and you do not need to do anything when developing on Neo. On Cloud Foundry, the SDK provides a workaround for testing purposes, but you need to enable this workaround explicitly for security considerations. To do so, follow the step outlined in the following, but be aware of the security implications explained below.

Supply your Cloud Foundry application with an additional environment variable as follows:

cf set-env firstapp ALLOW_MOCKED_AUTH_HEADER true

When the variable ALLOW_MOCKED_AUTH_HEADER is explicitly set to true, the SDK will fall back to providing mock tenant and user information when no actual tenant information is available. This setting must never be enabled in productive environments. It is only meant to make testing easier if you do not yet implement the authentication mechanisms of Cloud Foundry. Delete the environment variable as soon as it is no longer required, for example, because you implemented Step 7 of this tutorial series (cf unset-env firstapp ALLOW_MOCKED_AUTH_HEADER).

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

If you want to run your application locally with mvn tomee:run (in folder application/, available since version 1.1.1), you need to similarly set the environment variable ALLOW_MOCKED_AUTH_HEADER=true on your local machine before starting the local server, in addition to supplying the destinations (as described in Step 4).

 

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.

2 Comments

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

  1. SIMMACO FERRIERO

    Hi Ekaterina,

    regarding the final part of your blog, I’ve tried with the latest 1.1.2 version of the archetype, but it seems that the Tomee plugin for local testing is not included. I had to manually add the plugin in the pom.xml file of the application.

    However, even after this, the mvn tomee:run command seems to be working fine, but when I do http://localhost:8080/hello I get an error: it says “This localhost page can’t be found”.

    Do you have any tips?

    Simmaco

     

    (0) 
    1. Sander Wozniak

      Hi Simmaco,

      I’ve just created a project as follows:

      mvn archetype:generate -DarchetypeGroupId=com.sap.cloud.s4hana.archetypes -DarchetypeArtifactId=scp-cf-tomee -DarchetypeVersion=1.1.2

      Then, I build and run the project with:

      cd application/
      mvn clean package
      mvn tomee:run

      This is the plugin configuration that comes with the latest archetype:

      <plugin>
          <groupId>org.apache.openejb.maven</groupId>
          <artifactId>tomee-maven-plugin</artifactId>
          <version>1.7.4</version>
          <configuration>
              <context>ROOT</context>
              <libs>
                  <lib>org.apache.tomcat:tomcat-catalina:8.5.20</lib>
              </libs>
          </configuration>
      </plugin>

      Can you please share more details on your configuration?

      Thanks!
      Sander

      (0) 

Leave a Reply