Conditionally Disabling Database Sessions in Ruby on Rails 3

I’ll start with the obvious: Why would you want to conditionally disable database sessions in Ruby on Rails 3? It’s a problem I’ve faced with BugMuncher for a long time, but for a different reason than most. The most common reason people want this is to stop sessions being created for web crawlers (bots). Here’s a couple of people having this issue - this person and this one too.

The first link covers disabling Cookie based sessions for bots, but for those using Database storage it doesn’t work.

TL;DR

Not interested in the how or why? Simply create a file config/initializers/session_store_ext.rb and fill it with the following. Also make sure to define BOT_REGEX to match known crawler user agents.

# Save this file as config/initializers/session_store_ext.rb 
# and don't forget to define BOT_REGEX

class ActiveRecord::SessionStore
  _set_session = instance_method :set_session

  define_method :set_session  do | env, sid, session_data, options |
    unless env['HTTP_USER_AGENT'] =~ BOT_REGEX
      _set_session.bind(self).call env, sid, session_data, options
    end

    sid
  end

  private :set_session
end

The gory details

As I mentioned earlier, the issue with BugMuncher wasn’t crawlers, as the BugMuncher app disallows crawlers through its robots.txt, and even if it didn’t, the bots would only ever see a login page.

The issue I was having with BugMuncher was actually much worse - The BugMuncher feedback tab is embedded in hundreds of sites, and any time someone saw the BugMuncher feedback tool on any website, a completely unneeded session was created in BugMuncher’s database. Even with a script to clean out old sessions, there were millions of rows in the sessions table, and it was steadily growing. Something had to be done.

After a couple of hours of Googling, and only finding solutions that covered cookie storage, I realised I’d have to fix this one myself.

Finding the session storage code

I spent some time digging through the Rails source code, but at this point I wasn’t too sure what I was looking for, so I decided to take a more direct approach: On my local machine I deleted the sessions table and fired up my local copy of BugMuncher, knowing it would throw an error when it couldn’t find the sessions table. This gave me a stack trace which I hoped would guide me to the file I needed to modify.

One line in the stack trace instantly stood out:

activerecord (3.2.8) lib/active_record/session_store.rb:307:in `get_session'

The get_session method was probably not going to be exactly what I was looking for, as the loading of sessions wasn’t the problem, I needed to find where they were saved. It was very close though, as a set_session method is defined directly below get_session, and that was the method I needed to overwrite.

BugMuncher uses a slightly different monkey patch to the one above, as I needed to disable sessions based on the current controller, instead of the user agent. This is only a slight (1 line) difference, and both patches do the following:

  1. Store the original set_session method as _set_session, so that it can still be called (you can’t use super when monkey patching).
  2. Define a new set_session method.
  3. In the new set_session method check to see if the request is from a bot (or in BugMuncher’s case, a specific controller).
  4. If so, just return the Session ID (what the original method returns) without saving the session.
  5. Otherwise bind the currently unbound original _set_session method to the current object, call it, and return the Session ID
  6. Make the new set_session method private, this isn’t required, but the original set_session method is private, and I like to respect that.

That’s the story of my first Rails Monkey Patch, and one that has since slowed down the growth of BugMuncher’s sessions table by a factor of around 14,000

P.S.

Every time I write ‘Conditionally Disabling Database Sessions in Ruby on Rails 3’ I can’t help but think of this:

Tube Man

- Matt Bearman