Sean Eshbaugh

Web Developer + Programmer

Finding All ActiveRecord Callbacks

Most of the time ActiveRecord Callbacks are pretty straight forward. But sometimes in larger projects or when using certain gems you can end up with more callbacks happening than you realize. If you're curious about just what is happening when on your model there's no straight forward way that I'm aware of to find out. However, it's actually not too difficult to do yourself.

If you look at the methods available on an ActiveRecord model you'll find several related to callbacks. Here's what we find when inspecting a model that has a Paperclip attachment (you'll see why in a minute).

~/my_project% rails c
Loading development environment (Rails 4.2.0)
2.2.1 :001 > MyModel.methods.select { |method| method.to_s.include?('callback') }
 => [:_validate_callbacks,
 :_save_callbacks,
 :_destroy_callbacks,
 :_commit_callbacks,
 :_post_process_callbacks,
 :_post_process_callbacks?,
 :_post_process_callbacks=,
 :_file_post_process_callbacks,
 :_file_post_process_callbacks?,
 :_file_post_process_callbacks=,
 :_validate_callbacks?,
 :_validate_callbacks=,
 :_validation_callbacks,
 :_validation_callbacks?,
 :_validation_callbacks=,
 :_initialize_callbacks,
 :_initialize_callbacks?,
 :_initialize_callbacks=,
 :_find_callbacks,
 :_find_callbacks?,
 :_find_callbacks=,
 :_touch_callbacks,
 :_touch_callbacks?,
 :_touch_callbacks=,
 :_save_callbacks?,
 :_save_callbacks=,
 :_create_callbacks,
 :_create_callbacks?,
 :_create_callbacks=,
 :_update_callbacks,
 :_update_callbacks?,
 :_update_callbacks=,
 :_destroy_callbacks?,
 :_destroy_callbacks=,
 :_commit_callbacks?,
 :_commit_callbacks=,
 :_rollback_callbacks,
 :_rollback_callbacks?,
 :_rollback_callbacks=,
 :raise_in_transactional_callbacks,
 :raise_in_transactional_callbacks=,
 :define_paperclip_callbacks,
 :normalize_callback_params,
 :__update_callbacks,
 :set_callback,
 :skip_callback,
 :reset_callbacks,
 :define_callbacks,
 :get_callbacks,
 :set_callbacks,
 :define_model_callbacks]

That's a pretty lengthy list, and just by glancing at it we can see several methods like _initialize_callbacks= and skip_callback that aren't likely to be relevant to the problem at hand. The protected method get_callbacks looks promising, but if you look at the source:

def get_callbacks(name)
  send "_#{name}_callbacks"
end

it quickly becomes obvious that it wasn't meant to be used to get a comprehensive list of all the callbacks on a model. Instead it just gives us the callbacks related to one particular event. That's great, but what about when we don't know all of the events? I deliberately chose a model with a Paperclip attachment because Paperclip provides some of its own callback events. They could easily be missed if we assumed only the standard ActiveRecord callbacks were available. Without knowing otherwise before hand that's a fair, but potentially incorrect, assumption.

From get_callbacks we can see that the methods it calls all take the form of "_#{name}_callbacks" where name is the name of the event. Well, a few methods in our list from before seem to match that pattern, so with a little help from a regular expression we can get just those:

2.2.1 :002 > MyModel.methods.select { |method| method.to_s =~ /^_{1}[^_].+_callbacks$/ }
 => [:_validate_callbacks,
 :_save_callbacks,
 :_destroy_callbacks,
 :_commit_callbacks,
 :_post_process_callbacks,
 :_file_post_process_callbacks,
 :_validation_callbacks,
 :_initialize_callbacks,
 :_find_callbacks,
 :_touch_callbacks,
 :_create_callbacks,
 :_update_callbacks,
 :_rollback_callbacks]

This is great, but still not quite what we want. Each of these methods returns an array-like CallbackChain object containing a set of Callback objects:

2.2.1 :003 > MyModel._save_callbacks
 => #<ActiveSupport::Callbacks::CallbackChain:0x007fbf7567e918
 @callbacks=nil,
 @chain=
  [#<ActiveSupport::Callbacks::Callback:0x007fbf7362c098
    @chain_config=
     {:scope=>[:kind, :name],
      :terminator=>
       #<Proc:0x007fbf73237cf8@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/activemodel-4.2.0/lib/active_model/callbacks.rb:106 (lambda)>,
      :skip_after_callbacks_if_terminated=>true},
    @filter=
     #<Proc:0x007fbf7362c390@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/paperclip-4.2.1/lib/paperclip/has_attached_file.rb:91>,
    @if=
     [#<ActiveSupport::Callbacks::Conditionals::Value:0x007fbf7362c340
       @block=
        #<Proc:0x007fbf7362c2f0@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/activemodel-4.2.0/lib/active_model/callbacks.rb:141>>],
    @key=70230125666760,
    @kind=:after,
    @name=:save,
    @unless=[]>,
   #<ActiveSupport::Callbacks::Callback:0x007fbf75684ae8
    @chain_config=
     {:scope=>[:kind, :name],
      :terminator=>
       #<Proc:0x007fbf73237cf8@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/activemodel-4.2.0/lib/active_model/callbacks.rb:106 (lambda)>,
      :skip_after_callbacks_if_terminated=>true},
    @filter=:autosave_associated_records_for_document,
    @if=[],
    @key=:autosave_associated_records_for_document,
    @kind=:before,
    @name=:save,
    @unless=[]>,
   #<ActiveSupport::Callbacks::Callback:0x007fbf7567ea80
    @chain_config=
     {:scope=>[:kind, :name],
      :terminator=>
       #<Proc:0x007fbf73237cf8@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/activemodel-4.2.0/lib/active_model/callbacks.rb:106 (lambda)>,
      :skip_after_callbacks_if_terminated=>true},
    @filter=:autosave_associated_records_for_uploader,
    @if=[],
    @key=:autosave_associated_records_for_uploader,
    @kind=:before,
    @name=:save,
    @unless=[]>],
 @config=
  {:scope=>[:kind, :name],
   :terminator=>
    #<Proc:0x007fbf73237cf8@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/activemodel-4.2.0/lib/active_model/callbacks.rb:106 (lambda)>,
   :skip_after_callbacks_if_terminated=>true},
 @mutex=#<Mutex:0x007fbf7567e8c8>,
 @name=:save>

Each of these has an interesting method named raw_filter which returns either a method name Symbol or a Proc object. Let's see what we get when we inspect that for each of our model's save callbacks:

2.2.1 :004 > MyModel._save_callbacks.map { |callback| callback.raw_filter }
 => [#<Proc:0x007fbf7362c390@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/paperclip-4.2.1/lib/paperclip/has_attached_file.rb:91>,
 :autosave_associated_records_for_document,
 :autosave_associated_records_for_uploader]

We get an array with a Proc and a couple of Symbols which starts to give us a much better sense of what will happen when we save a model. There's one more important detail though that we've overlooked, each Callback object has a kind property that will tell us whether the callback gets called before, after, or around the event. Let's group our callbacks by kind:

2.2.1 :005 > MyModel._save_callbacks.group_by(&:kind).each { |_, callbacks| callbacks.map! { |callback| callback.raw_filter } }
 => {:after=>
  [#<Proc:0x007fbf7362c390@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/paperclip-4.2.1/lib/paperclip/has_attached_file.rb:91>],
 :before=>
  [:autosave_associated_records_for_document,
   :autosave_associated_records_for_uploader]}
 => {:after=>[#<Proc:0x007fbf7362c390@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/paperclip-4.2.1/lib/paperclip/has_attached_file.rb:91>], :before=>[:autosave_associated_records_for_document, :autosave_associated_records_for_uploader]}

Awesome! Finally something that will start to give us real insight into what happens when. But we can still do better, what about all the callbacks? If we combine the regular expression filter of the class methods from before with the above we get a complete picture for the whole model:

2.2.1 :006 > MyModel.methods.select { |method| method.to_s =~ /^_{1}[^_].+_callbacks$/ }.each_with_object({}) { |method, memo| memo[method] = MyModel.send(method).group_by(&:kind).each { |_, callbacks| callbacks.map! { |callback| callback.raw_filter } } }
 => {:_validate_callbacks=>
  {:before=>
    [#<ActiveModel::BlockValidator:0x007fbf7362d3f8
      @attributes=[:file],
      @block=
       #<Proc:0x007fbf7362d510@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/paperclip-4.2.1/lib/paperclip/has_attached_file.rb:27>,
      @options={}>,
     #<Paperclip::Validators::MediaTypeSpoofDetectionValidator:0x007fbf73624320
      @attributes=[:file],
      @options=
       {:if=>
         #<Proc:0x007fbf736245f0@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/paperclip-4.2.1/lib/paperclip/has_attached_file.rb:85 (lambda)>}>,
     #<ActiveRecord::Validations::PresenceValidator:0x007fbf7567e440
      @attributes=[:document],
      @options={}>,
     #<ActiveRecord::Validations::PresenceValidator:0x007fbf7567dc20
      @attributes=[:uploader],
      @options={}>,
     #<ActiveRecord::Validations::UniquenessValidator:0x007fbf7567d400
      @attributes=[:file_fingerprint],
      @klass=
       MyModel(id: integer, file_file_name: string, file_content_type: string, file_file_size: integer, file_updated_at: datetime, file_fingerprint: string, created_at: datetime, updated_at: datetime),
      @options=
       {:case_sensitive=>true,
        :if=>
         #<Proc:0x007fbf7567d5b8@/Users/sean_eshbaugh/sites/clickherelabs/hub/app/models/attachment.rb:22 (lambda)>}>,
     #<Paperclip::Validators::AttachmentPresenceValidator:0x007fbf7567c4b0
      @attributes=[:file],
      @options={}>,
     #<Paperclip::Validators::AttachmentSizeValidator:0x007fbf756774d8
      @attributes=[:file],
      @options={:less_than=>1073741824}>,
     #<Paperclip::Validators::AttachmentFileTypeIgnoranceValidator:0x007fbf75676510
      @attributes=[:file],
      @options={}>]},
 :_save_callbacks=>
  {:after=>
    [#<Proc:0x007fbf7362c390@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/paperclip-4.2.1/lib/paperclip/has_attached_file.rb:91>],
   :before=>
    [:autosave_associated_records_for_document,
     :autosave_associated_records_for_uploader]},
 :_destroy_callbacks=>
  {:before=>
    [#<Proc:0x007fbf73627f48@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/paperclip-4.2.1/lib/paperclip/has_attached_file.rb:92>]},
 :_commit_callbacks=>
  {:after=>
    [#<Proc:0x007fbf736279f8@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/paperclip-4.2.1/lib/paperclip/has_attached_file.rb:93>]},
 :_post_process_callbacks=>{},
 :_file_post_process_callbacks=>
  {:before=>
    [#<Proc:0x007fbf75687888@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/paperclip-4.2.1/lib/paperclip/validators.rb:67>,
     #<Proc:0x007fbf75677b18@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/paperclip-4.2.1/lib/paperclip/validators.rb:67>,
     #<Proc:0x007fbf75676bf0@/Users/sean_eshbaugh/.rvm/gems/ruby-2.2.1@my_project/gems/paperclip-4.2.1/lib/paperclip/validators.rb:67>]},
 :_validation_callbacks=>{},
 :_initialize_callbacks=>{},
 :_find_callbacks=>{},
 :_touch_callbacks=>{},
 :_create_callbacks=>{},
 :_update_callbacks=>{},
 :_rollback_callbacks=>{}}

And for the sake of reusability we can easily wrap this up in a module (pardon the terrible name):

module ShowCallbacks
  def show_callbacks
    _callback_methods = methods.select do |method|
      method.to_s =~ /^_{1}[^_].+_callbacks$/
    end

    _callback_methods.each_with_object({}) do |method, memo|
      memo[method] = send(method).group_by(&:kind).each do |_, callbacks|
        callbacks.map! do |callback|
          callback.raw_filter
        end
      end
    end
  end
end

class MyModel
  extend ShowCallbacks
  ...
end

One of the Hard Things

There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors.

I love that quote, not only because it's more amusing than it should be, but because it's extremely true. I know because I've been bitten by all three things plenty of times. Tonight it was while using the rails-settings-cached gem to handle some global settings for a Rails application.

At some point I truncated the settings table so I could reset it with new defaults. Afterwards my new settings weren't taking in the application or showing up in the database. I tried to mimic the behavior of #save_default but with some extra output by doing the following inside my initializer

if Setting.application_title.nil?
  puts 'Setting application_title.'

  Setting.application_title = 'My Application'
end

just to make sure something weird wasn't going on.

Setting.application_title wasn't returning nil so the setting wasn't being set, even after restarting the server. I discovered that when I added Rails.cache.delete('settings:application_title') before the above that it worked just fine. So of course the normal call to #save_default worked just fine as well.

It then occurred to me that the problem might be related to Spring which keeps Rails loaded and ready to get started quickly. I couldn't find confirmation in the Spring source but I'm guessing that by keeping the Rails process around it also keeps the cache nice and full. This means that, despite removing the setting's table's contents and restarting the server, the old settings were hanging around in memory. I'm hesitant to say with 100% confidence that this is what was happening, but it certainly makes sense to me.

Spring ships with Rails 4.1 by default so if you're making heavy use of the Rails cache this sort of thing is probably something you'll have to look out for. Also, keep in mind that the Spring readme does mention, "There's no need to 'shut down' spring. This will happen automatically when you close your terminal."

Configuring Nginx and Unicorn for force_ssl

It turns out that setting up SSL on Nginx + Unicorn + Rails is actually pretty easy. But there's a few pitfalls you have to watch out for. The following guide is based partially on these instructions and assumes you already have an SSL certificate and already have it placed on your server.

Let's take a look at our initial Nginx configuration file. You can find yours in /etc/nginx/sites-available, but if you're reading this you probably already knew that.

upstream unicorn_mysite {
	server unix:/tmp/unicorn.mysite.sock fail_timeout=0;
}

server {
	listen 80;
	server_name mysite.com;
	root /srv/mysite/current/public;
	access_log /srv/mysite/shared/log/nginx.access.log main;
	error_log /srv/mysite/shared/log/nginx.error.log info;

	location ^~ /assets/ {
		gzip_static on;
		expires max;
		add_header Cache-Control public;
	}

	try_files $uri/index.html $uri @unicorn;
	location @unicorn {
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header Host $http_host;
		proxy_redirect off;
		proxy_pass http://unicorn_mysite;
	}

	error_page 500 502 503 504 /500.html;
	client_max_body_size 4G;
	keepalive_timeout 10;
}

As you can see, this configuration makes some assumptions about our setup that are unlikely to be true for yours. However, for this exercise the details of the configuration are largely inconsequential.

In your editor of choice take the above config file and copy the server section and paste it below. Now, make the second server section look something like this:

server {
	listen 443;
	ssl on;
	ssl_certificate /etc/httpd/conf/ssl/mysite.com.crt;
	ssl_certificate_key /etc/httpd/conf/ssl/mysite.com.key;

	server_name mysite.com;
	root /srv/mysite/current/public;
	access_log /srv/mysite/shared/log/nginx.ssl.access.log main;
	error_log /srv/mysite/shared/log/nginx.ssl.error.log info;

	location ^~ /assets/ {
		gzip_static on;
		expires max;
		add_header Cache-Control public;
	}

	try_files $uri/index.html $uri @unicorn;
	location @unicorn {
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Proto https;
		proxy_set_header Host $http_host;
		proxy_redirect off;
		proxy_pass http://unicorn_mysite;
	}

	error_page 500 502 503 504 /500.html;
	client_max_body_size 4G;
	keepalive_timeout 10;
}

The first difference you should notice is the listen port. HTTPS uses port 443 instead of port 80. The following three lines tell Nginx that we want SSL on and where our certificate and where our certificate keys are being stored. /etc/httpd/conf/ssl is a pretty standard location, but you can keep them anywhere.

The next change we make is to the log file locations. The normal HTTP config will write to nginx.access.log and nginx.error.log. Here we're telling the HTTPS config to write to nginx.ssl.access.log and nginx.ssl.error.log instead. If you ever encounter any problems with your SSL setup it'll be pretty handy to have your logs separated out by protocol.

The last difference between the two configurations is the extra proxy_set_header setting. Since we plan on using force_ssl in our Rails application to selectively ensure SSL on different controllers this step is really important. force_ssl relies on the HTTP_X_FORWARDED_PROTO HTTP header to determine whether or not the request was an HTTPS request. If this setting isn't set to https then you will end up with an infinite redirect loop as force_ssl will always think the forwarded request isn't HTTPS.

At this point you should restart Nginx: sudo /etc/init.d/nginx restart. In your Rails app's controller add the call to force_ssl at the top like this:

class ContactsController < ApplicationController
  force_ssl
  before_filter :whatever
  ...

Now, when you go to any action on that controller you should immediately be redirected to the SSL version.

If you get an error similar to "Error 102 (net:: ERR CONNECTION REFUSED)" then this likely means your server is blocking port 443. Odds are you won't have this issue, but I did, so it makes sense to me to include a possible fix. This makes the assumption you're using iptables to manage your ports. Open up /etc/sysconfig/iptables and look for a line similar to this:

-A INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT

Immediately below it add the following:

-A INPUT -m state --state NEW -m tcp -p tcp --dport 443 -j ACCEPT

As usual, if your settings look similar but not quite the same then base your changes off your settings. The important part here is the --dport, we want to open up port 443. After you do this you'll need to restart iptables with sudo /etc/init.d/iptables restart.

At this point your controllers with force_ssl in them should be redirecting to the SSL version of your site. Like most ActionController callbacks you can also specify which actions force_ssl will be run on using the only and except options.

Overriding But Preserving Ruby Methods

Recently I found myself needing to overwrite ActiveRecord's default save method but still retain the ability to call the original method. I know, I know, that's crazy talk, right? What could you possibly need to do that for? Well, in my case it was to provide a way to create "drafts" of my models under certain conditions when save is called. Rather than have all sorts of messy logic repeated over an over my controllers or tucked away in an awkward helper method it made much more sense to me to attach the functionality on my models as I need it. The ever so sublime paper_trail gem does something quite similar with ActiveRecord callbacks. But that isn't quite what I needed. What I really wanted was the ability to prevent a model from being saved in the first place. After all, what good is saving a draft if we've overwritten the original in the process? I particularly had in mind a use case where some users could only save drafts, which could be approved at a later time by more privileged users.

So now that we know the why of doing something that at first seems crazy (and more than a bit dangerous), what about the how? The core of how to override but preserve a method is pretty simple, but I think it might be helpful to provide some context, so bear with me.

Just like paper_trail, and many other gems, we start off with the following to get our module to load whenever ActiveRecord is loaded. This ensures that we don't have to manually include our module.

# /lib/kentouzu.rb
ActiveSupport.on_load(:active_record) do
  include Kentouzu::Model
end

Next we define self.included in or Model module so that when it's included we extend the base class with the ClassMethods module. This provides a slew of class methods to our model, the most important of which for the purpose of this post is the has_drafts method.

# /lib/kentouzu/has_drafts.rb
module Kentouzu
  module Model
    def self.included(base)
      base.send :extend, ClassMethods
    end

The has_drafts method provides us with a nice way of making it so we only include our InstanceMethods when we actually need it. It'd be really bad if we always override a vital method like save! If we just included the code to orverride the method without going through this it would lead to all sorts of disasterous behavior as our earlier hook into ActiveSupport#on_load would include it in every model in our application even when it doesn't make sense.

By providing this method we give a nice clean way to add functionality to our models (or really, any class) in the same way paper_trail's has_paper_trail does. Lots of gems take advantage of this pattern.

    module ClassMethods
      def has_drafts options = {}
        send :include, InstanceMethods
      end
    end

Here's where things start to get interesting (and relevant). In our InstanceMethods module we use the same self.included method as before. But this time we call instance_method(:save) on the base class to get an UnboundMethod for save. This allows us to reuse it later.

    module InstanceMethods
      def self.included(base)
        default_save = base.instance_method(:save)

After getting a reference to the old save method we then override it with define_method, sent to the base class. define_method is important because it allows access to the surrounding scope where default_save is defined. This lets us use it even after its out of scope. Inside the block the key is the if statement. It checks for the conditions for using our new save method. In my particular case I check to make sure that everything is enabled on the model (in pretty much the same way paper_trail does) and that the conditions for saving are met and then create draft from the model and save the draft without saving the model. The details of what happens here are up to you.

        base.send :define_method, :save do
          if switched_on? && save_draft?
            draft = Draft.new(:item_type => self.class.base_class.to_s, :item_id => self.id, :event => self.persisted? ? "update" : "create", :source_type => Kentouzu.source.present? ? Kentouzu.source.class.to_s : nil, :source_id => Kentouzu.source.present? ? Kentouzu.source.id : nil, :object => self.to_yaml)

            draft.save

And now for the magic. If the conditions for using our new version of the save method aren't met we take our unbound reference to the old save and bind it to self which, since this is an instance method on our model now, is our model. Finally we call it with the () method. You could also use call.

          else
            default_save.bind(self).()
          end
        end
      end
    end
  end
end

Now whenever we call the save method on our model so long as switched_on? and save_draft? return true we'll get a copy of the model as a draft. Of course we could strip this down to something much simpler without all the fancy including, but in my opinion all that is what makes this so useful, we only get it when and where we want it. That's pretty important because overriding methods like this can be very dangerous. I strongly suggest that before you do this you make sure you actually need to.

The source for the gem this is from is on GitHub.