-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Payment processing state does not block double submission #4499
Conversation
@@ -99,5 +99,21 @@ | |||
end | |||
end | |||
|
|||
describe "payment processing state" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shioyama I think this test should be within core/spec/models. It's not doing any request-y stuff, so why would it need to be in spec/requests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See below, the fact that it is happening as a result on an order state transition is exactly why it doesn't work as expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But yes, I can move this to core/spec/models, you're right it's not doing anything request-y. First I'd just like to confirm that this is indeed the problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, moved this to core/spec/models/order/checkout_spec.rb. Not really sure that's the right place, but there didn't seem to be anywhere really appropriate. The existing payment specs are not testing this scenario.
@radar I added a commit which fixes this problem by turning off transactions around In any case we need a solution and this seems important, so any advice would be much appreciated. |
👍 This makes a lot of sense to me. We should apply to the other branches too. Thanks a lot @shioyama |
@huoxito Great, good to hear that. I'd be happy to make PRs for other branches, if these changes look okay. Just one thing, I see that the default ActiveRecord integration in state machine runs transitions around validations. This PR would turn that off unless we explicitly add it back (like I did to get it to run transition callbacks around the action itself.) No tests are failing and this doesn't look like an issue, but I just wanted to check. |
yes @shioyama please submit another PR to master including a changelog entry :)
Sorry I'm not sure I understand that bit. Are you saying that your patch turn off validations when doing transitions? I think I misunderstood that, validations stills seem to happen with your patch applied. thanks for looking into state_machine internals to figure this @shioyama, appreciate it |
@huoxito great, will submit that asap.
No, the other way around: the patch turns off transitions around validations. By default ActiveRecord models with state machines insert into model actions ( # Runs state events around the object's validation process
def around_validation(object)
object.class.state_machines.transitions(object, action, :after => false).perform { yield }
end So if you save the model, it will automatically run any transitions as an
Took me a while to get my head around this... but I believe this is basically how it works. |
The last part of my comment above is not quite right. The action callbacks are fired around the action by default, and only as model callbacks (within the action) for So, by default a save action looks like this: # save (transaction)
# before transition callback
# save code
# after transition callback
# end By renaming the action and turning transactions off, this becomes: # before transition callback
# save (transaction)
# save code
# end
# after transition callback But now if we save the model, transitions are not fired, only the other way around (i.e. making a transition between states saves the model). This test was failing because it seems to (implicitly?) rely on this behavior, so I added it back. The result is this: # before transition callback
# save (transaction)
# before transition callback
# save code
# after transition callback
# end
# after transition callback The transitions get called outside of the transaction and inside the transaction. In the case of the failing test, something between the first (outer) and second (inner) before transition callback changes so that in the latter case, it actually transitions to ... that's probably more than you wanted to know, but I wanted to get this down for the record, in case anybody comes back to this. |
Thanks for the thorough explanation, great to have it documented here. Took a while into this now but I still don't understand why that spec is failing. Maybe the set up is just wrong. Everything else seems to work perfectly for me without the I think ideally we should only turn off transactions when processing payments since I don't remember anywhere else on the order state_machine that relies on persisted data. But I'm not sure how to do that, if it's possible right now. Anyway we still go with your patch just wanted to understand it better before merging. Maybe this also brings other benefits we can't see right now. Again appreciate all your investigation here @shioyama |
@huoxito My conclusion after going deep into a stack trace is that that spec is just screwed up. Something is happening with email validation -- it shouldn't even pass because the email is not set, so the model can't be saved, but somehow it previously was (being saved) regardless of validation failing. With the change this no longer happens, but that's a good thing I think. Can I just mark it as pending with a note that it is not actually testing what it claims to be testing? The same thing happens on master. |
And: I will take out that |
Don't pull this yet, going to clean it up first. |
This test is now failing but it is unclear if it was ever really testing what it claims to be testing. An order with no email is not valid and should not save, so the test should not pass.
Ok took out the callback and marked the failing test as pending, as I did in #4542. This should be good to go. |
This test is now failing but it is unclear if it was ever really testing what it claims to be testing. An order with no email is not valid and should not save, so the test should not pass. Fixes #4499
Merged. Thank you @shioyama :) |
Thanks! |
Just as a follow up, we turned transactions back on in our Spree site and it improved the stability A LOT. Per the documentation here https://github.com/pluginaweek/state_machine/blob/master/lib/state_machine/integrations/active_record.rb#L286 here is the callback list Callbacks occur in the following order. Callbacks specific to state_machine
|
@tesserakt could you elaborate on the stability improvement? Should we turn them back on by default? I'm pretty sure we've even since removed the regression spec added to this. |
@JDutil We only allow 1 shipment per location in our Spree Store. Without the lock, when the server got slow some customers would double submit on the address page. This created a race condition that sometimes allowed 2 identical shipments to be created for the same location, i.e. duplicates. Adding the transaction back in prevents this. |
@JDutil there were some other weird edge cases we used to see but no longer do after transactions For example: some orders coming in without completed_at fields. Some orders that have no shipments Still looking into some of these, but they have disappeared since adding transactions. Granted we are running a slightly modified version of 2.3, but making the change helped. |
@tesserakt I think we wanted to disable the transaction only for the payment processing which seems pretty useful to prevent double processing but I dont think it's possible not sure. We'd probably need to decouple the payment processing from state machine callbacks (this alone sounds like a nice change already) |
I've seen tons of errors related to the duplicate shipments, and it drove me nuts on 2.2+ stores. I thought we addressed it by adding the order lock version, which I believe would also have helped resolve double payment processing as well. I don't recall if we made that lock version change to 2.3 or if it wasn't until 2.4+ I think removing payments from state machine callback would be a good idea too, but not sure how best to accomplish that. |
@huoxito you could write a record outside of the transaction (using a separate thread) that says "Hey I'm processing payments" and then in the transaction check for that. |
Well we added it to 2-2-stable+ db52e1e |
@JDutil we still get double shipments even with that order lock code in 2.3 |
@JDutil @huoxito re-enabling transactions on the order requires a little finagling in the finalize method. If you get past processing the payments but then hit a snag, i.e. a random exception gets thrown somewhere in the stack; it is nice to have the finalize code catch the exception, auto void the payment, send the admin an email, then roll back the transaction and notify the user that something went wrong. |
This test is now failing but it is unclear if it was ever really testing what it claims to be testing. An order with no email is not valid and should not save, so the test should not pass. Fixes spree#4499
We've been experiencing issues where two requests complete an order in rapid succession resulting in two purchase requests being sent to the gateway. This results in a success followed by a failure (double submission) which overwrites the payment state to failed, causing problems down the line.
At first I thought this was something specific to do with the payment gateway or our spree customizations, but this PR confirms that the "processing" state which should prevent this is not working, because it is set within a transaction. AFAICT this would affect any store using Spree. Although here I'm just testing that
ActiveRecord::Base.connection.open_transactions
is zero, I've actually tested parallel requests and confirmed that the "processing" state is not being persisted to the db at any time.Can someone confirm that this is the case? If so I can try to push up a fix. If I'm misunderstanding something, then some clarification would be great (I'm fairly new to Spree).
Thanks!