Introduce explicit way of halting callback chains by throwing :abort. Deprecate current implicit behavior of halting callback chains by returning `false` in apps ported to Rails 5.0. Completely remove that behavior in brand new Rails 5.0 apps. #17227

Merged
merged 7 commits into from Jan 3, 2015

Conversation

Projects
None yet
@claudiob
Member

claudiob commented Oct 10, 2014

Update (2015-01-02): a gist with the suggested release notes to add to Rails 5.0 after this commit is available at https://gist.github.com/claudiob/614c59409fb7d11f2931


Stems from discussion with @dhh at https://groups.google.com/forum/#!topic/rubyonrails-core/mhD4T90g0G4

@dhh – I created this work-in-progress PR to continue the conversation with some code.
Could you tell me if this is what you intended by using :throw with a symbol?

I have quite clear how to add the throw to the code in ActiveJob, but not as
much where to catch it in ActiveSupport. For now I have this code:

def run_callbacks(kind, &block)
  catch(:abort_job)  do
    send "run_#{kind}_callbacks", &block
  end
end

but maybe the catch is better located somewhere at a deeper level of the callback stack.

@dhh Thoughts? Once I have a clearer idea on how to proceed, I can complete
this PR with tests, documentations, etc. Thanks!

@dhh

This comment has been minimized.

Show comment
Hide comment
@dhh

dhh Oct 16, 2014

Member

Looking pretty good. I'd actually like to bubble this all the way up to ActiveSupport, though. IMO, it should be a generic feature of the callback code to have "throw :abort" that'll cancel the entire chain. Thanks for working on this!

Member

dhh commented Oct 16, 2014

Looking pretty good. I'd actually like to bubble this all the way up to ActiveSupport, though. IMO, it should be a generic feature of the callback code to have "throw :abort" that'll cancel the entire chain. Thanks for working on this!

@claudiob claudiob changed the title from Throw :abort_job to exit a callback chain to Throw :halt_callbacks halts default CallbackChains Oct 16, 2014

@claudiob

This comment has been minimized.

Show comment
Hide comment
@claudiob

claudiob Oct 16, 2014

Member

@dhh I investigated a little deeper and found that I did not have to add a brand new method to halt the execution of a chain callback: there is already a :terminator option that is meant for that:

:terminator - Determines when a before filter will halt the callback chain, preventing following callbacks from being called and the event from being triggered. This should be a lambda to be executed. Defaults to false, meaning no value halts the chain.

So my suggestion is: let's change the default value of :terminator, from false (never halt the chain) to a lambda that halts the chain if the callback includes throw(:halt_callbacks).

The advantage of this method is that it is backward-compatible for the modules that explicitly define a :terminator. For instance, ActiveModel currently define callbacks with the following terminator:

terminator: ->(_,result) { result == false },

meaning that any before_ callback returning false halts the chain.

This PR does not change that behavior, but prepares the way for the change you suggested on the Google Group. When the day comes, we can simply remove the terminator above, and have ActiveModel use the default terminator introduced by this PR, which ignores false values and only halts the chain when :halt_callbacks is thrown. (update: this PR now deprecates that behavior)

As you read the list of files changed by this PR, keep in mind that activesupport/lib/active_support/callbacks.rb is the key one; however, I also had the change the files where terminators are defined, since they now have to accept a Proc, rather than a value, as the second parameter.

In this way, I am able catch(:halt_callbacks) only in one place of the code, by invoking the passed Proc. Otherwise, I would have to catch the message in every single define_callback.

Please tell me what you think, and also if you think :halt_callbacks is a good name for the message.
Thanks! 🍭

Member

claudiob commented Oct 16, 2014

@dhh I investigated a little deeper and found that I did not have to add a brand new method to halt the execution of a chain callback: there is already a :terminator option that is meant for that:

:terminator - Determines when a before filter will halt the callback chain, preventing following callbacks from being called and the event from being triggered. This should be a lambda to be executed. Defaults to false, meaning no value halts the chain.

So my suggestion is: let's change the default value of :terminator, from false (never halt the chain) to a lambda that halts the chain if the callback includes throw(:halt_callbacks).

The advantage of this method is that it is backward-compatible for the modules that explicitly define a :terminator. For instance, ActiveModel currently define callbacks with the following terminator:

terminator: ->(_,result) { result == false },

meaning that any before_ callback returning false halts the chain.

This PR does not change that behavior, but prepares the way for the change you suggested on the Google Group. When the day comes, we can simply remove the terminator above, and have ActiveModel use the default terminator introduced by this PR, which ignores false values and only halts the chain when :halt_callbacks is thrown. (update: this PR now deprecates that behavior)

As you read the list of files changed by this PR, keep in mind that activesupport/lib/active_support/callbacks.rb is the key one; however, I also had the change the files where terminators are defined, since they now have to accept a Proc, rather than a value, as the second parameter.

In this way, I am able catch(:halt_callbacks) only in one place of the code, by invoking the passed Proc. Otherwise, I would have to catch the message in every single define_callback.

Please tell me what you think, and also if you think :halt_callbacks is a good name for the message.
Thanks! 🍭

@dhh

This comment has been minimized.

Show comment
Hide comment
@dhh

dhh Oct 16, 2014

Member

This seems better, but I don't like :halt_callbacks, because we're not just halting the callbacks, we're halting the whole chain which includes both the callbacks AND the actual action we're wrapping. The shorter throw :abort just strikes me as more pleasing too. But otherwise looks good to me. //cc @rafaelfranca @tenderlove

Member

dhh commented Oct 16, 2014

This seems better, but I don't like :halt_callbacks, because we're not just halting the callbacks, we're halting the whole chain which includes both the callbacks AND the actual action we're wrapping. The shorter throw :abort just strikes me as more pleasing too. But otherwise looks good to me. //cc @rafaelfranca @tenderlove

@claudiob

This comment has been minimized.

Show comment
Hide comment
@claudiob

claudiob Oct 17, 2014

Member

@dhh I'm fine with any name.

I have an additional note.
Until now, only before_ callbacks are able to halt the chain; the return value of after_ callbacks is ignored. If after_ callbacks are executed, then they are all executed.

