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
Fix AJ wrong deprecation message on after_callbacks_if_terminated
:
#38576
Fix AJ wrong deprecation message on after_callbacks_if_terminated
:
#38576
Conversation
16700d7
to
e59bd70
Compare
@@ -179,7 +179,17 @@ def self.halting_and_conditional(callback_sequence, user_callback, user_conditio | |||
result_lambda = -> { user_callback.call target, value } | |||
env.halted = halted_lambda.call(target, result_lambda) | |||
if env.halted | |||
target.send :halted_callback_hook, filter | |||
if target.method(:halted_callback_hook).arity == 1 |
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.
Did you measure if those conditionals will make callbacks slower?
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.
Thanks for pointing this out, didn't realise how much slower this could become 😱
Warming up --------------------------------------
without_arity_check 32.398k i/100ms
Calculating -------------------------------------
without_arity_check 380.361k (± 3.7%) i/s - 1.911M in 5.032849s
Comparison:
without_arity_check: 380361.4 i/s
with_arity_check: 106834.4 i/s - 3.56x slower
Do you have advice on how to proceed to make this change, without impacting performance ?
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.
The perf penalty comes from the deprecation. Without deprecation
Warming up --------------------------------------
without_arity_check 34.578k i/100ms
Calculating -------------------------------------
without_arity_check 389.923k (± 2.5%) i/s - 1.971M in 5.057767s
Comparison:
without_arity_check: 389922.9 i/s
with_arity_check_no_deprecation: 331838.6 i/s - 1.18x slower
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.
I'm assuming that result is from calling the callback, not defining, right?
I think we can drop the deprecation and just let the exception happen.
Or we can see if we can only show that deprecation once and cache the conditional.
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.
Sorry for the delay. I have made the change to cache the conditional.
This is the benchmark result:
❯ Warming up --------------------------------------
without_arity_check 33.638k i/100ms
Calculating -------------------------------------
without_arity_check 398.149k (± 2.7%) i/s - 2.018M in 5.073175s
Comparison:
without_arity_check: 398149.2 i/s
with_arity_check: 336557.6 i/s - 1.18x slower
I'm assuming that result is from calling the callback, not defining, right?
That's correct, the change I made that affects performance is when running the callbacks.
This is the script I used
require 'bundler/setup'
require 'active_support/callbacks'
require 'active_support/deprecation'
require 'active_support/core_ext/object/blank'
require 'benchmark/ips'
class A
include ActiveSupport::Callbacks
define_callbacks(:save)
set_callback(:save, :before) { throw(:abort) }
def halted_callback_hook(filter)
end
def run
run_callbacks(:save) do
end
end
end
ActiveSupport::Deprecation.silence do
a = A.new
Benchmark.ips do |x|
x.report('with') do
a.run
end
x.report('without') do
a.run
end
x.hold! 'temp_results'
x.compare!
end
end
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.
Callbacks are hotspot. I'm not sure we should add 20% overhead just to add a deprecation. I think it is better to document this breaking change in the CHANGELOG and release notes.
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 sounds good 👍 . It's pretty uncommon to redefine halted_callback_hook
so hopefully it shouldn't affect too many users
962fa58
to
6f309a4
Compare
6f309a4
to
e841884
Compare
- The `halted_callback_hook` method is called whenever the `terminator` halt the callback execution. Usually, this translate to when a `before` callback throw an `:abort`. <details> <summary> Example </summary> ```ruby class Foo include ActiveSupport::Callbacks define_callbacks :save set_callback(:save, :before) { throw(:abort) } def run run_callbacks(:save) do 'hello' end end def halted_callback_hook(filter) # filter is the proc passed to `set_callback` above end end ``` </details> ### Problem When a class has multiple callbacks, (i.e. `save`, `validate` ...), it's impossible to tell in the halted_callback_hook which type of callback halted the execution. This is useful to take different action based on the callback. <details> <summary> Use Case </summary> ```ruby class Foo include ActiveSupport::Callbacks define_callbacks :save define_callbacks :validate set_callback(:save, :before) { throw(:abort) } set_callback(:validate, :before) { throw(:abort) } def run run_callbacks(:validate) do ... end run_callbacks(:save) do ... end end def halted_callback_hook(filter) Rails.logger.warn("Couldn't save the record, the ??? callback halted the execution") end end ``` </details> ### Solution Allow `halted_callback_hook` to receive a second argument which is the name of the callback being run.
- ### Problem In some cirumstances, the deprecation message to warn that AJ won't run `after_(enqueue/perform)` callbacks when the chain is halted by a `throw(:abort)` will be thrown even though no `throw(:abort)` was thrown. ```ruby run_callback(:foo) do ... end ``` There is two possible way for the callback body to not be executed: 1) `before` callback throw a `abort` 2) `before` callback raises an error which is rescued by an around callback (See associated test in this commit for an example) When 2) happen we don't want to output a deprecation message, because what the message says isn't true and doesn't apply. ### Solution In order to differentiate between 1) and 2), I have added a `halted_callback_hook` which is called by ActiveSupport callback whenever the callback chain is halted.
e841884
to
0cdeee4
Compare
The first commit is a feature I'm adding to ActiveSupport callbacks which I use in the second commit.
Fix AJ wrong deprecation message on
after_callbacks_if_terminated
:Problem
In some cirumstances, the deprecation message to warn that AJ won't
run
after_(enqueue/perform)
callbacks when the chain is haltedby a
throw(:abort)
will be thrown even though nothrow(:abort)
was thrown.
There is two possible way for the callback body to not be executed:
before
callback throw aabort
before
callback raises an error which is rescued by anaround callback (See associated test in this commit for
an example)
When 2) happen we don't want to output a deprecation message,
because what the message says isn't true and doesn't apply.
Solution
In order to differentiate between 1) and 2), I have added
a
halted_callback_hook
which is called by ActiveSupport callbackwhenever the callback chain is halted.
cc/ @rafaelfranca @casperisfine @etiennebarrie