spring-webflow
spring-webflow copied to clipboard
Prevent successive browser click events from resulting in duplicate transactions, while still displaying the transaction confirmation view. [SWF-361]
Keith Donald opened SWF-361 and commented
This ticket is all about how to prevent your credit card from being charged twice In the case where the browser sends two requests into the server that get processed at the same time --both saying "charge my card" Only one of request should win... preventing the duplicate charge is most important And ideally, the other request is just silently ignored and the user stills sees a nice confirmation of the first request.
Spring Web Flow has a locking infrastructure that serializes requests into a conversation from a user session. This makes the state machine itself thread safe: a client event must be processed in full before the next event can be processed.
From the user's perspective, this locking helps solves the duplicate submit problem. For example, consider an event from a view-state that causes the flow to commit a DB transaction, then end. Another thread spawned by the same view also trying to commit would be blocked while the first thread processed in full. After the first thread, the conversation would have ended and the second thread would receive a NoSuchFlowExecutionException. There would only be 1 commit of the transaction, as expected. An error message would be displayed to the user saying the flow had terminated.
However, this support could be improved.
Consider the case where a view-state event commits a DB transaction that then has the flow enter another view-state. In this case, the second thread would have to wait and what would happen next would depend on the flow execution repository in use. If a simple repository was in use, the duplicate event would result in a PermissionDeniedFlowExecutionAccessException, because the flow execution key being presented is now no longer valid. If a continuation repository was in used, the duplicate event would again resume the continuation for the previous view and signal the event against it, resulting in a duplicate commit of the transaction. This would be unacceptable.
What are the ways around this?
Possible ways:
- Prevent duplicate clicks in the browser in the first place by disabling the browser button after the initial click. This requires JS and may upset a user if for some reason the request was not processed by the server immediately.
- Invalidate the continuation of the view-state that performs the commit after the commit action is executed and the flow pauses on the next view. This will prevent the second thread from being able to resume the previous continuation, because it will no longer exist. An InvalidFlowExecutionContinuation would be thrown and the user would get an error. No duplicate transaction would be executed.
- Before rendering the confirmation view, detect that there is an additional thread (threads?) waiting to resume access to the flow execution. Intercept that thread and have it render the final view, resulting in a single commit transaction occuring and the confirmation being displayed. This seems nice as it is safe while also being transparent to the user--but it may be complex (if impossible) to implement this way.
Affects: 1.0.4
Issue Links:
-
#184 Support for maxConversations / maxContinuations on a flow-by-flow basis
-
#1017 Prevent back tracking once a certain step of a flow is completed.
-
#184 Support for maxConversations / maxContinuations on a flow-by-flow basis
2 votes, 5 watchers
Denis Pasek commented
Hi Keith,
Can you expand on this point?
- double click support for normal transition based on the conversation lock: The second request on the lock should result in exactly new state generated by the first request (continuations do not split the >conversation into two paths)
I'm thinking of something like your third alternative: after the first request has been executed and produced any kind of result (redirect, new view, error page etc.) the second waiting request should use this result (including the flow execution key for the new state) as its result and render this response to the client.
Normally I resolve this kind of problem with a servlet filter and a HttpServletResponseWrapper that buffers the response of the first incoming request. Synchronization of request threads happens by a session token. After completion of first request the result will be stored inside session. Using Post-Redirect-GET and under the assumption that the not repeatable request is normally a POST this should be just a redirect URL (with the flow execution key) and not kilobytes of HTML content. All waiting requests can use this redirect URL for its own redirect resulting in a stable new view state. To support even rendered HTML content a simple LRU cache could be used for the responses to store, so the data won't be replicated in a clustered environment.
I now this is quite complex especially supporting very different view technologies (JSF, plain JSP etc.).