With this PR, the behavior does not change. Therefore, if an after_ callback decided to throw(:abort), this would not be caught by terminator: it would simply bubble up to the user.

I think this okay, since it matches what the documentation says and existing code.
I just wanted to confirm.

Member

claudiob commented Oct 17, 2014

@dhh I'm fine with any name.

I have an additional note.
Until now, only before_ callbacks are able to halt the chain; the return value of after_ callbacks is ignored. If after_ callbacks are executed, then they are all executed.

With this PR, the behavior does not change. Therefore, if an after_ callback decided to throw(:abort), this would not be caught by terminator: it would simply bubble up to the user.

I think this okay, since it matches what the documentation says and existing code.
I just wanted to confirm.

@dhh

This comment has been minimized.

Show comment
Hide comment
@dhh

dhh Oct 17, 2014

Member

Ultimately, I think you should be able to halt the after_* chain as well. But we don’t need to deal with it in the same PR.

On Oct 16, 2014, at 17:02, Claudio B. notifications@github.com wrote:

@dhh I'm fine with any name.

I have an additional note.
Until now, only before_ callbacks are able to halt the chain; the return value of after_ callbacks is ignored. If after_ callbacks are executed, then they are all executed.

With this PR, the behavior does not change. Therefore, if an after_ callback decided to throw(:abort), then it wouldn't be cached by terminator, and it would simply bubble up to the user.

I think this okay, since it matches what the documentation says and existing code.
I just wanted to confirm.


Reply to this email directly or view it on GitHub.

Member

dhh commented Oct 17, 2014

Ultimately, I think you should be able to halt the after_* chain as well. But we don’t need to deal with it in the same PR.

On Oct 16, 2014, at 17:02, Claudio B. notifications@github.com wrote:

@dhh I'm fine with any name.

I have an additional note.
Until now, only before_ callbacks are able to halt the chain; the return value of after_ callbacks is ignored. If after_ callbacks are executed, then they are all executed.

With this PR, the behavior does not change. Therefore, if an after_ callback decided to throw(:abort), then it wouldn't be cached by terminator, and it would simply bubble up to the user.

I think this okay, since it matches what the documentation says and existing code.
I just wanted to confirm.


Reply to this email directly or view it on GitHub.

@tenderlove

This comment has been minimized.

Show comment
Hide comment
@tenderlove

tenderlove Oct 17, 2014

Member

Any reason to use throw / catch vs an exception? We should probably test the performance impact of this change as well. Callbacks are a huge hotspot (since we use them literally everywhere).

Member

tenderlove commented Oct 17, 2014

Any reason to use throw / catch vs an exception? We should probably test the performance impact of this change as well. Callbacks are a huge hotspot (since we use them literally everywhere).

@dhh

This comment has been minimized.

Show comment
Hide comment
@dhh

dhh Oct 17, 2014

Member

Exceptions should only be when something is going wrong, really. We need a way to control the flow when aborting the callbacks is the expected path.

On Oct 16, 2014, at 18:55, Aaron Patterson notifications@github.com wrote:

Any reason to use throw / catch vs an exception? We should probably test the performance impact of this change as well. Callbacks are a huge hotspot (since we use them literally everywhere).


Reply to this email directly or view it on GitHub.

Member

dhh commented Oct 17, 2014

Exceptions should only be when something is going wrong, really. We need a way to control the flow when aborting the callbacks is the expected path.

On Oct 16, 2014, at 18:55, Aaron Patterson notifications@github.com wrote:

Any reason to use throw / catch vs an exception? We should probably test the performance impact of this change as well. Callbacks are a huge hotspot (since we use them literally everywhere).


Reply to this email directly or view it on GitHub.

@claudiob claudiob changed the title from Throw :halt_callbacks halts default CallbackChains to Throw :abort halts default CallbackChains Oct 17, 2014

@egilburg

This comment has been minimized.

Show comment
Hide comment
@egilburg

egilburg Oct 17, 2014

Contributor

Perhaps it's wrong assumption to make but I'd assume seeing abort in a save callback would cancel/reverse the save, even if it's an after_save callback (thinking about the "abort transaction" concept). For this reason :halt seemed better to me.

Perhaps break or return, similar to Ruby's syntax for loops and method respectively? Or a more explicit halt_callbacks or stop_callbacks, with plural "callbacks" implying all further ones will be stopped and not just current one?

Contributor

egilburg commented Oct 17, 2014

Perhaps it's wrong assumption to make but I'd assume seeing abort in a save callback would cancel/reverse the save, even if it's an after_save callback (thinking about the "abort transaction" concept). For this reason :halt seemed better to me.

Perhaps break or return, similar to Ruby's syntax for loops and method respectively? Or a more explicit halt_callbacks or stop_callbacks, with plural "callbacks" implying all further ones will be stopped and not just current one?

@dhh

This comment has been minimized.

Show comment
Hide comment
@dhh

dhh Oct 18, 2014

Member

Halting execution during the before callbacks is the primary use case, and stopping not just the callbacks, but the action itself, from being executed is the primary within that. So given that setup, I still prefer something that doesn't have *_callbacks. "throw halt" sounds a little weird to me. "throw abort" seems nicer.

"throw abort" will cancel/reverse the save if its still under the same transaction, so that seems accurate too.

Member

dhh commented Oct 18, 2014

Halting execution during the before callbacks is the primary use case, and stopping not just the callbacks, but the action itself, from being executed is the primary within that. So given that setup, I still prefer something that doesn't have *_callbacks. "throw halt" sounds a little weird to me. "throw abort" seems nicer.

"throw abort" will cancel/reverse the save if its still under the same transaction, so that seems accurate too.

@claudiob

This comment has been minimized.

Show comment
Hide comment
@claudiob

claudiob Dec 1, 2014

Member

Hello @dhh @pixeltrix @sferik @wjessop @tomstuart

Now that Rails 4.2.rc1 is out, it's probably a good time to continue the discussion on this ticket, which also matches what you guys discussed on Twitter.

The recap of this PR is: change the behavior of callbacks so that returning false in a before_ callback does not halt the chain anymore. The callback chain can only be halted explicitly by calling throw :abort.

More details in the comments above! 🍭

Member

claudiob commented Dec 1, 2014

Hello @dhh @pixeltrix @sferik @wjessop @tomstuart

Now that Rails 4.2.rc1 is out, it's probably a good time to continue the discussion on this ticket, which also matches what you guys discussed on Twitter.

The recap of this PR is: change the behavior of callbacks so that returning false in a before_ callback does not halt the chain anymore. The callback chain can only be halted explicitly by calling throw :abort.

More details in the comments above! 🍭

@dhh

This comment has been minimized.

Show comment
Hide comment
@dhh

dhh Dec 5, 2014

Member

👍

Member

dhh commented Dec 5, 2014

👍

@claudiob

This comment has been minimized.

Show comment
Hide comment
@claudiob

claudiob Dec 5, 2014

Member

@dhh Rebased and conflicts solved!

Member

claudiob commented Dec 5, 2014

@dhh Rebased and conflicts solved!

@dhh

This comment has been minimized.

Show comment
Hide comment
@dhh

dhh Dec 5, 2014

Member

Looks good to me, but I'll let @rafaelfranca or someone else on the team have a look as well.

Member

dhh commented Dec 5, 2014

Looks good to me, but I'll let @rafaelfranca or someone else on the team have a look as well.

@matthewd

This comment has been minimized.

Show comment
Hide comment
@matthewd

matthewd Dec 7, 2014

Member

I've only skimmed, but it sounds like we're missing a deprecation here at the moment?

I would expect that a falsey result would emit a deprecation warning, but still halt the chain, for a version.

Member

matthewd commented Dec 7, 2014

I've only skimmed, but it sounds like we're missing a deprecation here at the moment?

I would expect that a falsey result would emit a deprecation warning, but still halt the chain, for a version.

@dhh

This comment has been minimized.

Show comment
Hide comment
@dhh

dhh Dec 7, 2014

Member

Let's get that into 4.2.

On Dec 7, 2014, at 4:56 PM, Matthew Draper notifications@github.com wrote:

I've only skimmed, but it sounds like we're missing a deprecation here at the moment?

I would expect that a falsey result would emit a deprecation warning, but still halt the chain, for a version.


Reply to this email directly or view it on GitHub.

Member

dhh commented Dec 7, 2014

Let's get that into 4.2.

On Dec 7, 2014, at 4:56 PM, Matthew Draper notifications@github.com wrote:

I've only skimmed, but it sounds like we're missing a deprecation here at the moment?

I would expect that a falsey result would emit a deprecation warning, but still halt the chain, for a version.


Reply to this email directly or view it on GitHub.

@matthewd

This comment has been minimized.

Show comment
Hide comment
@matthewd

matthewd Dec 7, 2014

Member

@dhh the only way to get the deprecation into 4.2 would be to ship this whole feature change in 4.2. And it's way too late for that -- especially considering @tenderlove's very reasonable performance concerns.

With a deprecation warning, we can ship this as the right way to do things in 5.0... it's just that the old way will still work (noisily) until 5.1.

Member

matthewd commented Dec 7, 2014

@dhh the only way to get the deprecation into 4.2 would be to ship this whole feature change in 4.2. And it's way too late for that -- especially considering @tenderlove's very reasonable performance concerns.

With a deprecation warning, we can ship this as the right way to do things in 5.0... it's just that the old way will still work (noisily) until 5.1.

@dhh

This comment has been minimized.

Show comment
Hide comment
@dhh

dhh Dec 7, 2014

Member

Fine be me too.

On Dec 7, 2014, at 5:39 PM, Matthew Draper notifications@github.com wrote:

@dhh the only way to get the deprecation into 4.2 would be to ship this whole feature change in 4.2. And it's way too late for that -- especially considering @tenderlove's very reasonable performance concerns.

With a deprecation warning, we can ship this as the right way to do things in 5.0... it's just that the old way will still work (noisily) until 5.1.


Reply to this email directly or view it on GitHub.

Member

dhh commented Dec 7, 2014

Fine be me too.

On Dec 7, 2014, at 5:39 PM, Matthew Draper notifications@github.com wrote:

@dhh the only way to get the deprecation into 4.2 would be to ship this whole feature change in 4.2. And it's way too late for that -- especially considering @tenderlove's very reasonable performance concerns.

With a deprecation warning, we can ship this as the right way to do things in 5.0... it's just that the old way will still work (noisily) until 5.1.


Reply to this email directly or view it on GitHub.

@claudiob

This comment has been minimized.

Show comment
Hide comment
@claudiob

claudiob Dec 7, 2014

Member

@matthewd @dhh I'm not 100% sure this PR needs a deprecation policy.

Let me explain: the behavior of callback chains that define a terminator does not change after this PR.
For instance, ActiveModel's callback chains will still halt if any before_ callback returns false.

This PR adds a new behavior for callback chains that do not define a terminator.

Before this PR, if your callback chain did not define a terminator, then the callback chain could never halted.

After this PR, if your callback chain does not define a terminator, then you have the possibility to halt the callback chain by throwing :abort in a before_ callback.

I think a deprecation policy will only be needed once we change existing behaviors: for instance, if we decide that returning false in an ActiveModel before_ callback won't halt the chain anymore, or if we decide that callback chains can also be halted by an after_ callback. None of that is part of this PR.

Let me know what you think!

Member

claudiob commented Dec 7, 2014

@matthewd @dhh I'm not 100% sure this PR needs a deprecation policy.

Let me explain: the behavior of callback chains that define a terminator does not change after this PR.
For instance, ActiveModel's callback chains will still halt if any before_ callback returns false.

This PR adds a new behavior for callback chains that do not define a terminator.

Before this PR, if your callback chain did not define a terminator, then the callback chain could never halted.

After this PR, if your callback chain does not define a terminator, then you have the possibility to halt the callback chain by throwing :abort in a before_ callback.

I think a deprecation policy will only be needed once we change existing behaviors: for instance, if we decide that returning false in an ActiveModel before_ callback won't halt the chain anymore, or if we decide that callback chains can also be halted by an after_ callback. None of that is part of this PR.

Let me know what you think!

@dhh

This comment has been minimized.

Show comment
Hide comment
@dhh

dhh Dec 8, 2014

Member

Let's expand this PR to include the deprecation. The main benefit of this switch is not using throw :abort when you know you want to halt, but to avoid halting when the callback accidentally returns false.

