Don't make a new one on my account

— January 12, 2007 at 07:40 PST


Here's a fairly obscure but useful trick to optimize session usage in Rails. I can pretty safely bet that you haven't used it yet because it was broken until two weeks ago when, with bloody forehead, I discovered the bug that had been causing me to bang my head repeatedly on the table. Big kudos to Jeremy Kemper for assisting in the speedy fix. Anyway, where was I?

Oh yeah. Sessions.

Sessions are mostly useless. There, I said it. I'd guess that at least 95% of the time all people are using sessions for in Rails are flash messages and setting the :user_id of authenticated users. If your app doesn't authenticate users or have some other unusual need for sessions, it's a pretty painful waste of that overhead to use them only for flash messages.

One of the nice things about how Mephisto is designed is that only authenticated users get a session when they login to the admin console. (Yes, there are no flash messages.) That means that the kajillions of guest users who are just reading a blog don't get a session. And that removes a lot of overhead from serving up pages, especially when most of the pages are cached as static HTML (yay for Rails' page caching!). I think that was a great decision, but it became a pain when I decided I wanted to highlight comments I made in my own blog as special so readers could easily see which comments were mine.

The problem was that the main controller is configured with session :off, so no session was available to check to see if there was an authenticated user making the comment. Now, the admin controller has sessions on, so if I had already logged in on the admin console I'd have a session_id cookie in the browser, and the main controller could just notice it there and use it to get the commenter's user id to mark the comment as special. At least in theory. But you can't see an existing session with sessions turned off in your controller!

It turns out there is a lovely option on the session directive for controllers.

LazyController < ActiveController::Base
  session :new_session => false
  # ...
end

Using the :new_session => false option tells Rails not to create a new session if there isn't one already, but if there is, go ahead and use it. That's exactly what I needed, and it ended up working perfectly to let me hack Mephisto to mark my comments as special. (At least it did after Jeremy and I fixed the crashing bug.) If you use this option and there is no previously existing session, the session will appear as an empty hash. You can set values in that hash, but they won't stick around to the next action.

You can see the special comments in action right here in my blog - just look around. You can also use that feature in your own Mephisto blog, since Rick accepted my patch and now trunk will do that for you too. Check out the wiki page on how to do it.

And if you're not using Mephisto, you can still use the :new_session => false option to lighten the load on your web app. I think this is potentially very useful for RESTful apps. I like the idea of blending admin functions into the normal content UI. This way you can have sessionless guest users and still provide authorized admin features on public pages.

18 commentsmephisto, rails, sessions

