~7 minutes

ActiveSupport: Self Deprecation and Deprecations

As I write today’s journal, I am struck with how I’m not the ideal tour guide of the Rails source. Having never contributed to the code and coming at it with fresh eyes, I’m bound to get things wrong in my notes here. Hopefully when I do, you fabulous readers out there can provide some corrections in the comments section which I can fold back into the body of the post.

Ok, back to the source!

Deprecations

I know you have seen the output of this code before…you’re booting your app when all of the sudden you see something like:

DEPRECATION WARNING: ActiveSupport::BufferedLogger is deprecated!  Use ActiveSupport::Logger instead.

This helpful (though generally frustrating) warning message is emitted by some pretty cool code, code that uses a technique we haven’t seen yet in this series. Let’s take a look.

module ActiveSupport
  class Deprecation
    DEFAULT_BEHAVIORS = {
      :stderr => Proc.new { |message, callstack|
        $stderr.puts(message)
        $stderr.puts callstack.join("\n  ") if debug
      },
      :log => Proc.new { |message, callstack|
        logger =
            if defined?(Rails) && Rails.logger
              Rails.logger
            else
              require 'active_support/logger'
              ActiveSupport::Logger.new($stderr)
            end
        logger.warn message
        logger.debug callstack.join("\n  ") if debug
      },
      :notify => Proc.new { |message, callstack|
        ActiveSupport::Notifications.instrument("deprecation.rails",
                                                :message => message, :callstack => callstack)
      },
      :silence => Proc.new { |message, callstack| }
    }

    module Behavior
      attr_accessor :debug

      def behavior
        @behavior ||= [DEFAULT_BEHAVIORS[:stderr]]
      end

      def behavior=(behavior)
        @behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || b }
      end
    end
  end
end

To simplify to the part I’m interested in (though the $stderr global variable is interesting too):

DEFAULT_BEHAVIORS = {
  :stderr => Proc.new { |message, callstack|
    # stuff
  },
  :log => Proc.new { |message, callstack|
    # stuff
  },
  :notify => Proc.new { |message, callstack|
    # stuff
  },
  :silence => Proc.new { |message, callstack| }
}

I believe this is a use of the Strategy Pattern for swapping out the behaviour of the deprecation warnings at runtime, though I’m certainly not an expert at Patterns. It is implemented via a constant hash where the values are Procs.

As a neat bonus, the assignment operator is pretty clever, enabling single strategies, multiple strategies, a custom handler, or an inline Proc:

def behavior=(behavior)
  @behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || b }
end

So you can call it in any of the following ways:

ActiveSupport::Deprecation.behavior = :stderr
ActiveSupport::Deprecation.behavior = [:stderr, :log]
ActiveSupport::Deprecation.behavior = MyCustomHandler
ActiveSupport::Deprecation.behavior = proc { |message, callstack|
  # custom stuff
}

Very cool, this is super extensible and adaptable to whatever kinds of behaviors a developer might want.

Proc

I should take a brief aside to talk about Procs since they may or may not be used a lot in the code you’ve come across (but will be everywhere in the Rails source).

You know how we like to deal with objects, bundling up data and code into a package that represents a conceptual unit of some kind? Well, a Proc or Lambda gives us the ability to make an object out of some code which we can then pass around to other methods. This is why Ruby is described as having “first-class functions” (meaning it doesn’t treat them as second-class citizens, but they can be arguments to other functions). Those functions which take functions for arguments are then called “higher order functions”, which Ruby has a metric ton of (think map, inject, and on and on…). For a thorough overview of the ways to pass around code in Ruby, check out this article on Skorks.

Bottom line, they are awesome and come in handy.

Alias Method Chaining

Another thing I found interesting in my study of the Deprecation subsystem was the convenient tools provided for deprecating methods. It gives a chance to look into some metaprogramming, which is always fun.

Let’s say we were the designers of a module called Fred which we declared as follows (example from source comments):

module Fred
  extend self

  def foo; end
  def bar; end
  def baz; end
end

Pretty awesome module right?

As time went on, we realized that we wanted to get users of our module to move away from Fred#foo. With active_support, we could just:

ActiveSupport::Deprecation.deprecate_methods(Fred, foo: 'use Bar#foo instead')

Now when someone tries to use our method, they will be encouraged to make the switch:

Fred.foo
# => "DEPRECATION WARNING: foo is deprecated and will be removed from Rails 4.1 (use Bar#foo instead)."

But, how does it do that? It turns out it uses a very common pattern in Ruby metaprogramming:

  • alias the original method to another name
  • insert a method into the module that wraps…
  • call whatever new logic you want
  • call the original method, now aliased

Here’s the code that does it:

module ActiveSupport
  class Deprecation
    module MethodWrapper
      def deprecate_methods(target_module, *method_names)
        options = method_names.extract_options!
        deprecator = options.delete(:deprecator) || ActiveSupport::Deprecation.instance
        method_names += options.keys

        method_names.each do |method_name|
          target_module.alias_method_chain(method_name, :deprecation) do |target, punctuation|
            target_module.send(:define_method, "#{target}_with_deprecation#{punctuation}") do |*args, &block|
              deprecator.deprecation_warning(method_name, options[method_name])
              send(:"#{target}_without_deprecation#{punctuation}", *args, &block)
            end
          end
        end
      end
    end
  end
end

We see our familiar friend extract_options! doing it’s job, and then a default for the deprecator that is going to do the work. The real bulk of the work is in the enumeration through the method_names. Matching up the logic there with my description of the pattern above we see:

  • alias_method_chain creates both aliases for us, it’s found in core_ext/module/aliasing.rb
  • .send(:define_method, ...with_deprecation) makes the wrapper method
  • deprecation_warning actually generates the event for whatever behavior handler picks it up
  • .send(:...without_deprecation) then calls the original method

We will see this quite a bit I’m sure, as it’s a really handy way to put a wrapper around a method.

Rails 4

I may not have mentioned this, but all of this series is on the Rails 4 source. I’m sure we will come across areas where it will behave differently than your installed Rails. In that case, use the Rails-Dev-Box that we setup in Part 1. I have been using it a lot thoughout this exploration.

cd rails-dev-bex
vagrant up
vagrant ssh
cd /vagrant/rails
./tools/console

Voila, Rails 4 source console all loaded and ready for experimentation.

Have fun reading the source!

Tagged with: programming

My first novel is coming soon-ish!

Check out Singular