On Dec 7, 2014, at 22:31, Claudio B. notifications@github.com wrote:

@matthewd @dhh I'm not 100% sure this PR needs a deprecation policy.

Let me explain: the behavior of callback chains that define a terminator does not change after this PR.
For instance, ActiveModel's callback chains will still halt if any before_ callback returns false.

This PR adds a new behavior for callback chains that do not define a terminator.

Before this PR, if your callback chain did not define a terminator, then the callback chain could never halted.

After this PR, if your callback chain does not define a terminator, then you have the possibility to halt the callback chain by throwing :abort in a before_ callback.

I think a deprecation policy will only be needed once we change existing behaviors: for instance, if we decide that returning false in an ActiveModel before_ callback won't halt the chain anymore, or if we decide that callback chains can also be halted by an after_ callback. None of that is part of this PR.

Let me know what you think!


Reply to this email directly or view it on GitHub.

Member

dhh commented Dec 8, 2014

Let's expand this PR to include the deprecation. The main benefit of this switch is not using throw :abort when you know you want to halt, but to avoid halting when the callback accidentally returns false.

On Dec 7, 2014, at 22:31, Claudio B. notifications@github.com wrote:

@matthewd @dhh I'm not 100% sure this PR needs a deprecation policy.

Let me explain: the behavior of callback chains that define a terminator does not change after this PR.
For instance, ActiveModel's callback chains will still halt if any before_ callback returns false.

This PR adds a new behavior for callback chains that do not define a terminator.

Before this PR, if your callback chain did not define a terminator, then the callback chain could never halted.

After this PR, if your callback chain does not define a terminator, then you have the possibility to halt the callback chain by throwing :abort in a before_ callback.

I think a deprecation policy will only be needed once we change existing behaviors: for instance, if we decide that returning false in an ActiveModel before_ callback won't halt the chain anymore, or if we decide that callback chains can also be halted by an after_ callback. None of that is part of this PR.

Let me know what you think!


Reply to this email directly or view it on GitHub.

@claudiob

This comment has been minimized.

Show comment
Hide comment
@claudiob

claudiob Dec 8, 2014

Member

@dhh I have followed your suggestion and included the deprecation in the three places where callback chains can be halted by returning false:

  1. ActiveModel Validation callbacks
  2. ActiveModel callbacks
  3. ActiveRecord callbacks

As a result of this PR, returning false in a before_ callback will still work as before, but will display the following message:

DEPRECATION WARNING: Returning false in a before_ callback will not implicitly halt a callback chain in the next release of Rails. To explicitly halt a callback chain, please use throw :abort instead.

Please tell me if you'd like to rephrase the message. The message should make clear that returning false in a callback is not wrong but in future version of Rails will not have the implicit side effect it has now.

Member

claudiob commented Dec 8, 2014

@dhh I have followed your suggestion and included the deprecation in the three places where callback chains can be halted by returning false:

  1. ActiveModel Validation callbacks
  2. ActiveModel callbacks
  3. ActiveRecord callbacks

As a result of this PR, returning false in a before_ callback will still work as before, but will display the following message:

DEPRECATION WARNING: Returning false in a before_ callback will not implicitly halt a callback chain in the next release of Rails. To explicitly halt a callback chain, please use throw :abort instead.

Please tell me if you'd like to rephrase the message. The message should make clear that returning false in a callback is not wrong but in future version of Rails will not have the implicit side effect it has now.

@dhh

This comment has been minimized.

Show comment
Hide comment
@dhh

dhh Dec 8, 2014

Member

Looks good to me 👍

On Dec 8, 2014, at 4:23 PM, Claudio B. notifications@github.com wrote:

@dhh I have followed your suggestion and included the deprecation in the three places where callback chains can be halted by returning false:

• ActiveModel Validation callbacks
• ActiveModel callbacks
• ActiveRecord callbacks
As a result of this PR, returning false in a before_ callback will still work as before, but will display the following message:

DEPRECATION WARNING: Returning false in a before_ callback will not implicitly halt a callback chain in the next release of Rails. To explicitly halt a callback chain, please use throw :abort instead.

Please tell me if you'd like to rephrase the message. The message should make clear that returning false in a callback is not wrong but in future version of Rails will not have the implicit side effect it has now.


Reply to this email directly or view it on GitHub.

Member

dhh commented Dec 8, 2014

Looks good to me 👍

On Dec 8, 2014, at 4:23 PM, Claudio B. notifications@github.com wrote:

@dhh I have followed your suggestion and included the deprecation in the three places where callback chains can be halted by returning false:

• ActiveModel Validation callbacks
• ActiveModel callbacks
• ActiveRecord callbacks
As a result of this PR, returning false in a before_ callback will still work as before, but will display the following message:

DEPRECATION WARNING: Returning false in a before_ callback will not implicitly halt a callback chain in the next release of Rails. To explicitly halt a callback chain, please use throw :abort instead.

Please tell me if you'd like to rephrase the message. The message should make clear that returning false in a callback is not wrong but in future version of Rails will not have the implicit side effect it has now.


Reply to this email directly or view it on GitHub.

+
+ def display_deprecation_warning_for_false_terminator
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Returning `false` in a `before_` callback will not implicitly halt a callback chain in the next release of Rails.

This comment has been minimized.

@rafaelfranca

rafaelfranca Dec 10, 2014

Member

I believe it is not just in before_ callbacks right?

@rafaelfranca

rafaelfranca Dec 10, 2014

Member

I believe it is not just in before_ callbacks right?

This comment has been minimized.

@pixeltrix

pixeltrix Dec 11, 2014

Member

@rafaelfranca that's right - after and around both support :terminator options.

@pixeltrix

pixeltrix Dec 11, 2014

Member

@rafaelfranca that's right - after and around both support :terminator options.

This comment has been minimized.

@claudiob

claudiob Dec 11, 2014

Member

@rafaelfranca @pixeltrix I'll look this up too while I fix the failing tests! Thanks 😸

@claudiob

claudiob Dec 11, 2014

Member

@rafaelfranca @pixeltrix I'll look this up too while I fix the failing tests! Thanks 😸

This comment has been minimized.

@claudiob

claudiob Dec 15, 2014

