REST Idempotence implementation - how to rollback when the request has already been processed?

What I am trying to achieve

We have a REST API built with Spring Boot, JPA and Hibernate. Clients using the API have unreliable network access. To avoid too many errors for the end user, we forced the client to retry the failed requests (for example, after a timeout had expired).

Since we cannot be sure that the request has not yet been processed by the server when it is accessed again, we must make the POST requests idempotent. That is, sending the same POST request twice should not create the same resource twice.

What have I done so far

To achieve this, here's what I did:

  • The client sends the UUID along with the request in a custom HTTP header.
  • When a client sends the same request, the same UUID is sent.
  • The first time the server processes a request, the response to the request is stored in the database along with the UUID.
  • The second time, when the same request is received, the result is fetched from the database and the response is executed without reprocessing the request.

So far so good.

Problem

I have multiple server instances running on the same database and the queries are load balanced. As a result, any instance can process requests.

In my current implementation, the following scenario might occur:

  • The request is being processed by instance 1 and takes a long time
  • Since it takes too long, the client aborts the connection and sends the same request
  • The second request is processed by instance 2
  • The processing of the first request ends and the result is stored in the database for instance 1
  • The processing of the second request ends. When instance 2 tries to store the result in the database, the result already exists in the database.

In this case, the request is processed twice, which I want to avoid.

I thought of two possible solutions:

  • Rollback request 2 when the result for the same request has already been saved, and send the saved response back to the client.
  • Prevent request 2 from being processed by storing the request id in the database as soon as instance 1 starts processing it. This solution will not work as the connection between the client and instance 1 is timed out, making it impossible for the client to receive the response processed by instance 1.

Attempted solution 1

I am using Filter

to retrieve and save the response. My filter looks something like this:

@Component
public class IdempotentRequestFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException     {

        String requestId = getRequestId(request);


        if(requestId != null) { 

            ResponseCache existingResponse = getExistingResponse(requestId);

            if(existingResponse != null) {
                serveExistingResponse(response, existingResponse);
            }
            else {

                filterChain.doFilter(request, response);

                try {
                    saveResponse(requestId, response);
                    serve(response);
                }
                catch (DataIntegrityViolationException e) {

                    // Here perform rollback somehow

                    existingResponse = getExistingResponse(requestId);
                    serveExistingResponse(response, existingResponse);
                }
            }
        }
        else {
            filterChain.doFilter(request, response);
        }

    }

    ...

      

Then my requests are processed like this:

@Controller 
public class UserController {

    @Autowired 
    UserManager userManager; 

    @RequestMapping(value = "/user", method = RequestMethod.POST)
    @ResponseBody
    public User createUser(@RequestBody User newUser)  {
        return userManager.create(newUser);
    }
}

@Component
@Lazy
public class UserManager {

    @Transactional("transactionManager")
    public User create(User user) {
        userRepository.save(user); 
        return user; 
    }

}

      

Questions

  • Can you think of any other solution to avoid the problem?
  • Is there any other solution to make POST requests idempotent (possibly completely different)?
  • How can I start a transaction, commit it, or reverse it from the Filter

    one shown above? Is this good practice?
  • While processing requests, existing code already creates transactions by calling several methods annotated with @Transactional("transactionManager")

    . What happens when I start or rollback a filtered transaction?

Note. I am new to spring, hibernate and JPA and I have a limited understanding of the transaction mechanism and filters.

+3


source to share


1 answer


The request is being processed by instance 1 and takes a long time

Let's consider splitting the process into 2 stages.



Step 1: saving the request and step 2 of processing the request. On the first request, you simply store all the request data somewhere (maybe a DB or a Queue). Here you can enter statuses, for example. "new", "progress", "ready". You can make them synchronous or asynchronous, it doesn't matter. So, in the second attempt to process the same request, you check if it is already saved and the status. Here you can reply with a status, or just wait until the status becomes "ready". So in the filter, you just check if the request exists (was previously saved) and if so, just get the status and results (if ready) to send the response.

You can add custom validation annotation - @UniqueRequest to RequestDTO and add @Valid for DB validation (see example ). You don't need to do this in the filter, but move the logic to the controller (that's part of the validation really). It's up to you how to answer in this case - just check the BindingResult.

0


source







All Articles