Comments
  1. Jan Wikholm2007-01-12 11:56:44

    Thanks Josh for this tip and a tiny peek at how Mephisto works :)

  2. Matt2007-01-12 11:56:48

    When you say it was broke - can I assume its only now fixed in the Rails edge? - and as such is still broken in 1.1.6 (and even the upcoming 1.2)

  3. Piers Cawley2007-01-12 11:57:46

    Oh thank you! That just made one of the things I was aiming to do to fix Typo a great deal easier to do.

  4. Don2007-01-12 15:45:28

    Cool, thanks for the tip. Yeah that is obscure, now I just hope that little nugget of knowledge comes back to the front of my brain the next time I need it! :)

  5. Brittain2007-01-12 17:04:07

    On a somewhat related note, does this relate to us having 10s of thousands of records in our production DB SESSIONS table?

    We configured session usage following the Agile and Recipes books and somewhat blindly assumed there'd either be (a) session reuse or (b) some RAILS session housekeeping.

    Were we deluded? Should we be policing our SESSIONS table?

  6. Josh Susser2007-01-12 17:06:10

    Matt: it's fixed in trunk and 1.2.

    Piers: yep, I had that in mind too. I'd been wanting to do that in typo as well. Also think about embedded "delete this comment" controls for admins on the article views. That's on my list now too.

    rluv: that's a bug in the scribbish template. No, actually, I want you to keep reading my blog post over and over and over and over...

  7. Josh Susser2007-01-12 17:13:00

    Brittain: Rails doesn't do any automatic housekeeping of sessions for you. If you are using ActiveRecordStore for sessions, the rake task rake db:sessions:clear will remove all sessions without regard to age. Most people write a cron job to periodically flush sessions older than a certain limit (1 hour, 1 day, take your pick).

  8. mw2007-01-13 06:35:26

    You wrote: ...especially when most of the pages are cached as static HTML

    I fail to see how session control matters in case of cached pages. If a page is cached it will be delivered to the client by the web server without any ruby code involved. Session won't be read in this case anyway.

  9. bill heartwell2007-01-13 19:21:15

    hey Josh, great stuff! I always learn something new here. This particular post helps me out a whole lot!

    Thanks, bill

  10. Josh Susser2007-01-13 20:23:56

    mw: Consider the scenario where you create a session for every user that views your blog. That will require running Rails code. But if your pages are cached and you don't need to create a session, then you can just have the web server return the cached page and not run any Rails code at all, just as you said. The point is that you can't get away with that if you are creating sessions for all users. Mephisto is a great example of how to architect an app to avoid that situation.

  11. mw2007-01-14 11:44:55

    How do you create a session for a user only seeing cached pages? There will be sessions only for the few people creating the cached pages, the rest will not have a session anyway. So there is still an advantage avoiding these sessions, but if a significant amount of requests is handled by cached pages, the advantage is small not large. Probably I got something wrong about page caches but AFAIK there is no chance to have a session for a user reading only cached pages.

  12. Josh Susser2007-01-15 05:03:04

    mw: That's my point. It seems we're in violent agreement. But beyond that, I'm saying that you can't architect your app to make as good use of page caching if you need to create sessions for all users for doing things like using flash messages ("Your comment was successfully posted!").

  13. Tim Lucas2007-01-15 22:29:05

    Sweet... hadn't noticed this option.

  14. gcnovus2007-01-20 06:51:35

    Like many of your posts, this one got me thinking about the next step. I love how many "hidden features" you reveal.

    Unlike many of your posts, however, I'm stuck on the next step on this one.

    I think what I want to do is this:

    ApplicationController < ActiveController::Base session :new_session => false # turn off sessions by default # ... end

    controller that handles logins

    SessionController < ActiveController::Base session :new_session => true, :only => {:login} def login if new User(...).login_successful? redirect_to ... else delete the session - but how? redirect_to :login end end

    That is, if the login wasn't successful, I want to get rid of that session. The problem is, I don't see a method in Controller that will do this. Do I have to just leave it there for everyone who tries to login and have my cleanup job (cron or the built-in one from mem_cached) handle it?

  15. Josh Susser2007-01-20 16:41:39

    gcnovus: That's an interesting question. Mephisto now does the simple thing. It creates a session for anyone using the AdminController, which means anyone even attempting to log in, and no special cleanup for sessions of failed logins.

    I think you could probably refactor your login process so that a session would not be created until after the authentication credentials had been verified, say by having separate methods to check credentials and processes a verified login, using the sessiondirective to restrict session creation to the processing method . There are obviously some security concerns to address if you do that, but I think it should work.

  16. gcnovus2007-01-20 20:05:30

    The major problem with delaying session creation is that you can't do a redirectto(session[:jumpto_url]).

    I guess the "Rails way" answer is to not care about the extra session unless it starts bottlenecking my server. I was just hoping there were some obvious session management methods I was missing.

  17. rick2007-02-01 18:23:23

    gcnovus: just a guess, but if you're using a session key to redirect a request, that probably means you're probably requiring a login to a page before continuing. In this case, you do need a session anyway.

    Though, if you're at an un-authenticated page and you decided -- hey, I'd like to log in now --, you won't have that lovely session to send you back to where you were at. In this case, I tend to pass the current path in the login link. <%= linkto 'login', loginpath(:to => request.request_uri) %> gives a url like /login?to=/foo/bar.

  18. wuputah2007-03-30 19:19:17

    Hey Josh - I was trying to use this in a slightly different manner and found that it was basically impossible to revert to default behavior on a controller-by-controller basis, e.g.:

    class ApplicationController < ActionController::Base
      session :new_session => false
    end
    
    class SomeOtherController < ApplicationController
      session :new_session => nil # ???
    end
    

    Unfortunately, the way cgi/session.rb works, if there is any value in the hash it tries to use that, even if its nil. It must be deleted out of the hash to work properly - thus, the only way I was able to get this to work was to add this to my ApplicationController:

      def self.session_options_for(request, action)
        options = super
        if options.has_key?(:new_session) && options[:new_session].nil?
          options.delete(:new_session)
        end
        options
      end
    

    I'm not sure if this is the Right Wayâ„¢ to do this but getting this patched up sometime would be great.

Sorry, comments for this article are closed.