Member

@rafaelfranca @pixeltrix I was having some confusing regarding around_ and after_ callbacks until I realized that the existing documentation was not 100% clear.

I created #18031 which should makes things clearer on the current master!

@claudiob

claudiob Dec 15, 2014

Member

@rafaelfranca @pixeltrix I was having some confusing regarding around_ and after_ callbacks until I realized that the existing documentation was not 100% clear.

I created #18031 which should makes things clearer on the current master!

This comment has been minimized.

@claudiob

claudiob Dec 15, 2014

Member

Also, I think I found an error in the documentation of ActiveRecord's callbacks… check #18033 🍭

@claudiob

claudiob Dec 15, 2014

Member

Also, I think I found an error in the documentation of ActiveRecord's callbacks… check #18033 🍭

claudiob added a commit to claudiob/rails that referenced this pull request Dec 15, 2014

Add test for after_validation returning false
This stems from rails#17227 (comment)

It's simply a clarification of the current behavior by which if an
`after_validation` callback returns +false+, then further `after_`
callbacks **are not halted**.

claudiob added a commit to claudiob/rails that referenced this pull request Dec 15, 2014

Add test for after/around callback returning false
This stems from rails#17227 (comment)

It's simply a clarification of the current behavior by which if an
`after_` or `around_` ActiveModel callback returns +false+, then the callback
chain **is not halted**.

The callback chain in ActiveModel is only halted when a `before_`
callback returns `false`.

claudiob added a commit to claudiob/rails that referenced this pull request Dec 15, 2014

Add AM test for after_validation returning false
This stems from rails#17227 (comment)

It's simply a clarification of the current behavior by which if an
`after_validation` ActiveModel callback returns +false+, then further
`after_` callbacks **are not halted**.

claudiob added a commit to claudiob/rails that referenced this pull request Dec 15, 2014

Add AM test: after/around callback returning false
This stems from rails#17227 (comment)

It's simply a clarification of the current behavior by which if an
`after_` or `around_` ActiveModel callback returns +false+, then the callback
chain **is not halted**.

The callback chain in ActiveModel is only halted when a `before_`
callback returns `false`.

claudiob added a commit to claudiob/rails that referenced this pull request Dec 15, 2014

Add AM test for after_validation returning false
This stems from rails#17227 (comment)

It's simply a clarification of the current behavior by which if an
`after_validation` ActiveModel callback returns +false+, then further
`after_` callbacks **are not halted**.

claudiob added a commit to claudiob/rails that referenced this pull request Dec 15, 2014

Add AM test: after/around callback returning false
This stems from rails#17227 (comment)

It's simply a clarification of the current behavior by which if an
`after_` or `around_` ActiveModel callback returns +false+, then the callback
chain **is not halted**.

The callback chain in ActiveModel is only halted when a `before_`
callback returns `false`.

@claudiob claudiob changed the title from Throw :abort halts default CallbackChains to Introduce explicit way of halting callback chains by throwing :abort. Deprecate current implicit behavior of halting callback chains by returning `false`. Dec 15, 2014

@schneems

This comment has been minimized.

Show comment
Hide comment
@schneems

schneems Dec 15, 2014

Member

Love the idea, thanks for your work!

Member

schneems commented Dec 15, 2014

Love the idea, thanks for your work!

@claudiob

This comment has been minimized.

Show comment
Hide comment
@claudiob

claudiob Dec 20, 2014

Member

This PR might look daunting at first but if you read the code changes commit by commit it becomes much more understandable:

  1. first I changed AS so that, by default, callback chains can be halted by throwing :abort
  2. then I removed the current terminator from AM validation callbacks. These callbacks used to be halted by returning false; from now on will instead use the new default terminator which requires to throw :abort
  3. then I did the same for the remaining AM callbacks
  4. then I did the same for the existing AS callbacks
  5. then I did the same for the existing AR callbacks

In steps 2–5 I also added deprecation warnings. Therefore, in Rails 5.0, returning false in a callback will display a warning but will still halt AM/AS/AR callbacks. This will make the change easier for developers.

Member

claudiob commented Dec 20, 2014

This PR might look daunting at first but if you read the code changes commit by commit it becomes much more understandable:

  1. first I changed AS so that, by default, callback chains can be halted by throwing :abort
  2. then I removed the current terminator from AM validation callbacks. These callbacks used to be halted by returning false; from now on will instead use the new default terminator which requires to throw :abort
  3. then I did the same for the remaining AM callbacks
  4. then I did the same for the existing AS callbacks
  5. then I did the same for the existing AR callbacks

In steps 2–5 I also added deprecation warnings. Therefore, in Rails 5.0, returning false in a callback will display a warning but will still halt AM/AS/AR callbacks. This will make the change easier for developers.

@dhh

This comment has been minimized.

Show comment
Hide comment
@dhh

dhh Dec 20, 2014

Member

I think we need to combine this with a new migration config that turns off the 'false halts the chain and warns' behavior both for new apps and for apps that have migrated. Because without it, you don't actually get much benefit out of this. Yes, if you're explicitly wanting to halt, you can now throw halt, but the much bigger issue was that accidentally having the last statement returns false would halt -- and this doesn't solve that.

So if we have something like config.active_support.callbacks.halt_on_false = false or the like, then we can generate new 5.0 apps with that, and others can jump on it when they migrate. And everyone else doesn't see their app break.

Member

dhh commented Dec 20, 2014

I think we need to combine this with a new migration config that turns off the 'false halts the chain and warns' behavior both for new apps and for apps that have migrated. Because without it, you don't actually get much benefit out of this. Yes, if you're explicitly wanting to halt, you can now throw halt, but the much bigger issue was that accidentally having the last statement returns false would halt -- and this doesn't solve that.

So if we have something like config.active_support.callbacks.halt_on_false = false or the like, then we can generate new 5.0 apps with that, and others can jump on it when they migrate. And everyone else doesn't see their app break.

@matthewd

This comment has been minimized.

Show comment
Hide comment
@matthewd

matthewd Dec 20, 2014

Member

@dhh well, the benefit would come in 5.1, when we switch to ignoring false. And you immediately gain a little: any time you accidentally return false, you get a noisy deprecation warning, instead of a silent abort.

