Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practice to 'rollback' REST method calls inside method

The title might be incorrect, but I will try to explain my issue. My project is a Spring Boot project. I have services which do calls to external REST endpoints.

I have a service method which contains several method calls to other services I have. Every individual method call can be successful or not. Every method call is done to a REST endpoint and there can be issues that for example the webservice is not available or that it throws an unknown exception in rare cases. What ever happens, I need to be able to track which method calls were successful and if any one of them fails, I want to rollback to the original state as if nothing happened, see it a bit as @Transactional annotation. All REST calls are different endpoints and need to be called separately and are from an external party which I don't have influence on. Example:

public MyServiceImpl implements MyService {
@Autowired
private Process1Service;
@Autowired
private Process2Service;
@Autowired
private Process3Service;
@Autowired
private Process4Service;

public void bundledProcess() {
       process1Service.createFileRESTcall();
       process2Service.addFilePermissionsRESTcall();
       process3Service.addFileMetadataRESTcall(); <-- might fail for example
       process4Service.addFileTimestampRESTcall();       
  }
}

If for example process3Service.addFileMetadataRESTcall fails I want to do something like undo (in reverse order) for every step before process3:

process2Service.removeFilePermissionsRESTcall();
process1Service.deleteFileRESTcall();

I read about the Command pattern, but that seems to be used for Undo actions inside an application as a sort of history of actions performed, not inside a Spring web application. Is this correct for my use case too or should I track per method/webservice call if it was successful? Is there a best practice for doing this?

I guess however I track it, I need to know which method call failed and from there on perform my 'undo' method REST calls. Although in theory even these calls might also fail of course.

My main goal is to not have files being created (in my example) which any further processes have not been performed on. It should either be all successful or nothing. A sort of transactional.

Update1: improved pseudo implementation based on comments:

public Process1ServiceImpl implements Process1Service {
    public void createFileRESTcall() throws MyException {
         // Call an external REST api, pseudo code:
         if (REST-call fails) {
            throw new MyException("External REST api failed");
         }                         
    }
}

public class BundledProcessEvent {
    private boolean createFileSuccess;
    private boolean addFilePermissionsSuccess;
    private boolean addFileMetadataSuccess;
    private boolean addFileTimestampSuccess;

    // Getters and setters
}

public MyServiceImpl implements MyService {
@Autowired
private Process1Service;
@Autowired
private Process2Service;
@Autowired
private Process3Service;
@Autowired
private Process4Service;
@Autowired
private ApplicationEventPublisher applicationEventPublisher;

@Transactional(rollbackOn = MyException.class)
public void bundledProcess() {
   BundleProcessEvent bundleProcessEvent = new BundleProcessEvent();
   this.applicationEventPublisher.publishEvent(bundleProcessEvent);

   bundleProcessEvent.setCreateFileSuccess = bundprocess1Service.createFileRESTcall();
   bundleProcessEvent.setAddFilePermissionsSuccess = process2Service.addFilePermissionsRESTcall();
   bundleProcessEvent.setAddFileMetadataSuccess = process3Service.addFileMetadataRESTcall();
   bundleProcessEvent.setAddFileTimestampSuccess = process4Service.addFileTimestampRESTcall();
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void rollback(BundleProcessEvent bundleProcessEvent) {
      // If the last process event is successful, we should not
      // be in this rollback method even
      //if (bundleProcessEvent.isAddFileTimestampSuccess()) {
         // remove timestamp
      //}

      if (bundleProcessEvent.isAddFileMetadataSuccess()) {
         // remove metadata
      }

      if (bundleProcessEvent.isAddFilePermissionsSuccess()) {
         // remove file permissions
      }

      if (bundleProcessEvent.isCreateFileSuccess()) {
         // remove file
      }
}
like image 213
tjeerdnet Avatar asked Mar 11 '18 22:03

tjeerdnet


People also ask

How to rollback in REST API?

Since REST is stateless by design, there is no way to roll back a transaction.

How do I rollback in hibernate Spring transaction?

Transaction Rollback. The @Transactional annotation is the metadata that specifies the semantics of the transactions on a method. We have two ways to rollback a transaction: declarative and programmatic. In the declarative approach, we annotate the methods with the @Transactional annotation.

What is PlatformTransactionManager in Spring boot?

PlatformTransactionManager implementations are defined like any other object (or bean) in the Spring Framework IoC container. This benefit alone makes Spring Framework transactions a worthwhile abstraction even when you work with JTA. Transactional code can be tested much more easily than if it used JTA directly.


2 Answers

Your operation looks like a transaction, so you can use @Transactional annotation. From your code I can't really tell how you are managing HTTP response calls for each of those operations, but you should consider having your service methods to return them, and then do a rollback depending on response calls. You can create an array of methods like so, but how exactly you want your logic to be is up to you.

private Process[] restCalls = new Process[] {
        new Process() { public void call() { process1Service.createFileRESTcall(); } },
        new Process() { public void call() { process2Service.addFilePermissionsRESTcall(); } },
        new Process() { public void call() { process3Service.addFileMetadataRESTcall(); } },
        new Process() { public void call() { process4Service.addFileTimestampRESTcall(); } },
};

interface Process {
    void call();
}

@Transactional(rollbackOn = Exception.class)
public void bundledProcess() {
    restCalls[0].call(); 
    ... // say, see which process returned wrong response code
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void rollback() {
    // handle rollback according to failed method index
}

Check this article. Might come in handy.

like image 110
vyrwu Avatar answered Sep 28 '22 07:09

vyrwu


The answer to this question is quite broad. There are various ways to do distributed transactions to go through them all here. However, since you are using Java and Spring, your best bet is to use something like JTA (Java Transaction API), which enables a distributed transactions across multiple services/instances/etc.. Fortunately, Spring Boot supports JTA using either Atomikos or Bitronix. You can read the doc here.

One approach to enable distributed transactions is through a message broker such as JMS, RabbitMQ, Kafka, ActiveMQ, etc. and use a protocol like XA transactions (two-phase commit). In the case of external services that do not support distributed, one approach is to write a wrapper service that understands XA transactions to that external service.

like image 27
Christopher Z Avatar answered Sep 28 '22 08:09

Christopher Z