Ramin Ismayilov
Ramin Ismayilov

Reputation: 158

How to avoid race conditions for duplicate requests?

Say I receive two concurrent requests with the same payload. But I must (1) perform a single payment transaction (using a third-party API) and (2) somehow return the same response to both requests. It's this second requirement that complicates things. Otherwise, I could have just returned an error response to the duplicate request.

I have two entities: Session and Payment (related via @OneToOne relation). Session has two fields to keep track of the overall state: PaymentStatus (NONE, OK, ERROR), SessionStatus (CHECKED_IN, CHECKED_OUT). The initial condition is NONE and CHECKED_IN.

The request payload does contain a unique session number, which I use to get the relevant session. For now, assume that the payment service is sort of "idempotent" for a unique order id: it performs only one transaction for a given order id. The order id also comes in the request payload (the same value for the twin requests).

The flow I have in mind is along these lines:

  1. Get the session
  2. If session.getPaymentStatus() == OK, find the payment and return success response.
  3. Perform the payment
  4. Save the payment to DB. Session has a field with unique constraint generated from the request payload. So if one of the threads tries to insert a duplicate, a DataIntegrityViolationException will be thrown. I catch it, find the already inserted payment, and return a response based on it.
  5. If no exception is thrown in 4, return the appropriate response.

In this flow, there seems to be at least one scenario where I might have to return error responses to both requests despite the fact that the payment transaction was successfully completed! For instance, say an error occurs for the "first" request, payment is not done, and an error response is returned. But for the "second" request, which happens to take a bit longer to process, payment is done, but upon insertion to DB, the already inserted payment record is discovered, and an error response is formed on the basis of it.

I'd like to avoid all these race condition-like situations. And I've a feeling that I'm missing something very obvious here. In essence, the problem is to somehow make one request to wait for another to complete. Is there a way that I can utilize DB transactions and locks to handle this smoothly?

Above I assumed that the payment service is idempotent for a given order id. What if it wasn't and I had to absolutely avoid sending duplicate requests to it?

Here's the relevant part of the service method:

Session session = sessionRepo.findById(sessionId)
        .orElseThrow(SessionNotFoundException::new);

Payment payment = paymentManager.pay(session, req.getReference(), req.getAmount());

Payment saved;
try {
    saved = paymentRepo.save(payment);
} catch (DataIntegrityViolationException ex) {
    saved = paymentRepo.findByOrderId(req.getReference())
            .orElseThrow(PaymentNotFoundException::new);
}

PaymentStatus status = saved.getSession().getPaymentStatus();
PaymentStage stage = saved.getSession().getPaymentStage();

if (stage == COMPLETION && status == OK)
    return CheckOutResponse.success(req.getTerminalId(), req.getReference(), 
            req.getPlateNumber(), saved.getAmount(), saved.getRrn());

return CheckOutResponse.error(req.getTerminalId(), req.getReference(),
            "Unable to complete transaction.");

Upvotes: 3

Views: 2570

Answers (3)

Alexei Kaigorodov
Alexei Kaigorodov

Reputation: 13525

You are talking about ”same Payload“. So you have to create class Payload with hash/equal methods which implement to notion of ”same”.

Then you create a synchronized hashset for all payloads ever started.

When next request is processed, you create new payload if absent, and starts it. If such payload already existed, then simply return its result. Even existing payload can be not finished, to comfortably wait its result declare payload as CompletableFuture.

Upvotes: 2

John Bollinger
John Bollinger

Reputation: 180113

I'd like to avoid all these race condition-like situations. And I've a feeling that I'm missing something very obvious here. In essence, the problem is to somehow make one request to wait for another to complete. Is there a way that I can utilize DB transactions and locks to handle this smoothly?

I'm inclined to think that there is no way to eliminate all possibility of returning an error response despite the payment being processed successfully, because there are too many places where breakage can occur, including outside your own code. But yes, you can remove some opportunities for inconsistent responses by applying some locking.

For example,

  1. Get the session
  2. Get the session's PaymentStatus and take out a pessimistic lock on it. You must also include code to ensure that this lock is released before request processing completes, even in error cases (I say nothing further about this).
  3. If session.getPaymentStatus() != NONE, return a corresponding response.
  4. Perform the payment
  5. Save the payment to the DB, which I am supposing includes updating the PaymentStatus to either OK or ERROR. Because of the locking, it is not expected that any attempt will be made to insert a duplicate. If that happens anyway then an admin needs to be notified, and a different response should be returned, maybe a 501.
  6. Return the appropriate response.

Note that idempotency of successfully making payment does not help you in this area, but if idempotency extended to payment failure cases then your original workflow would not be susceptible to the inconsistent response issue described in the question.

Upvotes: 2

k-wasilewski
k-wasilewski

Reputation: 4623

I think that this combination of an assignment of an id to an entity (hopefully always the same for the same request) and a UNIQUE (id) constraint constitute a sufficient condition to avoid db duplicates.

If you want to (for a reason unknown to me) avoid this first condition, you can always check the request's timestamp or design your persistance layer to "manually" check for duplicates before updating.

But, as always, the question is what are you trying to acheive? This place (StackOverflow) is more about discussing/correcting the implementations rather then theoretical questions.

edit

If I understand correctly, it's a matter of setting a public static flag somewhere (or a List of flags, you get the idea). In your service, you'd check the flag first, and if true, wait for it to be false; then finally perform the main operation.

As for the duplicate requests, I'd compare every req with the last one. If all the parameters are the same and the timestamp is close enough, I'd return status 400 or whatever.

But I still don't get why would you want the same responses. Of course you could wait for an arbitrary amount of time after receiving every req and before actually executing it, but why not always permit a "unique" request to proceed?

Upvotes: 1

Related Questions