I worry about fast-forwarding the change via a config because it would cause issues/confusion if you're using (or worse, start to use) a gem that defines a callback.

Member

matthewd commented Dec 20, 2014

@dhh well, the benefit would come in 5.1, when we switch to ignoring false. And you immediately gain a little: any time you accidentally return false, you get a noisy deprecation warning, instead of a silent abort.

I worry about fast-forwarding the change via a config because it would cause issues/confusion if you're using (or worse, start to use) a gem that defines a callback.

@dhh

This comment has been minimized.

Show comment
Hide comment
@dhh

dhh Dec 20, 2014

Member

5.1 is too far away for this. The gem issue would be no different than from whatever would happen when this is the new default.

On Dec 20, 2014, at 11:14 AM, Matthew Draper notifications@github.com wrote:

@dhh https://github.com/dhh well, the benefit would come in 5.1, when we switch to ignoring false. And you immediately gain a little: any time you accidentally return false, you get a noisy deprecation warning, instead of a silent abort.

I worry about fast-forwarding the change via a config because it would cause issues/confusion if you're using (or worse, start to use) a gem that defines a callback.


Reply to this email directly or view it on GitHub #17227 (comment).

Member

dhh commented Dec 20, 2014

5.1 is too far away for this. The gem issue would be no different than from whatever would happen when this is the new default.

On Dec 20, 2014, at 11:14 AM, Matthew Draper notifications@github.com wrote:

@dhh https://github.com/dhh well, the benefit would come in 5.1, when we switch to ignoring false. And you immediately gain a little: any time you accidentally return false, you get a noisy deprecation warning, instead of a silent abort.

I worry about fast-forwarding the change via a config because it would cause issues/confusion if you're using (or worse, start to use) a gem that defines a callback.


Reply to this email directly or view it on GitHub #17227 (comment).

@sgrif

This comment has been minimized.

Show comment
Hide comment
@sgrif

sgrif Dec 20, 2014

Member

Is that implying we would change the default without a deprecation cycle?

Member

sgrif commented Dec 20, 2014

Is that implying we would change the default without a deprecation cycle?

claudiob added some commits Dec 15, 2014

Deprecate `false` as the way to halt AS callbacks
After this commit, returning `false` in a callback will display a deprecation
warning to make developers aware of the fact that they need to explicitly
`throw(:abort)` if their intention is to halt a callback chain.

This commit also patches two internal uses of AS::Callbacks (inside
ActiveRecord and ActionDispatch) which sometimes return `false` but whose
returned value is not meaningful for the purpose of execution.

In both cases, the returned value is set to `true`, which does not affect the
execution of the callbacks but prevents unrequested deprecation warnings from
showing up.
Deprecate `false` as the way to halt AM validation callbacks
Before this commit, returning `false` in an ActiveModel validation
callback such as `before_validation` would halt the callback chain.

After this commit, the behavior is deprecated: will still work until
the next release of Rails but will also display a deprecation warning.

The preferred way to halt a callback chain is to explicitly `throw(:abort)`.
Deprecate `false` as the way to halt AM callbacks
Before this commit, returning `false` in an ActiveModel `before_` callback
such as `before_create` would halt the callback chain.

After this commit, the behavior is deprecated: will still work until
the next release of Rails but will also display a deprecation warning.

The preferred way to halt a callback chain is to explicitly `throw(:abort)`.
Deprecate `false` as the way to halt AR callbacks
Before this commit, returning `false` in an ActiveRecord `before_` callback
such as `before_create` would halt the callback chain.

After this commit, the behavior is deprecated: will still work until
the next release of Rails but will also display a deprecation warning.

The preferred way to halt a callback chain is to explicitly `throw(:abort)`.
Add config to halt callback chain on return false
This stems from [a comment](rails#17227 (comment)) by @dhh.
In summary:

* New Rails 5.0 apps will not accept `return false` as a way to halt callback chains, and will not display a deprecation warning.
* Existing apps ported to Rails 5.0 will still accept `return false` as a way to halt callback chains, albeit with a deprecation warning.

For this purpose, this commit introduces a Rails configuration option:

```ruby
config.active_support.halt_callback_chains_on_return_false
```

For new Rails 5.0 apps, this option will be set to `false` by a new initializer
`config/initializers/callback_terminator.rb`:

```ruby
Rails.application.config.active_support.halt_callback_chains_on_return_false = false
```

For existing apps ported to Rails 5.0, the initializers above will not exist.
Even running `rake rails:update` will not create this initializer.

Since the default value of `halt_callback_chains_on_return_false` is set to
`true`, these apps will still accept `return true` as a way to halt callback
chains, displaying a deprecation warning.

Developers will be able to switch to the new behavior (and stop the warning)
by manually adding the line above to their `config/application.rb`.

A gist with the suggested release notes to add to Rails 5.0 after this
commit is available at https://gist.github.com/claudiob/614c59409fb7d11f2931

@rafaelfranca rafaelfranca merged commit 9c65c53 into rails:master Jan 3, 2015

1 check passed

continuous-integration/travis-ci The Travis CI build passed
Details

rafaelfranca added a commit that referenced this pull request Jan 3, 2015

Merge pull request #17227 from claudiob/explicitly-abort-callbacks
Introduce explicit way of halting callback chains by throwing :abort. Deprecate current implicit behavior of halting callback chains by returning `false` in apps ported to Rails 5.0. Completely remove that behavior in brand new Rails 5.0 apps.

Conflicts:
	railties/CHANGELOG.md

@claudiob claudiob deleted the claudiob:explicitly-abort-callbacks branch Jan 4, 2015

@claudiob

This comment has been minimized.

Show comment
Hide comment
@claudiob

claudiob Jan 4, 2015

Member

😱 :bowtie: ✌️ 👏

And now… on with the next step. @dhh said:

Ultimately, I think you should be able to halt the after_* chain as well.
But we don’t need to deal with it in the same PR.

This also related to #8479. What's the best plan to move forward with this? Is this something still wanted?

Member

claudiob commented Jan 4, 2015

😱 :bowtie: ✌️ 👏

And now… on with the next step. @dhh said:

Ultimately, I think you should be able to halt the after_* chain as well.
But we don’t need to deal with it in the same PR.

This also related to #8479. What's the best plan to move forward with this? Is this something still wanted?

@dhh

This comment has been minimized.

Show comment
Hide comment
@dhh

dhh Jan 4, 2015

Member

Bad ass, Claudio. You ran the gauntlet! :)

