nuthole.com "reverse engines, you nut!"
contact:email
subscribe:feed
compute / rails
Powered by Blosxom
Get Firefox!
geourl
subgenius
spampoison hosting rails

Ruby metaprogramming: Not scary at all

posted by jack at 08:17 CET in / compute / rails feed

There seems to be a sort of mystique around the concept of metaprogramming, but in Ruby it's really not mystical at all. It's all about leveraging a few existing methods in smart ways to let you eliminate boring code. This sort of thing is possible in my "native language" of Objective-C, but in Ruby it turns out to be even easier.

Today I'm going to talk about how to use the method_missing method to eliminate repetitive boilerplate code. method_missing is Ruby's fallback strategy for unresolvable method names encountered at runtime. If you try to call a non-existent method on any object, the Ruby runtime will instead call method_missing on that object; The default behavior of this method is to raise an exception, but we can override it to do smart things based on the method name.

The Badness

The Rails application I'm working on has a role-based permissions scheme, with a ManagementRole model that sits between User and Program models, allowing us to define roles that a User can have relative to a given Program, which in turn constrain what they're allowed to do with the application. These roles are expressed concretely as simple subclasses of ManagementRole, with names like RoleSearchMembers and RoleCreateMemberships. Up until recently, checking whether a User were allowed to perform certain tasks within a Program consisted of calling one of several nearly-identical class methods defined on ManagementRole, such as these:


  def self.allow_search_members?(user, program)
    return User.is_administrator?(user) ||
      self.program_role_exists?(user, program, "RoleSearchMembers")
  end
  
  def self.allow_create_memberships?(user, program)
    return User.is_administrator?(user) ||
      self.program_role_exists?(user, program, "RoleCreateMemberships")
  end

  # etc
  
  # the program_role_exists? method, not listed here, simply checks
  # for the existence of a matching ManagementRole object

As you can see, this is extremely repetitive, and adding new types of permissions means adding new, nearly-identical methods. As of now there are 18 methods like this, and that number will just go up in the future, leading to even more boilerplate code. Where will this madness end?

The Goodness

Providing a simple definition for method_missing in the same class lets me eliminate all those 18 methods, and will automatically deal with any new ManagementRole subclasses. It looks like this:


  # instead of a bunch of allow_xxx_xxx_xxx? methods that differ only slightly, 
  # we just catch all such calls here.
  def self.method_missing(method_id, *args)
    if match = /allow_([_a-zA-Z]\w*)\?/.match(method_id.to_s) and args.size==2
      return User.is_administrator?(args[0]) || 
        self.program_role_exists?(args[0], args[1], "Role#{match[1].camelize}")
    else
      super
    end
  end

The first thing this does is examine the method and the argument list, to determine if it looks like the kind of method we are trying to replace. If the method name looks anything like "allow_xxx_yyy_zzz?", and if there are exactly 2 arguments, then we'll do something interesting. Otherwise, we'll just call super and let the parent class deal with it.

In the "interesting" case, we simply perform the same checks we did before to determine whether we return true or false, but now we're using indexed values from the args array, and constructing the string naming the MangementRole subclass by using the result of the earlier regexp match. Voila!

The Even-Betterness

Of course, software development being what it is, any time you revisit an old design to try to improve it, the act of improving it can lead you to discover new, further improvements. While writing this, I realized something that would have avoided all this boilerplate code, and the metaprogramming it led to, in the first place: Instead of asking the ManagementRole class explicitly about each of these permissions, I'll be better off writing a single method called allow? in the ManagementRole class, which will do a lookup based on the recipient's class-name, e.g. RoleSearchMembers, RoleCreateMemberships. And then instead of calling ManagementRole.allow_search_members?(u,p) , I will call RoleSearchMembers.allow?(u,p). That will lead to even less code than the metaprogramming version, and less code is always better code. I haven't written it yet, but it'll probably look something like this:


  def self.allow?(user, program)
    return User.is_administrator?(user) ||
      self.find_by_user_and_program(user, program)
  end

So this problem, with its metaprogramming-to-the-rescue solution, turned out to be easily solvable with more traditional object-orientation, but that's OK! Hopefully this post will be helpful anyway, for someone else whose problem doesn't have the same easy solution that mine turned out to have...

permalink | digg this | slashdot this | add to del.icio.us | 0 comments (why don't you write the first?)

RailsConf Europe notes

posted by jack at 13:15 CET in / compute / rails feed

I attended RailsConf Europe 2007 in Berlin last week. This has already been blogged to death I suppose, but here's my take on a few items:

Bratwurst on Rails
The local Berlin ruby brigade organized this event; The night before the conference began, over 400 rails nerds gathered to eat freshly-made sausage and drink beer. This was a great way to start things off, I had lots of interesting conversations with a variety of people.
Networking, in general
All throughought the 3+ days I was there, I had the opportunity to meet and speak to so many people doing so many interesting things. Anywhere you turned, you could find people ready and willing to strike up a conversation about rails, programming in general, business strategies, or anything else you might think of. This was just a great, friendly crowd to hang out with. Extra props to Thoughtworks for picking up the tab for everyone to get a beer or two at the hotel bar one evening!
JRuby
There was lots of emphasis on JRuby. Thoughtworks and others are pushing JRuby on Rails as an easy way to get Rails into the enterprise, since you can deploy on any standard Java app server; Sun includes support for JRuby (as well as normal Ruby) in NetBeans; and Ola Bini's new book on the topic has just hit the shelves, and there's a lot of interest and potential in JRuby overall. But at the same time, I'm somewhat skeptical of Sun's involvement. It seems great that they're embracing JRuby, but hopefully they won't somehow turn it into the sort of bloated mess that Java has become. And frankly, I'm not convinced that the Java app server stack isn't a part of the IT bloatware beast that Thoughtworks' own Cyndi Mitchell was railing against during her short talk.
RejectConf
This was a hoot. The idea is that any presentation that had been rejected by the conference organizers, plus anything else that anyone wanted to present, could be shown in a severely time-limited format (20 seconds per slide). Lots of interesting presentations, in a short time and fun, friendly atmosphere. Unfortunately the venue was a little too small, it was tough to get close enough to be able to see and hear much, but hey.
Australians
Throughout the conference (including RejectConf), the presentations that were the most consistently funny and entertaining, while remaining informative and valuable, were almost always led by Australians. Make of that what you will.
The occasional bad presentation
I'm not going to name names here. But one presentation, held on the final day, was really a waste. The guy started off by telling us that some time after his topic was approved for the conference, further development of the topic revealed to him that what he was going to present really wasn't working out that well in practice. Now this could have been interesting in and of itself, to see in detail how a seemingly-sound concept breaks down when pushed a bit farther. Instead of that, however, the audience were treated to a lot of hand-waving and disclaimers of "I'm not going to show you the code because it won't work for you anyway". When there was still half an hour of time left, he was completely out of material and stopped; At that point, at least a third of the audience had already given up and left.
DHH Keynote
I don't think I'm going out on a limb if I say that DHH's public speaking is known for balancing a fine line between self-confidence and arrogance. I think this one was edging towards the latter. Sure, it's great to hear that Rails 2.0 is approaching quickly, etc, but the part of the speech where he was proclaiming that the Rails revolution is already over, we've won, and now we can rest on our laurels and "enjoy Rails"? I think he's putting the cart before the horse. Maybe DHH is at this point surrounded by so many yes-men that to him, Rails seems like it's truly the only game in town, but in reality it can still go so much farther, be applied to so many areas, that it strikes me as premature to proclaim victory. In some places and markets, Rails is still a very difficult sell; In Stockholm where I live, for example, as far as I can tell most of the big consulting firms that have dipped a toe into Rails development have had a really hard time making inroads into the Swedish enterprise market.
permalink | digg this | slashdot this | add to del.icio.us | 1 comment (add more!)

Installing RMagick on Mac OS X and Ubuntu

posted by jack at 14:22 CET in / compute / rails feed

I haven't done anything with RMagick at all, partly because it relies on ImageMagick which many consider difficult to install on Mac OS X. Lately I've had a reason to use the excellent Gruff charting library, so it was time to buckle down and install this stuff.

As it turns out, it's really not as complicated as some people say. Assuming you've already got macports and rubygems installed, it basically comes down to this:

sudo port install freetype
sudo port install ghostscript
sudo port install imagemagick
#sudo port install graphicsmagick
sudo gem install rmagick

Somewhere I got the idea to also install graphicsmagick after imagemagick, but for some reason port doesn't seem to be able to find the package; It tries a few dozen sites and fails, so I commented it out above, but feel free to try it yourself. But no matter, it seems to not be necessary; I can now create images, render text, write them to disk, etc.

On Ubuntu (6.10), the incantation is different, but even simpler (via EXPRESSICA:

sudo apt-get install imagemagick
sudo apt-get install libmagick9-dev ruby1.8-dev
sudo gem install rmagick

The only caveat is that if you're running without swap space and have less than a couple hundred megs of free RAM (e.g. on a VPS server) you may need to stop some of your processes before attempting that last line, or you'll get this not-so-helpful set of errors after it runs for a while, consumes all available RAM, and subsequently crashes:

Building native extensions.  This could take a while...
ERROR:  While executing gem ... (Gem::Installer::ExtensionBuildError)
    ERROR: Failed to build gem native extension.
Hopefully this will save someone a few hours.
permalink | digg this | slashdot this | add to del.icio.us | 0 comments (why don't you write the first?)

My first home-made rake task

posted by jack at 12:53 CET in / compute / rails feed

I wanted to be able to examine my development log for rails deprecation warnings, and came up with this handy one-liner:

grep "^DEPRECATION WARNING:" development.log | awk '{print $3, $4, $5, ":", $NF}' | sort | uniq

This gives me output something like this:

@params is deprecated! : script/../config/../app/views/waiting_messages/prepare_reply.rhtml:9)
@request is deprecated! : ./script/../config/../app/controllers/account_controller.rb:10)
You called render('new_member_in_program'), : /opt/local/lib/ruby/gems/1.8/gems/actionpack-1.13.3/li [...]
end_form_tag is deprecated : script/../config/../app/views/members/bulknew.rhtml:10)
find_all is deprecated : (irb):3)
start_form_tag is deprecated : script/../config/../app/views/members/bulknew.rhtml:2)

It's not perfect; I'm missing some useful info from some of the lines (such as the 'You called render...') that I'll have to go back and pick over. But in broad strokes, it gives me a fairly concise list of some things that need to be cleaned up.

However, that one-liner is fairly meaty. Who wants to remember that, or make an alias for it, or need to look it up? Yuck.

How about a rake task? I've never written one before, but everyone's always raving about how simple it is, so I figured I'd go ahead. Hopefully this helps show how simple it is. While I'm at it, I'll go ahead and include the full text of the warning (except for the redundant "DEPRECATION WARNING" at the beginning, since we already know we're looking for that) instead of truncating it like I did in my awk-based one-liner.

Here it is:


namespace :utils do
  desc "List deprecation warnings in development log"
  task :find_deprecated do
    File.open('log/development.log') do |file|
      lines = file.select {|line| line =~ /^DEPRECATION WARNING:/}
      lines.sort.uniq.each {|line| puts line.split('DEPRECATION WARNING: ')[1]}
    end
  end
end

Put that in a file called utils.rake inside your lib/tasks directory, and call it by typing rake utils:find_deprecated, and you'll get something like this (lines chomped for presentation purposes):


@params is deprecated! Call params.[] instead of @params.[]. Args: [:response_text]  See http://www. [...]
@request is deprecated! Call request.accepts instead of @request.accepts. Args: []  See http://www.r [...]
You called render('new_member_in_program'), which is a deprecated API call. Instead you use render : [...]
end_form_tag is deprecated and will be removed from Rails 2.0  See http://www.rubyonrails.org/deprec [...]
find_all is deprecated and will be removed from Rails 2.0 (use find(:all, ...))  See http://www.ruby [...]
start_form_tag is deprecated and will be removed from Rails 2.0 (use form_tag instead)  See http://w [...]

There are certainly improvements that could be made, such as adding the ability to search test.log and production.log as well, but this'll do for now.

permalink | digg this | slashdot this | add to del.icio.us | 0 comments (why don't you write the first?)

Introducing RUMP

posted by jack at 21:23 CET in / compute / rails feed

Many of you have heard of web application deployment using the LAMP set of technologies that came into vogue a few years ago. Later, MARS was thrown about as a four-letter acronym to describe a Rails-centric deployment scheme, but some of the choices (Solaris? Apache? Please.) leave me cold.

So, without further ado, let me introduce you to RUMP:

  • Rails
  • Ubuntu
  • Mongrel
  • Postgresql

Why these choices? Well, Rails goes without saying (if you aren't interested in Rails, then RUMP is not for you); Ubuntu because it offers a lightweight server config with a package system that is even better than Debian's; Mongrel because it's currently the best web server targetted at Ruby application deployment and is getting better all the time; and Postgresql because MySQL, despite all the good it's done for the world, still falls behind on a number of features.

To help get you started, what follows is a reasonably accurate description of what I went through to get my RUMP in gear. I had occasion to do this a couple times within a few days a little while ago, and kept some reasonably complete notes as I went along. Hopefully this will help others experience the satisfaction of a nice, fit RUMP.

Wherever possible, I've tried to find the simplest configuration that works. Of course each component can be installed and configured in a variety of ways, and YMMV. For what it's worth, the following is what seems to be working well for me.

Installation notes for RUMP deployment server (running ubuntu 6.10 "edgy eft")

I started off with an empty image installed by my hosting company of choice. This was a bare-bones Ubuntu server install, containing a single root account with ssh enabled, and pretty much nothing else.

First, log in to your new system and use adduser to create a user named "deploy" with homedir /home/deploy, and then run visudo to let the "deploy" user run commands with sudo. Then log in as "deploy" and do the rest of this.

Start off with editing /etc/apt/sources.list to enable some packages:

deb http://se.archive.ubuntu.com/ubuntu/ edgy main restricted
deb-src http://se.archive.ubuntu.com/ubuntu/ edgy main restricted

## Major bug fix updates produced after the final release of the
## distribution.
deb http://se.archive.ubuntu.com/ubuntu/ edgy-updates main restricted
deb-src http://se.archive.ubuntu.com/ubuntu/ edgy-updates main restricted

## Uncomment the following two lines to add software from the 'universe'
## repository.
## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu
## team, and may not be under a free licence. Please satisfy yourself as to
## your rights to use the software. Also, please note that software in
## universe WILL NOT receive any review or updates from the Ubuntu security
## team.
 deb http://se.archive.ubuntu.com/ubuntu/ edgy universe
# deb-src http://se.archive.ubuntu.com/ubuntu/ edgy universe

## Uncomment the following two lines to add software from the 'backports'
## repository.
## N.B. software from this repository may not have been tested as
## extensively as that contained in the main release, although it includes
## newer versions of some applications which may provide useful features.
## Also, please note that software in backports WILL NOT receive any review
## or updates from the Ubuntu security team.
 deb http://se.archive.ubuntu.com/ubuntu/ edgy-backports main restricted universe multiverse
# deb-src http://se.archive.ubuntu.com/ubuntu/ edgy-backports main restricted universe multiverse


deb http://security.ubuntu.com/ubuntu edgy-security main restricted
deb-src http://security.ubuntu.com/ubuntu edgy-security main restricted
# deb http://security.ubuntu.com/ubuntu edgy-security universe
# deb-src http://security.ubuntu.com/ubuntu edgy-security universe

After that, run sudo apt-get update to get the latest package definitions, and then start installing:

Some common tools you will surely need at some point:

sudo apt-get install man curl wget

and some dev tools for later compilation steps

sudo aptitude install build-essential

Time for ruby:

sudo apt-get install ruby ri rdoc irb ri1.8 ruby1.8-dev libzlib-ruby zlib1g

and then postgres:

sudo apt-get install postgresql-8.2
sudo apt-get install postgres82-dev

Configuring Postgres

Now we'll configure postgres for our purposes. Creating a role with the same name as the user we're running with makes everything a little easier:

sudo -u postgres createdb NAME_OF_YOUR_APP_HERE_production
sudo -u postgres createuser -P
Enter name of role to add: deploy
Enter password for new role: 
Enter it again: 
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) y
Shall the new role be allowed to create more new roles? (y/n) n
after that you should be able to do execute psql NAME_OF_YOUR_APP_HERE_production as the deploy user and get right in.

Pound this.

One piece of the puzzle that didn't quite make it into the RUMP moniker is pound, a handy reverse-DNS and load-balancer that fronts for our multiple mongrels. Install it thusly:

sudo apt-get install pound
sudo vi /etc/default/pound

setting startup=1 in there, and then:

sudo vi /etc/pound/pound.cfg

making it something like this:

## Minimal sample pound.cfg
##
## see pound(8) for details


######################################################################
## global options:

User            "www-data"
Group           "www-data"
#RootJail        /chroot/pound

## allow PUT and DELETE also (by default only GET, POST and HEAD)?:
#ExtendedHTTP    0

#WebDAV          0

## Logging: (goes to syslog by default)
##      0       no logging
##      1       normal
##      2       extended
##      3       Apache-style (common log format)
LogLevel        1

## check backend every X secs:
Alive           30

## use hardware-accelleration card supported by openssl(1):
#SSLEngine       <hw>


######################################################################
## listen, redirect and ... to:

## redirect all requests on port 8888 ("ListenHTTP") to the local webserver see "UrlGroup" below):
#ListenHTTP 127.0.0.1,8080

##
#UrlGroup ".*"
#BackEnd 127.0.0.1,80,1
#EndGroup

ListenHTTP
  Address 0.0.0.0
  Port    80
  Service
    BackEnd
      Address 127.0.0.1
      Port    8000
    End
    BackEnd
      Address 127.0.0.1
      Port    8001
    End
    BackEnd
      Address 127.0.0.1
      Port    8002
    End
    Session
      Type COOKIE
      TTL 900
      ID "_myappname_session_id"
    End
  End
End

That sets us up with pound listening on port 80, transferring the request to one of our three mongrel instances (see Mongrel section). Note the Session section, which may help keep things sorted out for some Rails session schemes.

Now start pound, like this:

sudo /etc/init.d/pound start

Try to connect to the site with a browser. you should get a 503 error, and something like this in /var/log/syslog:

Mar 15 23:51:31 ubuntu pound: backend 127.0.0.1:8000 connect: Connection refused
Mar 15 23:51:31 ubuntu pound: backend 127.0.0.1:8001 connect: Connection refused
Mar 15 23:51:31 ubuntu pound: backend 127.0.0.1:8002 connect: Connection refused
Mar 15 23:51:31 ubuntu pound: no back-end "GET / HTTP/1.1" from 123.123.123.123
Mar 15 23:51:31 ubuntu pound: no back-end "GET /favicon.ico HTTP/1.1" from 123.123.123.123

Subversion

If you want to run subversion on this server, here's how to do it. If you are running subversion elsewhere and don't want to move it, skip this section.

sudo apt-get install subversion xinetd

Create a user that will own the repositories:

sudo adduser --system --no-create-home --home /var/svn --group --disabled-login svn

Create the directory that will hold the repositories:

sudo mkdir /var/svn
sudo chown svn:svn /var/svn

Create file /etc/xinetd.d/svnserve with the following content, to autolaunch svn when it's called upon:

service svn
{
       port = 3690
       socket_type = stream
       protocol = tcp
       wait = no
       user = svn
       server = /usr/bin/svnserve
       server_args = -i -r /var/svn
}

Restart xinetd so that svn will work

sudo /etc/init.d/xinetd restart

Create a test repository:

sudo -u svn svnadmin create /var/svn/testrepo

It should be possible to check out the repository and work on it:

svn co svn://localhost/testrepo

Assuming that worked, create the real repository for your project:

sudo -u svn svnadmin create /var/svn/myproject

Add a little security:

cd /var/svn/myproject/conf
sudo vi svnserve.conf

Set up like this:

    anon-access = none
    auth-access = write
    password-db = passwd

Create users for your remote username(s), and the local deploy user. This simplifies login for everyone, and makes it easier to spot changes that were committed from the production server! sudo vi passwd, making it look about like this:

    # create a user/pw or two
    joe_user = superseekrit
    deploy = soopersecret

Now try to check out, it should demand a valid username/password:

svn co svn://localhost/myproject

Assuming you have an existing repository you'd like to bring over, go to the old server and do this:

svnadmin dump /path/to/repos > my_repos.dump
gzip my_repos.dump
scp my_repos.dump.gz deploy@my_ubuntu_host:/home/deploy

Then, on the new machine:

cd /home/deploy
gunzip my_repos.dump.gz
cd /var/svn
sudo -u svn svnadmin load myproject < /home/deploy/my_repos.dump 

Then make sure you can check it out:

cd /home/deploy
# delete the myproject directory if it's there, then:
svn co svn://localhost/myproject

Wrapping it up

Finally it's time to install rails etc. I've installed them with rubygems:

curl -O http://rubyforge.org/frs/download.php/17190/rubygems-0.9.2.tgz
tar xzf rubygems-0.9.2.tgz ; cd rubygems-0.9.2
sudo ruby setup.rb
sudo gem update
sudo gem install rails --include-dependencies
sudo gem install postgres-pr
sudo gem install mongrel

(ignore warnings about not being able to compile fastthread stuff, it's optional)

sudo gem install mongrel_cluster

If you're using BackgroundRb, you'll want these:

sudo gem install slave
sudo gem install daemons

Very soon we'll have our app up and running. Create the database schema:

cd ~/myproject
rake db:schema:load

Configure a little mongrel cluster:

cd ~/myproject
mongrel_rails cluster::configure -p 8000 -N 3

As of this writing (May 2007), there's a small bug that will bite you if you're using both postgres and BackgroundRb. This is fixed in edge rails, but not yet in the released version. If you fall into this category, patch this file, as shown in http://dev.rubyonrails.org/attachment/ticket/7727/postgres_async_patch.diff:

sudo vi /usr/lib/ruby/gems/1.8/gems/activerecord-1.15.2/lib/active_record/connection_adapters/postgresql_adapter.rb 

Make sure you're always running in production on this machine by putting the following line into your .bashrc:

export RAILS_ENV=production

If you're using BackgroundRb, start the background processes:

cd ~/myproject
script/backgroundrb start

Tail log/backgroundrb.log to make sure it looks OK. Something like:

	20070316-13:56:35 (12680) Starting WorkerLogger
	20070316-13:56:35 (12681) In ResultsWorker
	...

At long last, start the mongrel cluster:

cd ~/myproject
mongrel_rails cluster::start

At this point you should be able to smack your RUMP on port 80 and watch it wiggle!


In no particular order, here are some of the sites where I found information to put this all together:
http://wiki.rubyonrails.org/rails/pages/PostgreSQL/
http://happycodr.com/blog/view/13-putting-a-ruby-on-rails-application-into-production
http://mongrel.rubyforge.org/docs/pound.html
http://www.die.net/doc/linux/man/man8/pound.8.html
http://kurup.org/blog/tag/pound
http://ubuntuguide.org/wiki/Ubuntu_Edgy#How_to_install_Subversion_version_control_server_.28svnserve.29
http://blog.tupleshop.com/2006/7/8/deploying-rails-with-pound-in-front-of-mongrel-lighttpd-and-apache

That's it, I guess. I hope this proves helpful to someone. Feel free to leave feedback in the comments below!

permalink | digg this | slashdot this | add to del.icio.us | 2 comments



Looking for programming talent that doesn't make you say "WTF!"? Try the hidden network.