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.
Now you just need to revive the “Sign my guestbook” trend. That was hot.
you just did. i think.
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..
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.
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?
Ok, I got it now. Sorry.
Am I the first to spot the “htmlgoodies.com” reference? >:)
I was hoping someone would notice that Lamby :-)
Wee… added to my blog. :-)
“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?
bjc: PJ is making sure the id is an int.
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!
Chime in.