Err the Blog Atom Feed Icon
Err the Blog
Rubyisms and Railities
  • “So, You Want Your Own Counter, Huh?”
    – PJ on August 05, 2007

    Counters Have you ever caught yourself daydreaming about the good ol’ days. You know, like 1995. A time where 28.8k modems and CGI scripts were all anyone needed in life.

    Now we’re flooded with garbage like DSL and Ruby on Rails, ugh. Those lost in this new age, fear not, for Err the Blog has rewound the clock for you. May I introduce to you our time machine, ErrCount. Cleverly named, I know. It’s that self-deprecating wit that makes us so lovable, right?

    These folks and I are revolutionizing the internet by bringing back CGI-style hit counters. Get on board while you still can.

    Ok, fine, it’s a Rails app. A single-model app that consists of three views and a Mongrel handler that, uh, handles the counting.

    How ErrCount Handles It

    I used a handler specifically because of speed. Rails would have done the job just fine, but when our thousands of loyal readers start using the site to track millions of hits, the overhead of the full Rails’ stack would have slowed request times dramatically.

    Its structure looks like this:

    class Counter < Mongrel::HttpHandler
      def process(request, response)
        response.start(200) do |head, out|
          ...
        end
      end
    end
    uri "/ctr", :handler => Counter.new, :in_front => true
    

    So, what do we have? A Counter class that inherits Mongrel::HttpHandler which gives us a bunch of handy tools, including the process method that handles the requests coming in, and a uri method that defines which urls are gonna make the magic happen. The :in_front option is important as well, so the Rails dispatcher won’t slow down handle the request.

    Lets look at what’s happening inside of ErrCount’s response block, because I’m pretty sure you came here to see real code.

    First thing’s first, I grab the site id from the url and select its corresponding row from the database using ActiveRecord. I could have used the mysql gem directly, but since ActiveRecord is loaded anyways, I might as well use it.

    id = request.params["PATH_INFO"][/\d+/].to_i
    row = ActiveRecord::Base.connection.select_one
      "select url, hits from sites where id = #{id}"
    

    There may be a better way to get the id than the way I’ve done it, but I wanted to support urls that looked like /ctr/224.js instead of /ctr?id=224.

    Next up is a small check to make sure the counter is installed on the correct site. If it is, we bump the hit count by one with an atomic update. Theoretically, I don’t need a mutex around these AR calls, but I’m sure somebody is going to tell me I’m wrong. (Please do if I am)

    if Regexp.new(row["url"]) =~ request.params["HTTP_REFERER"]
      ActiveRecord::Base.connection.update
        "update sites set hits = hits + 1 where id = #{id}"
    end
    

    The meat and potatoes is building the counter image. Using a little CSS trickery, I’m able to reuse the same counter image

    by using divs with different background positions for each number to create something that looks like:

    counter = "var counter='<style>#{Style}</style>"
    row["hits"].to_s.split(//).each do |num|
      counter += %{<div class="ctr" style="background-position:}
      counter += %{#{150 - (15 * num.to_i)}px 0;"></div>}
    end
    counter += ";document.write(counter);"
    

    All that’s left is serving the javascript with the proper header.

    head["Content-Type"] = "text/javascript"
    out.write counter
    

    Something that’s easy to forget should you chose to write your own is to start mongrel with a -S so it knows about the handler you just wrote:

    mongrel_rails start -S counter.rb

    That’s all there is to it, feel free to check out the entire app here:

    Warehouse: http://projects.require.errtheblog.com/browser/counter

    SVN: svn co svn://errtheblog.com/svn/projects/counter

    Site: http://errcount.com

    How Sake Handles It

    But, you’re not limited to just using Mongrel handlers with Rails apps. You can use these things on your own and that’s just what Chris did with Sake. Check it:

    require 'sake' unless defined? Sake
    require 'mongrel'
    
    class Sake
      module Server
        extend self
    
        def start(args)
          if index = args.index('-p')
            port = args[index+1].to_i
          else
            port = 4567
          end
    
          daemoned = args.include? '-d'
    
          config = Mongrel::Configurator.new :host => "127.0.0.1" do
            daemonize(:cwd => '.', :log_file => 'sake.log') if daemoned
            listener(:port => port) { uri "/", :handler => Handler.new }
          end
    
          trap("INT") { config.stop }
    
          config.run
          unless daemoned
            puts "# Serving warm sake tasks on port #{port}..."
          end
          config.join
        end
    
        class Handler < Mongrel::HttpHandler
          def process(request, response)
            uri    = request.params['PATH_INFO'].sub(/^\//, '')
            status = uri.empty? ? 200 : 404
            body   = status == 200 ? Store.to_ruby : 'Not Found'
    
            response.start(status) do |headers, output|
              headers['Content-Type'] = 'text/plain'
              output.write body
            end
          end
        end
      end
    end
    
    Sake::Server.start(ARGV) if $0 == __FILE__
    

    The only major difference is he’s using Mongrel::Configurator directly so he could daemonize it and define when the server should start and stop. Other than that, you use Mongrel handlers the exact same way as you would when using them with Rails.

    Check out the code, along with the rest of sake here:

    Warehouse: http://projects.require.errtheblog.com/browser/sake

    SVN: svn co svn://errtheblog.com/svn/projects/sake

    Post: http://errtheblog.com/post/6069

    Happy Handling.

  • Andy, about 13 hours later:

    Now you just need to revive the “Sign my guestbook” trend. That was hot.

  • kit, about 15 hours later:

    you just did. i think.

  • Pratik, about 15 hours later:

    Heh, I was about to blabber about Mutex after looking at the code, but then read your post.

    One thing you did miss though, was adding mongrel handler in mongrel_cluster.yml. I guess you’ll need to create a config script for that. Right ?

    Something like http://pastie.caboo.se/85264 may be..

  • Ismael, about 20 hours later:

    This is so cool! (of coursr, “cool” being a relative term). I’m thinking what kind of little fun handler can I create with this.

  • Ismael, about 20 hours later:

    So… This might be a stupid question, but where in the app are you requiring the “counter.rb” file in order to register the handler?

  • Ismael, about 21 hours later:

    Ok, I got it now. Sorry.

  • Lamby, about 22 hours later:

    Am I the first to spot the “htmlgoodies.com” reference? >:)

  • PJ, 1 day later:

    I was hoping someone would notice that Lamby :-)

  • Robby Russell, 1 day later:

    Wee… added to my blog. :-)

  • bjc, 3 days later:

    “update sites set hits = hits + 1 where id = #{id}”

    SQL Injection attacks ftw? Or does Rails do some magic here that prevents the id from being anything but a number under all cases?

    Wouldn’t it still be better to quote the id, just to be safe? Can you not use bindparams via ActiveRecord#connection, or something?

  • Chris, 3 days later:

    bjc: PJ is making sure the id is an int.

    <pre>
    id = request.params["PATH_INFO"][/\d+/].to_i
    </pre>
  • matzbot, 14 days later:

  • ryan, about 1 month later:

    Could have also called it ‘CountErr’ I suppose eh? :P

    Love the blog! Been lurking for a while at work, going into my feed reader tonight. Keep it up!

  • Thirteen people have commented.
    Chime in.
    Sorry, no more comments :(
This is Err, the weblog of PJ Hyett and Chris Wanstrath.
All original content copyright ©2006-2008 the aforementioned.