I still think halting the callback chain on after_ makes plenty of sense to me. Let’s just start a new PR for that.

On Jan 3, 2015, at 4:28 PM, Claudio B. notifications@github.com wrote:

And now… on with the next step. @dhh https://github.com/dhh said:

Ultimately, I think you should be able to halt the after_* chain as well.
But we don’t need to deal with it in the same PR.

This also related to #8479 #8479. What's the best plan to move forward with this? Is this something still wanted?


Reply to this email directly or view it on GitHub #17227 (comment).

Member

dhh commented Jan 4, 2015

Bad ass, Claudio. You ran the gauntlet! :)

I still think halting the callback chain on after_ makes plenty of sense to me. Let’s just start a new PR for that.

On Jan 3, 2015, at 4:28 PM, Claudio B. notifications@github.com wrote:

And now… on with the next step. @dhh https://github.com/dhh said:

Ultimately, I think you should be able to halt the after_* chain as well.
But we don’t need to deal with it in the same PR.

This also related to #8479 #8479. What's the best plan to move forward with this? Is this something still wanted?


Reply to this email directly or view it on GitHub #17227 (comment).

@@ -13,6 +13,13 @@ class Railtie < Rails::Railtie # :nodoc:
end
end
+ initializer "active_support.halt_callback_chains_on_return_false", after: :load_config_initializers do |app|

This comment has been minimized.

@rafaelfranca

rafaelfranca Apr 6, 2015

Member

@claudiob do you remember why we are running this after load_config_initializers? This is making load_config_initializers run way early in the initializers order.

@rafaelfranca

rafaelfranca Apr 6, 2015

Member

@claudiob do you remember why we are running this after load_config_initializers? This is making load_config_initializers run way early in the initializers order.

This comment has been minimized.

@claudiob

claudiob Apr 7, 2015

Member

@rafaelfranca Brand new Rails 5 come with an initializer file called config/initializers/callback_terminator.rb which sets the value for app.config.active_support.halt_callback_chains_on_return_false.

In this code, we check if that value has been set and eventually assign it to ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false.

Therefore, it made sense to me to run this method after the config/initializers/callback_terminator.rb has been called.

What kind of issues is this causing?

@claudiob

claudiob Apr 7, 2015

Member

@rafaelfranca Brand new Rails 5 come with an initializer file called config/initializers/callback_terminator.rb which sets the value for app.config.active_support.halt_callback_chains_on_return_false.

In this code, we check if that value has been set and eventually assign it to ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false.

Therefore, it made sense to me to run this method after the config/initializers/callback_terminator.rb has been called.

What kind of issues is this causing?

This comment has been minimized.

@matthewd

matthewd Apr 7, 2015

Member

@rafaelfranca this sounds like a bug^W possibly unintended behaviour I've run across before. After you define an initializer(before/after: ..), subsequent initializer definitions with no special ordering requirements (like the three below) will be inserted immediately after that one, instead of "falling" to their more natural position.

When I hit it, I just worked around it without investigating whether it was deliberate. Maybe it's not? Certainly my default expectation is that adding a definition like this should be a simple insertion, and have no effect on the order of the others.

@matthewd

matthewd Apr 7, 2015

Member

@rafaelfranca this sounds like a bug^W possibly unintended behaviour I've run across before. After you define an initializer(before/after: ..), subsequent initializer definitions with no special ordering requirements (like the three below) will be inserted immediately after that one, instead of "falling" to their more natural position.

When I hit it, I just worked around it without investigating whether it was deliberate. Maybe it's not? Certainly my default expectation is that adding a definition like this should be a simple insertion, and have no effect on the order of the others.

This comment has been minimized.

@rafaelfranca

rafaelfranca Apr 7, 2015

Member

@claudiob this caused a subtle change in our initializing order troter@50f51b0.

@matthewd yeah. I had the same suspicion, but for what I could understand this is how our TSort implementation is working. I'll investigate better today, but I fell that if we change this we may have more problems.

@rafaelfranca

rafaelfranca Apr 7, 2015

Member

@claudiob this caused a subtle change in our initializing order troter@50f51b0.

@matthewd yeah. I had the same suspicion, but for what I could understand this is how our TSort implementation is working. I'll investigate better today, but I fell that if we change this we may have more problems.

This comment has been minimized.

@rafaelfranca

rafaelfranca Apr 7, 2015

Member

Fixed at 0a120a8

@claudiob

This comment has been minimized.

Show comment
Hide comment
@claudiob

claudiob Apr 21, 2015

Member

I created a presentation related to this PR for RailsConf 2015
If anyone's interested, here's the link: https://speakerdeck.com/claudiob/better-callbacks-in-rails-5

Member

claudiob commented Apr 21, 2015

I created a presentation related to this PR for RailsConf 2015
If anyone's interested, here's the link: https://speakerdeck.com/claudiob/better-callbacks-in-rails-5

@claudiob

This comment has been minimized.

Show comment
Hide comment
Member

claudiob commented Jun 19, 2015

==> #20612

claudiob added a commit to claudiob/rails that referenced this pull request Sep 13, 2015

Remove methods that are never invoked
Fixes #21122 - does not change any current behavior; simply reflects
the fact that two conditions of the if/else statement are never reached.

The reason is #17227 which adds a default terminator to AS::Callbacks.

Therefore, even callback chains that do not define a terminator now
have a terminator, and `chain_config.key?(:terminator)` is always true.

Of course, if no terminator was defined, then we want this new default
terminator not to do anything special. What the terminator actually does
(or should do) is discussed in #21218 but the simple fact that a default
terminator exists makes this current PR valid.

claudiob added a commit to claudiob/rails that referenced this pull request Sep 13, 2015

Remove methods that are never invoked
Fixes #21122 - does not change any current behavior; simply reflects
the fact that two conditions of the if/else statement are never reached.

The reason is #17227 which adds a default terminator to AS::Callbacks.

Therefore, even callback chains that do not define a terminator now
have a terminator, and `chain_config.key?(:terminator)` is always true.

