Skip to content
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

Merged
merged 2 commits into from Mar 3, 2020

Conversation

Edouard-chin
Copy link
Member

@Edouard-chin Edouard-chin commented Feb 26, 2020

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 halted
    by a throw(:abort) will be thrown even though no throw(:abort)
    was thrown.

      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.

cc/ @rafaelfranca @casperisfine @etiennebarrie

@@ -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
Copy link
Member

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?

Copy link
Member Author

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 ?

Copy link
Member Author

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

Copy link
Member

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.

Copy link
Member Author

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

Copy link
Member

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.

Copy link
Member Author

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

@Edouard-chin Edouard-chin force-pushed the ec-activejob-deprecation branch 2 times, most recently from 962fa58 to 6f309a4 Compare March 3, 2020 19:16
- 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.
@rafaelfranca rafaelfranca merged commit 40b7d93 into rails:master Mar 3, 2020
@Edouard-chin Edouard-chin deleted the ec-activejob-deprecation branch March 3, 2020 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants