For most web application / REST form POSTs, you usually do not have a concern with double-click protection. But, sometimes it is important to worry about double-clicks, and NOT processing two or more requests independently and simultaneously.
Let us suppose you are implementing a form POST in a web application where a commerce transaction is involved. At some stage, some customer will accidentally or purposefully double-click the FORM submit button, creating two or more processing threads in your web application that perform the same task twice. A number of issues can arise when this happens:
In this article, we describe how one can go about minimizing the chances of these double-click issues arising. I offer four integrated strategies for handling the processing of double-clicks for commerce transactions. The strategies involve avoidance, detection, prevention, and recovery from double-clicks, while minimizing race conditions.
An aside concerning deadlocks and double-clicks: If your code involves locking, the multiple clicks will spawn threads that may grab the lock resources in different orders. This could cause deadlocks at which point your commerce transactions would both be blocked. This would be indicative of a more general problem with your locking strategies or design. I am not going to discuss strategies for deadlock detection, prevention or avoidance techniques here. It is assumed that you are not grabbing locks in different orders during the commerce transaction, or are handling that correctly, so I will not talk about deadlocks further - they are out of scope.
There are four levels of protection, described in the next sections.
We disable the transaction submit button when it is clicked the first time on the page.
We see if a transaction has been made since we sent the triggering page; if so we simply confirm the latest transaction rather than process another transaction.
This covers the common case where someone gets impatient and clicks a second time, after the first transaction is complete but JUST before the browser shows the success page. This is not foolproof, but covers the common case where a badly-timed click can trigger a second unwanted transaction.
This happens when the user somehow clicks the submit button multiple times in succession, or when a bot attempts multiple transactions at once or in rapid succession. We use a lock to detect the presence of a winning thread who processes the transactions. Other threads wait for the winning thread to complete, and one of them will confirm the transaction to the user.
In some more detail because this is the heart of the solution: We hold a 'transaction-lock' on a stable client identifier for the duration of the transaction to prevent multiple-transactions due to fast clicking. By locking the transactions to one-per stable-client-id, we set up the world so that we get one 'click' that is the processing click (holding the lock), and all other clicks (let's call them locked-out clicks). One of the locked out clicks will become the one responsible for sending the response back to the user (let's call it the display-click).
All locked-out clicks wait for the processing click to finish. Once that happens, they all look for the completed transaction data and confirm that data back to the client.This ensures the client gets their confirmation, and completes the purchase. These just happen via separate 'clicks' and is better than doing nothing.
You might ask about what happens if the locked-out threads remain locked, or if there is no completed transaction data to send back etc.
A locked-out thread remains locked for 25 seconds (configurable). When this happens, we have fallback logic that tries very hard to send the appropriate confirmation back to the client using the prevention strategy above. If we just cannot be sure that the transaction succeeded, we really need to provide a way for client to go check for themselves, perhaps with a link to a transaction history page.
At first glance something seems wrong intuitively that we would ever need a recovery strategy here, but threads could be starved and we never would want to give wrong information to the client; sometimes you can never be sure if the transaction completed when a lock times out. By implementing all these strategies together, and using the prevention strategy to avoid un-necessarily throwing up the hands with the recovery strategy, we reduce the likelihood of the identified issues (Race conditions, double-transactions and mis-information) about a key transaction to a level that most businesses could tolerate. We should only pullout this bag of tricks for important transactions. For example, at Netflix we use modified versions of these strategies for gift purchases and starting memberships, both of which are commerce transactions in the purest sense.