Of course, if no terminator was defined, then we want this new default
terminator not to do anything special. What the terminator actually does
(or should do) is discussed in #21218 but the simple fact that a default
terminator exists makes this current PR valid.

claudiob added a commit to claudiob/rails that referenced this pull request Sep 13, 2015

Remove AS methods that are never invoked
Fixes #21122 - does not change any current behavior; simply reflects
the fact that two conditions of the if/else statement are never reached.

The reason is #17227 which adds a default terminator to AS::Callbacks.

Therefore, even callback chains that do not define a terminator now
have a terminator, and `chain_config.key?(:terminator)` is always true.

Of course, if no terminator was defined, then we want this new default
terminator not to do anything special. What the terminator actually does
(or should do) is discussed in #21218 but the simple fact that a default
terminator exists makes this current PR valid.

claudiob added a commit to claudiob/rails that referenced this pull request Sep 13, 2015

Remove AS methods that are never invoked
Fixes #21122 - does not change any current behavior; simply reflects
the fact that two conditions of the if/else statement are never reached.

The reason is #17227 which adds a default terminator to AS::Callbacks.

Therefore, even callback chains that do not define a terminator now
have a terminator, and `chain_config.key?(:terminator)` is always true.

Of course, if no terminator was defined, then we want this new default
terminator not to do anything special. What the terminator actually does
(or should do) is discussed in #21218 but the simple fact that a default
terminator exists makes this current PR valid.

*Note* that the conditional/simple methods have not been removed in
AS::Conditionals::Filter::After because of `:skip_after_callbacks_if_terminated`
which lets a user decide **not** to skip after callbacks even if the chain was
terminated.

@bf4 bf4 referenced this pull request in rails-api/active_model_serializers Jan 4, 2016

Merged

Basic deserialization. #1248

betesh added a commit to betesh/paperclip that referenced this pull request Jan 20, 2016

Rails 5 fix for skipping post process if validation fails
rails/rails#17227 introduces a change to the way execution is halted by a before callback.  Instead of the before-hook returning false, it must `throw(:abort)`.

This feature is opt-in for projects upgrading from older versions of Rails, so we need to handle both new and upgrading apps by checking whether they are opted into this new behavior yet.

@rbr rbr referenced this pull request in rubysherpas/paranoia Feb 22, 2016

Open

Adding support to cancel "restore" and "real_destroy" actions #283

@denniscollective

This comment has been minimized.

Show comment
Hide comment
@denniscollective

denniscollective Mar 8, 2016

Is there anyway the scope of this could be increased to allow you to throw in controller methods that are run as guard clauses rather than before_filters to abort the rest of the action.

It would be really nice for avoiding DoubleRenderErrors.

Trivial Case:

class ThingsController < ApplicationController
  def index
    ensure_authorized
  end

  protected

  def ensure_authorized
    redirect_to root_url
    throw(:abort)
  end
end

I will gladly submit a pull request if it would be accepted, but don't want to waste time.

/cc @claudiob @dhh

Is there anyway the scope of this could be increased to allow you to throw in controller methods that are run as guard clauses rather than before_filters to abort the rest of the action.

It would be really nice for avoiding DoubleRenderErrors.

Trivial Case:

class ThingsController < ApplicationController
  def index
    ensure_authorized
  end

  protected

  def ensure_authorized
    redirect_to root_url
    throw(:abort)
  end
end

I will gladly submit a pull request if it would be accepted, but don't want to waste time.

/cc @claudiob @dhh

@claudiob

This comment has been minimized.

Show comment
Hide comment
@claudiob

claudiob Mar 9, 2016

Member

@denniscollective I think what you suggest would be overwhelming, meaning that we would have to wrap every action inside a catch(:abort), and we would let every method (not just "guard clauses") throw(:abort) and expect the action to be interrupted.

In the example above, what would you like to happen if the user is not authorized?
If you have something in mind, you can explicitly write it as:

def index
  if authorized
    # .. render view or any behavior that you desire
  else
    # .. redirect_to with a flash or any behavior that you desire
  end
end

If you really want the behavior provided by throw(:abort), you can instead move ensure_authorized from inside the index method into before_action :ensure_authorized, only: :index.

Member

claudiob commented Mar 9, 2016

@denniscollective I think what you suggest would be overwhelming, meaning that we would have to wrap every action inside a catch(:abort), and we would let every method (not just "guard clauses") throw(:abort) and expect the action to be interrupted.

In the example above, what would you like to happen if the user is not authorized?
If you have something in mind, you can explicitly write it as:

def index
  if authorized
    # .. render view or any behavior that you desire
  else
    # .. redirect_to with a flash or any behavior that you desire
  end
end

If you really want the behavior provided by throw(:abort), you can instead move ensure_authorized from inside the index method into before_action :ensure_authorized, only: :index.

jcoyne added a commit to samvera/active_fedora that referenced this pull request Jul 29, 2016

@jcoyne jcoyne referenced this pull request in samvera/active_fedora Jul 29, 2016

Merged

Throw abort to terminate callbacks #1116

cbeer added a commit to samvera-labs/active_fedora-datastreams that referenced this pull request Aug 18, 2016

cbeer added a commit to samvera-labs/active_fedora-datastreams that referenced this pull request Aug 18, 2016

@seanhandley seanhandley referenced this pull request in seanhandley/stronghold Dec 17, 2016

Merged

Upgrade to Rails 5 #132

23 of 23 tasks complete
@yamanaltereh

This comment has been minimized.

Show comment
Hide comment
@yamanaltereh

yamanaltereh Mar 1, 2017

@claudiob I used throw(:abort), but for save! it should return false not raise exception!

yamanaltereh commented Mar 1, 2017

@claudiob I used throw(:abort), but for save! it should return false not raise exception!

@claudiob

This comment has been minimized.

Show comment
Hide comment
@claudiob

claudiob Mar 1, 2017

Member

@yamanaltereh Hello! Can you provide a little more context or a snippet of code?

If you found a bug, a bug report would also be very useful. Thanks!

Member

claudiob commented Mar 1, 2017

@yamanaltereh Hello! Can you provide a little more context or a snippet of code?

If you found a bug, a bug report would also be very useful. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment