Peepcode Code Review
Peepcode Code Review
Ruby on Rails
Code
Review
Improve the quality of your code
by Geoffrey Grosenbach
contents
I've read through the code of both proprietary Rails applications and
open source projects. All the concepts you see here came from real-
world examples (but variable names have been changed to protect
the innocent). Some of the examples, I'm afraid to admit, came from
my own code.
The good news is that most of these tips are easy to implement.
You'll get into the habit of switching your timezone to UTC, setting
up database tables for sessions, and installing the exception_noti-
fier every time you write a new Rails application. Once you've been
developing an application for a while, you'll look forward to running it
against ruby-prof or setting up good monitoring tools.
3
Store Sessions in
the Database
chapter 1
First, generate the migration to build the sessions table in the data-
base.
4
ruby script/generate session_migration
rake db:migrate
For maintenance, the following tasks are useful. Create a file in lib/
tasks/sessions.rake with these contents:
namespace :sessions do
desc "Count database sessions"
task :count => :environment do
count = CGI::Session::ActiveRecordStore::Session.count
puts "Sessions stored: #{count}"
end
Run rake sessions:count to find out how many sessions are being
stored in the database. Run rake sessions:prune to clear out ses-
sions older than two weeks.
It's a good idea to run the prune task regularly. Set a daily cron job
to run this command and your sessions table will be in good shape:
6
Use Custom
Configuration Files
chapter 2
• Your secret keys go everywhere your code does. If you have sub-
contractors working on your code, they will have access to your
secret information.
• They are not relevant to the environment. If you need to use dif-
ferent credentials from your staging server or development envi-
ronment, you'll have to add logic into environment.rb or scatter
it again in config/environments/development.rb and its counter-
7
parts. The environment files often include non-secret settings that
really do need to go everywhere your code does, so this isn't a
complete solution.
development: &non_production_settings
amazon_api:
key: 1234567890
secret: abcdefg
flickr_api:
key: 1234567890
mailer:
server: mail.example.com
username: bubba
password: s0meth1ngs3cr3t
test:
<<: *non_production_settings
production:
amazon_api:
key: 9999999999
secret: aaaaaaa
flickr_api:
key: 222222222
mailer:
server: mail.example.com
username: bart
password: 1c2r3c4g,8,938,7
8
None of these values are required, so you can create whichever ones
you need. It's best to keep them consistent across the different envi-
ronments. You can also use the ampersand trick, but add other val-
ues for the same environment, which will override duplicated settings.
APP_CONFIG['flickr_api']['key']
You can use this to store API keys, mail server settings, or any other
kind of configuration data that your application needs.
task :copy_config_files do
filename = "config/config.yml"
run "cp #{shared_path}/#{filename} #{release_
path}/#{filename}"
end
With this kind of setup, you can safely store a dummy copy of con-
fig.yml in your repository. The copy in the repository should not
9
contain the production passwords, but can contain sandbox-related
passwords needed for development.
resources
• Using OpenStruct for easier key access (http://kpumuk.info/ruby-on-rails/
flexible-application-configuration-in-ruby-on-rails)
• The Ampersand trick in YAML files (http://blog.bleything.net/2006/06/27/
dry-out-your-database-yml)
10
Use Constants for
Repeated Strings
chapter 3
include ActionController::UrlWriter
# The wrong way
default_url_options[:host] = 'peepcode.com'
11
Using a hard-coded string here makes the emails unusable when
sent from the development environment, since they point at the pro-
duction server.
# In environments/development.rb
APP_DOMAIN = 'localhost:3000'
# In environments/staging.rb
APP_DOMAIN = 'staging.peepcode.com'
# In environments/production.rb
APP_DOMAIN = 'peepcode.com'
Then reuse the constant in the rest of your code. For the example
shown earlier, it would look like this:
default_url_options[:host] = APP_DOMAIN
resources
• AppConfig plugin (http://agilewebdevelopment.com/plugins/app_config)
13
Keep Time in UTC
chapter 4
By default, Rails stores all these times in the local time zone of the
database server. This makes no sense since web servers are often in
a different time zone than the developer. Visitors are almost always
in a different time zone than the web server.
Rails::Initializer.run do |config|
# Make Active Record use UTC-base instead of local time
config.active_record.default_timezone = :utc
14
end
Now the possibilities are endless! Show local times with the help of
client side Javascript, or use Jamis Buck's TzTime plugin to calculate
dates on the server with Ruby (requires Rails 2.0 edge).
resources
• UTC (http://en.wikipedia.org/wiki/Coordinated_Universal_Time)
• Showing Perfect Time with Javascript (http://redhanded.hobix.com/inspect/
showingPerfectTime.html)
• Jamis Buck's TzTime plugin (http://weblog.jamisbuck.org/2007/2/2/introduc-
ing-tztime)
• PostgreSQL timestamp with time zone (http://www.postgresql.org/
docs/8.0/interactive/functions-datetime.html)
• PostgreSQL timezones (http://www.postgresql.org/docs/7.2/static/timezones.
html)
• MySQL CONVERT_TZ (http://dev.mysql.com/doc/refman/5.0/en/time-zone-
support.html)
15
Don't Loop Around
ActiveRecord
chapter 5
@all_stars = []
@league.teams.each do |team|
team.players.each do |player|
@all_stars << player if player.all_star?
end
end
@all_stars
In this case there were only a few teams, but 5 queries were gen-
erated. A league with more teams, more players, or further nest-
ing could easily result in dozens of SQL queries and a very slow
response.
@all_stars = []
@league.teams.each do |team|
team.players.each do |player|
@all_stars << player if player.all_star?
end
end
@all_stars
17
:include Options Use the singular farm to include a join from the Chicken
model back to the Farm model.
The include argument is very powerful but can also be
Chicken.find(:all, :include => :farm)
confusing. Here are a few issues to be aware of.
If you wanted to get all the farms and all their chickens,
all the way down
you would use the plural chickens because that is how it is
used in the has_many statement in the model: You can go even further (within reason). We could retrieve
all the farms, all their cows and chickens, and each of the
Farm.find(:all, :include => :chickens) chickens' eggs.
Farm.find(:all, {
In reverse, you would use the singular. Chicken belongs to :include => [
the singular farm: :cows,
{:chickens => :eggs}
]
class Chicken < ActiveRecord::Base
})
belongs_to :farm
has_many :eggs
end
18
AS t1_r0, teams."name" AS t1_r1, teams."league_id" AS
t1_r2, players."id" AS t2_r0, players."name" AS t2_r1,
players."team_id" AS t2_r2, players."all_star" AS t2_r3 FROM
leagues LEFT OUTER JOIN teams ON teams.league_id = leagues.
id LEFT OUTER JOIN players ON players.team_id = teams.id
WHERE (leagues."name" = 'Northwest')
Even better, there will never be more than one query, no matter how
many rows are in the database.
But this isn't the best way. We're still using Ruby to filter the records,
which can be slow. We're still returning more records than we need,
which results in extra traffic between your application server and your
database server.
The log here is still only one query, but now the database is doing
the work of filtering the players, which will usually be much faster
than asking Ruby to do it.
resources
• RailsCasts on include (http://railscasts.com/episodes/22)
• Include by Default plugin (http://blog.jcoglan.com/includebydefault)
20
:select and :include use. To be honest, I always had to look it up online even
before I had access to a nice ORM library.
There is a tricky interaction between the :select and
:include options. If you are constructing special queries for My modus operandi is to type my query into the Rails con-
reports, you'll need to understand how they work together. sole and look at the output in the development log. I can
then copy and paste the proper join string back into my
code in the right place.
:include clobbers :select
LEFT OUTER JOIN chickens ON chickens.farm_id =
If a query contains an :include directive, any :select farms.id
options will be ignored. For example you might expect this
to return an array of ids only. Not so!
Farm.find(:all, {
:select => "farms.id, chickens.id",
:join => "LEFT JOIN ..."
})
21
Beware of Binary Fields
chapter 6
Many sites try to solve this problem from the outset by storing files in
the database. A table with a binary field stores the file and a control-
ler action streams the file when it is requested.
class CreatePhotos < ActiveRecord::Migration I often use the Sqlite database for small pro-
def self.up
create_table :photos do |t|
totypes (including the examples for this chap-
t.column :caption, :string ter). Unfortunately, Sqlite doesn’t support the
t.column :data, :binary binary field properly, so your binary data will
t.column :updated_at, :datetime be mangled when you retrieve it. The lesson is
end
to use MySQL or Postgres, or you’ll spend an
end
hour banging your head against a wall like I did
while writing this chapter.
You may be tempted to write a simple index action that lists the pho-
tos like this:
def index
@photos = Photo.find :all
end
22
The problem here is that ActiveRecord retrieves all fields from the
database unless told otherwise. The entire contents of each of your
binary fields will be returned, even if you don't use them! If you have
20 rows that each contain a 100 KB file, you're transferring an extra 2
MB across the network from your database server (or at least loading
them into memory if you're using a database on the same machine
as the application server).
def self.find_all_for_listing
find(:all, :select => "id, caption, updated_at")
end
def self.find_all_for_listing_scoped(*args)
with_scope(:find => {
:select => "id, caption, updated_at"
}) do
find(:all, *args)
end
end
You can even use your custom class methods in chained calls:
You can call your custom class method when you just need to show
a listing of the photos in the view. The id and caption are usually
enough to show a gallery with a thumbnail and a link to the full
resolution photo. You could add a user_id or other ownership-related
fields if necessary.
You'll also need a controller to send the photo data when the image
is requested by a web browser. This can be done all in one action
with respond_to and an extra mime type. In environment.rb, declare
the types of the images that you will be serving.
In your controller you can handle requests for the photo with a
matching respond_to block.
def show
@photo = Photo.find params[:id]
24
respond_to do |format|
format.html
format.jpg do
send_data @photo.data, {
:disposition => "inline",
:type => "image/jpeg"
}
end
end
end
If you don't need to restrict access to the images, you can save pro-
cessing power by letting Rails cache the images to disk the first time
they are requested. Add this to the top of your PhotosController:
caches_page :show
resources
• scope_out plugin (http://agilewebdevelopment.com/plugins/scope_out)
• Dan Manges on scope_out (http://dcmanges.com/blog/21)
25
Cache Your Reports
chapter 7
If you're like me, you start out with a simple report that runs fairly
quickly. It gradually slows down as your tables fill with data. After a
few months it takes 30 seconds to run.
Eventually, impatience wins out over the need to know what the
report was going to tell you. You've unintentionally trained yourself to
never look at it because it takes so long to respond and sends your
server load to eleven! If it's really bad, Mongrel will timeout before it
even completes the request.
For this example, I'll assume that I have a views table with one row for
26
every visit to a page on the site, which I'll call a resource. I'll create a
report that shows the total number of visits to each resource for each
day.
end
Next, create a child class. This will still store its data in the reports
table. However, the STI mechanism will be used to differentiate
27
reports from each other.
visits = Visit.report_for_day(day)
visits.each do |visit|
create({
:label => visit.resource,
:quantity => visit.hits,
:reported_on => visit.created_on
})
end
end
• Most reports will be run nightly, after midnight, and will summarize
information from the previous day. I start the method with yester-
day as the default day (which can be overridden).
28
• I like to write these methods so they can be run multiple times
without inserting redundant records. The easiest way to do this is
to start the method by deleting all the existing aggregated records
for the date being reported on. Because I'm using STI, only the
records for this report will be deleted, not records of other sub-
classes of Report.
• Next, call a reporting method in another model with the day argu-
ment. You can also write the method that generates the report and
put it in the ReportVisit model. If you do, be sure to make it a
class method (i.e. prepended with self).
• Finally, loop through the records and save them to the current
aggregation table with create. I'm not doing any error checking,
but you could check the return value of create or call create! to
throw errors on failure.
For reference, here is the Visit model that is being called from the
aggregate method:
def self.report_for_day(day)
find(:all, {
:select => "count(id) as hits,
LEFT(created_at, 10) as created_on,
resource",
:conditions => ["left(created_at, 10) = ?", day],
:group => "created_on, resource",
:order => "created_on DESC, resource ASC"
})
end
end
Finally, call the aggregate method daily with a cron job. Here are two
rake tasks for running daily or yearly aggregates.
namespace :report do
29
desc "Aggregate daily views (yesterday only)"
task :daily => :environment do
ReportVisit.aggregate
end
end
The beauty of this design is that it is very flexible. If you need to cre-
ate a report that should join with a products table, just add a prod-
uct_id foreign key column to the reports table and create a Report-
Product model that belongs_to :product.
resources
• PeepCode Screencast on Page, Action, and Fragment Caching
(http://peepcode.com/products/page-action-and-fragment-caching)
30
Store Country Lists
chapter 8
Rails provides a nice helper for building a select list with most of the
countries of the world.
31
the right way
It is much better to store the built-in country list in the database. This
makes it possible to add fields for sorting the list. You can also take
advantage of class methods to query the list of countries. The built-
in list of countries can be used as a starting point.
First, make a model (and automatic migration) for the country table.
(You could also use a full scaffold if you want to be able to edit the Caveat: The TzTime plugin mentioned else-
list in the browser.) where in this book defines a subclass called
Country. If you have conflicts while using this,
./script/generate model Country
you may have to refer to the Country model
with ::Country instead.
Edit the migration to include a name field in the countries table. I
usually add a special field for marking countries that should be
displayed at the top of the list. We'll use the built-in array of country
names to populate the database.
names = ActionView::Helpers::FormOptionsHelper::COUNTRIES
names.each do |country_name|
Country.create(:name => country_name)
end
Run the migration and you'll have a fully populated table with over
200 countries.
##
# Find all countries in a special order.
def self.find_ordered
find :all, :order => 'special DESC, name'
end
The last thing to do is to format the returned array for the select
helper. This method uses the find_ordered method that we already
wrote and returns an array of arrays that includes the name and id of
each country.
##
# Find all countries and return in an Array usable
# by the select helper.
def self.find_for_select
find_ordered.collect { |record| [record.name, record.id] }
end
Finally, we can use this with the normal select helper to show a list
of countries to choose from.
resources
• API Reference for country_select (http://api.rubyonrails.org/classes/Action-
View/Helpers/FormOptionsHelper.html#M000508)
33
Avoid Bloated Controllers
chapter 9
index
show
new
create
edit
update
destroy
add_comment
destroy_comment
approve_comment
mark_comment_as_spam
mark_comment_as_ham
mark_comment_as_should_have_been_spam
destroy_old_spam_comments
etc.
Of course the flip side is often a few actions that are too compli-
cated.
34
if @article.save
flash[:notice] = "Article was created!"
redirect_to article_url(@article)
else
render "new"
end
else
@article = Article.find(params[:id])
render "show"
end
else
@article = Article.new
render "new"
end
end
• index
• show
• new
• create
• edit
• update
• destroy
There is some flexibility for adding a few custom methods that don't
fit into this pattern. I often add one or two custom methods, but have
set a hard maximum at 10. If I get to 10 actions, I probably need to
35
rethink the design of the application.
You don't need to use all 7 if you don't need them. For example, Rick
Olson's restful_authentication plugin creates a SessionsController
with only a create action for logging in and a destroy action for log-
ging out. You can find these via the sensible aliases http://peepcode.
com/login and http://peepcode.com/logout.
Here are a few fallacies that confuse people when trying to build
around REST:
36
• Are the extra actions just changing an attribute of a model? This
is a good opportunity to make the attribute twiddling into it's own
controller. You could use the update action if the attribute has
many possible values, or create and destroy if it is a boolean field.
• Am I working with a collection of things? Sorting and searching
are some of the hardest ideas to cram into a RESTful pattern. For
searching, it's nice to use the HTTP GET method so a single URL
recreates the search and can be emailed or bookmarked for future
reference. Sorting (usually via a Javascript callback) is a type of
update and should be done with a PUT. I often break the RESTful
pattern for sorting and add an 8th action to update the database
with the results of the sort.
• Am I working with a third-party API that requires a certain inter-
face? PayPal sends people back to your site at a URL that you can
specify. But depending on the type of payment used, the person
could come back via POST or GET. In this case you could create
a new controller that does nothing other than receive people who
are returning from PayPal, or you could add an extra action to an
OrdersController that can be accessed with either POST or GET.
I've chosen to do the latter.
resources
• PeepCode Screencast on REST (http://peepcode.com/products/restful-rails)
• Trotter Cashion's Rails Refactoring to Resources (http://www.informit.
com/store/product.aspx?isbn=0132417995)
• Abstraction via Hampton Catlin's make_resourceful Plugin (http://
groups.google.com/group/make_resourceful)
37
Keep Your Controllers
and Views Skinny
chapter 10
Write some controller code, print it out it in the view, refresh the web
browser, repeat.
The view is often the garbage pile for inadequate models. If you're
doing complicated comparisons and calculations in your views, it's
time to rethink and rework your models:
For example, move all that model-specific controller code into the
User model:
39
Note that this is a class method that starts with self. This makes it
possible to call the method on the User object as seen here:
def skinny_login
if @user = User.authenticate(params[:user])
session[:user_id] = @user.id
flash[:notice] = "You are now logged in"
redirect_to "/"
return
else
flash[:error] = "Wrong password!"
end
end
Now your controller is much simpler! Yes, you still have to manipulate
the flash and the session in the controller, but you've made your
code both more readable and more faithful to MVC. It's also simpler
to pass the :user hash out of the params and let the model pull out
the keys that it needs to know about.
It works with views, too. Those crazy conditionals from the view can
become sensible methods on the Article model.
def recent?
updated_at < 1.month.ago
end
Do you still need a bit of bit of control from the view? Take an argu-
ment, but start with a sensible default.
def more_recent_than?(compare_to=1.month.ago)
updated_at < compare_to
end
40
<%# The right way -%>
<% if article.recent? %>
New article!
<% end %>
getting there
As I've improved my Rails coding skills, and interesting thing has
happened. I've found that I naturally put more code in the models. In
fact, it would probably be better if find were a protected method that
could only be called from other model methods. This would force
developers to encapsulate complicated database-related code with
custom model methods. Unfortunately, find is a public method and
is often overused outside the context of the model. Somehow, I've
changed my technique and write better code almost without trying.
How did I learn this?
Test-first development.
If you start as I did a few years ago (by opening a controller file,
writing some code, and refreshing the browser), it makes absolute
sense to cram all kinds of code into the controller. Writing model
code would take an extra step and would slow down the development
41
process. So you end up with heavy controllers and code that can't be
reused in the console, in rake tasks, or in other controllers.
The problem is partly about design, but it's also about workflow.
Changing your workflow to use the principles of test-first develop-
ment not only provides you with a nice test suite, it can also improve
the implementation of your code.
In any case, here are a few practical tips for making the most of
models.
standard encapsulation
Most controllers contain a lot of database-specific code that can
be pulled into class methods in a model. For example, a compli-
cated find an a User model can become a class method in the User
model:
def self.count_daily_signups
find(:all, {
:select => "count(id) as signup_count,
left(created_at, 10) as signup_day",
:group => "signup_day",
:order => "signup_day ASC"
})
end
User.count_daily_signups
42
Developer Jay Fields (http://blog.jayfields.com) once suggested
that find be made into a protected method, callable
only within a model. This isn't a such a bad idea!
Even now, you can treat it as such and write all your
finds inside descriptively-named model methods.
with_scope
If your query is used in several places and needs to take a few argu-
ments, the with_scope method can be useful.
def self.find_recent(*args)
with_scope(:find => {
:conditions => ["created_at > ?", 1.week.ago]
}) do
find(:all, *args)
end
end
For example, this query will combine the username condition with the
created_at condition.
43
instance methods
Model instance methods are a great way to make your code more
readable. Instead of scattering complicated conditions all around
your controllers and views, encapsulate the condition in the model.
def recent?
updated_at < 1.month.ago
end
In your view, you can call the recent? method on any instance of
Article, like this:
@article.recent?
resources
• Jamis Buck on Skinny Controller, Fat Model (http://weblog.jamisbuck.
org/2006/10/18/skinny-controller-fat-model)
• Jay Fields on the Presenter Pattern (http://blog.jayfields.com/2007/03/
rails-presenter-pattern.html)
• Courtenay Gasking on the Presenter Pattern (http://blog.caboo.se/
articles/2007/8/23/simple-presenters)
• Bruce Williams on Views (http://codefluency.com/search?q=views)
• API Docs on with_scope (http://api.rubyonrails.org/classes/ActiveRecord/Base.
html#M001024)
• with_scope (http://blog.caboo.se/articles/2006/02/22/nested-with_scope)
44
Don't Store Objects
in the Session
chapter 11
def login
user = User.authenticate(params[:user])
session[:user] = user
redirect_to '/'
end
So instead of storing the entire User object, store only the user's id:
def login
user = User.authenticate(params[:user])
session[:user_id] = user.id
redirect_to '/'
end
When you need to retrieve an object, you can pull it from the
database. I find it helpful to follow the technique used in the rest-
ful_authentication plugin (http://svn.techno-weenie.net/projects/plugins/rest-
ful_authentication). Write a method that pulls the id from the session
and does a lookup on the corresponding object. Add this protected
method to your ApplicationController:
protected
The last step is to make this available to all your views. In the Appli-
cationController, declare the method as a helper_method:
helper_method :current_order
memcached
Do you really, really need to store an object? Then memcached is
the way to go.
It's better to start without memcached and add it later when your site
really needs it. If you follow the advice elsewhere in this book about
organization of model code, you'll be in good shape to wrap a few
database queries with calls to memcached when your site grows.
47
resources
• memcached (http://danga.com/memcached)
• Nuby on Rails article about Rails and memcached (http://nubyonrails.
com/articles/2006/08/17/memcached-basics-for-rails)
• memcache-client gem (http://seattlerb.rubyforge.org/memcache-client)
• cache_fu plugin (http://plugins.require.errtheblog.com/browser/cache_fu)
48
Avoid Heavy Response
Processing
chapter 12
49
preemptive conversion
A common expenditure of CPU cycles is text conversion with Textile
or Markdown. Rails provides a view helper called textilize that will
convert Textile to HTML. Don't use it!
If you use the textile helper in your view, you're converting the text
over and over every time it is viewed. You only need to convert when
the data changes, not every single time you display the content.
before_save :convert_description
def convert_description
return if self.description.nil?
self.description_html = RedCloth.new(self.description).
to_html
end
queueing systems
There are many messaging and queuing systems available, includ-
ing Ezra Zygmuntowicz's BackgroundRB and Amazon's SQS. But
this involves additional daemons and even more remote API calls. A
simple combination of rake tasks and cron jobs can be a good start-
ing point. When your site grows you will be in a good place to use a
more robust message queue system.
We'll need a few states to work with. Here is a simple set of states:
state :pending
state :spam
state :ham
state :denied
Now, define events and transitions that will change the record from
one state to another. These will be called as @comment.spam! or @com-
ment.ham!.
event :spam do
51
transitions :to => :spam, :from => :pending
end
event :ham do
transitions :to => :ham, :from => :pending
end
This is a strict set of events. I often find the need to add other transi-
tion directives so a comment can be changed from spam back to ham
or vice versa.
find_all_by_state('pending').each do |pending_comment|
comment_request = pending_comment.parsed_request
akismet_data = {
:user_ip => comment_request.env['REMOTE_ADDR'],
:user_agent => comment_request.env['HTTP_USER_AGENT'],
:referrer => comment_request.env['HTTP_REFERER'],
:permalink => pending_comment.article.permalink,
:comment_type => "comment",
:comment_author => pending_comment.author,
:comment_author_email => pending_comment.email,
:comment_author_url => pending_comment.url,
:comment_content => pending_comment.body
}
if akismet.is_spam?(akismet_data)
pending_comment.spam!
else
pending_comment.ham!
end
end
end
This method can be called regularly with the following rake task:
52
desc "Process comments and mark as spam or ham"
task :process_spam => :environment do
akismet_key = APP_CONFIG['akismet']['key']
blog_name = APP_CONFIG['akismet']['blog_name']
Comment.process_spam(akismet_key, blog_name)
end
Other tasks like log parsing or reporting don't need a queue but can
still be completed with a combination of rake tasks and cron jobs.
resources
• Acts as State Machine plugin (http://elitists.textdriven.com/svn/plugins/
acts_as_state_machine/trunk)
• BackgroundRB (http://backgroundrb.rubyforge.org)
53
Use ActiveRecord Mailing
chapter 13
Things get worse if you are using a remote SMTP server such as
Gmail. Sending 100 emails will be as slow as 100 sequential hits to
the remote server.
In addition, you get the ability to queue many emails and then send
a limited number at a time. If your ISP limits your ability to send
54
email, this is the only way to stay under the limit.
Hendy Irawan of Ruby Inside argues that ar_mailer also makes the
whole process more reliable since ar_sendmail will keep trying to
send an email until a successful connection with the SMTP server has
been made. By default, Rails assumes that the SMTP server is avail-
able 100% of the time, so only one attempt is made to send any
piece of email. Read Hendy's article and comments at RubyInside
(http://www.rubyinside.com/ar_mailer-batch-send-actionmailer-messages-517.html).
And, it's not synchronous. You'll have to live with the fact that some
emails may be delayed by 60 seconds. In most cases, this isn't a big
issue since email delivery is not always instant anyway.
using it
Assuming your application is already setup to send email, installing
ar_mailer involves only a few steps. First, install the ar_mailer and
ar_mailer_generator gems with this single command:
55
Run the generator inside your Rails application. This will copy the
migration and model to your application:
require 'action_mailer/ar_mailer'
You also need to make sure that all your mail methods have a from
address specified. I set a constant in environment.rb and use that
throughout so I don't have to repeat the same address over and over
again.
def signup_confirmation(user)
@subject = "Thanks for signing up!"
@body = { "user" => user}
@recipients = user.email
@from = PEEPCODE_EMAIL
end
At this point, you're ready to use ar_mailer, but your emails will still
be sent immediately unless you tweak a setting in the proper envi-
ronment. This is a feature, not a bug! Usually, you'll want to leave the
development and test environments in their normal state so emails
will not be queued.
57
this single line:
config.action_mailer.delivery_method = :activerecord
After all that work, you're finally ready to send some email. Trigger
the event in your application that sends email (or use the console to
call your mailer's delivery method).
ar_sendmail --help
The mailq argument will show the contents of the unsent mail queue.
These are all the emails which have been sent by Rails but not deliv-
ered to the mail server yet.
ar_sendmail --mailq
ar_sendmail --once
58
The ar_sendmail daemon has been known to
On your production server, you'll want to run that command fre-
go crazy from time to time. It’s a good idea to
quently with cron. Alternately, you can run ar_sendmail in continuous
monitor and restart it if you notice it taking up
daemon mode with the daemonize argument.
too much memory.
resources
• ActionMailer (http://api.rubyonrails.com/classes/ActionMailer/Base.html)
• ar_mailer (http://seattlerb.rubyforge.org/ar_mailer)
59
Monitor Your Servers
chapter 14
How much memory is your application using right now? Is this more
or less than average?
How much disk space remains on your server? How fast is it filling
up?
60
Large sites have been known to see thousands of these emails after
launching, so you may want to use Rick Olson's exception logger
(http://svn.techno-weenie.net/projects/plugins/exception_logger) which logs to a
database instead of sending email. However you choose to use it,
it's an invaluable tool for discovering errors before your users report
them.
Finally, set a few variables to identify who should receive the mes-
sages:
61
* URL: http://videosite.com/downloads/123456789?s3=true
* Parameters: {"s3"=>"true", "action"=>"show",
"id"=>"123456789", "controller"=>"downloads"}
* Rails root: /var/www/apps/videosite.com/current
Install it with
62
gem install production_log_analyzer
The pl_analyze script was originally written to work with the operat-
ing system's syslog logging facility. This is great if you have many
machines and want to consolidate logs to a single machine for
analysis, but requires a few setup steps and familiarity with your spe-
cific flavor of Unix. See the full instructions (http://seattlerb.rubyforge.org/
SyslogLogger/classes/SyslogLogger.html) for setting it up.
require 'hodel_3000_compliant_logger'
config.logger =
Hodel3000CompliantLogger.new(config.log_path)
pl_analyze log/production.log
63
ALL REQUESTS: 5539 0.173 0.000 11.87
-------------------------------------------------------
This report is so useful that you may get all the knowledge you need
just from looking at one run of it. However, it's also useful to store the
results in a database and chart performance over time. You can use
the Analyzer class in the gem to parse the log and store the results.
Here's a sample:
require 'production_log/analyzer'
a = Analyzer.new('log/production.log')
a.process
The built-in Ruby Enumerable class gives you min and max. The gem
adds sum, average, and standard_deviation. You can call these on the
times returned by the Analyzer class and store them in a database
each day with a rake task.
The information you discover from viewing Munin won't tell you what
the solution is to your performance problems. It may not even tell
you what the problem is! However, it will give you a starting point and
65
a historical reference to judge by.
resources
• Exception Notifier (http://svn.rubyonrails.org/rails/plugins/exception_notification)
• Exception Logger (http://svn.techno-weenie.net/projects/plugins/exception_log-
ger)
• dwatch (http://nubyonrails.com/articles/surviving-rails-1-1-with-server-monitoring)
• Munin (http://munin.projects.linpro.no)
• Installing Munin and Monit (http://www.howtoforge.com/server_monitor-
ing_monit_munin)
• production_log_analyzer (http://seattlerb.rubyforge.org/production_log_ana-
lyzer)
• Mailing list message about pl_analyze (http://www.zenspider.com/piper-
mail/ruby/2007-March/003314.html)
• Hodel 3000 Compliant Logger (http://nubyonrails.com/articles/2007/01/03/
a-hodel-3000-compliant-logger-for-the-rest-of-us)
• god (http://god.rubyforge.org)
• monit (http://www.tildeslash.com/monit)
66
Don't Cut Costs
on Hardware
chapter 15
Even after the site has been opened to the public, many startups
take a "wait and see" approach, waiting to increase their server
capacity until they have a large number of visitors.
Waiting means you delay server issues until the worst possible time…
the time that you have a sudden influx of traffic. Having early access
to the operating system and hardware that your final site will run
on means that you can find and fix issues during the development
process.
Some startups have even observed that the number of new accounts
correlates directly with the speed of the site. If you wait to add that
stick of RAM until an extra 1,000 users signup, you may never get
those 1,000 new users.
Assume that your main developer is being paid $75/hour and also
67
performs some sysadmin work. Assume also that you are choosing
between a bare-bones hosting plan that will cost you $10/month and
a full-featured one that will cost $300 every month.
In this case, one month worth of hosting costs less than one day's
worth of developer time. So if the full featured server would save your
developer one day of work each month, you've covered your costs.
Many quality hosts offer plans for even less than $100 per month,
which makes this a no-brainer.
If your site collects any kind of revenue or if you ever hope to, here
are some minimal requirements:
The staging server is configured with all the same software as the
production server and even has its own third-party SSL certificate so
it can send sample payments to the staging counterparts of PayPal
and Google Checkout. Just today, I learned that a new version of
the Nginx webserver will fix some problems I've been having. Instead
69
of rolling it out on the live site, I can try it on the staging server and
verify that it works as expected.
The good news is that you don't have to buy multiple machines to
accomplish this. You could buy a single server and split it into 3, 6, or
even 12 virtual hosts. Each can have their own operating system and
IP address. Then you'll be ready for the rush of traffic when it comes!
resources
• Nuby on Rails hosting article (http://nubyonrails.com/articles/the-host-with-
the-most)
• Xen virtualization (http://www.xensource.com)
70
Test-Drive
chapter 16
I haven't met anyone who has blatantly deleted the test directory
or who denies that tests provide a benefit, but I've met many people
who leave it empty or never use it. There is usually a lingering sense
of guilt. "Yeah, I know I should write tests…I'll get around to it eventu-
ally."
Some people try to learn new habits by force. They grit their teeth
and start doing something they don't want to do in the hope that
they will eventually like it. This rarely works! It's also the reason that
people who write tests after they've written the implementation
quickly tire of it. It's an auxiliary to their workflow, not a part of it.
The instructions tell you to run a generator, but you can also just
make a file called test/integration/spider_test.rb and copy these
contents into it (this is the integration test file from PeepCode):
require "#{File.dirname(__FILE__)}/../test_helper"
include Caboose::SpiderIntegrator
def test_spider
get '/'
assert_response :success
72
spider(@response.body, '/', {
:verbose => true
})
end
end
You need to load all the fixtures used by any of the show
actions for pages on your site. Originally, I had a problem
where the integration test would pass when run with the
entire test suite, but fail when run alone. It turned out that
my :countries fixture was being loaded by other tests
and the data lingered in the database for the integration
test. The solution was to find the missing fixture and load
it explicitly in the integration tests that needed it.
rake
Or alone
ruby test/integration/spider_test.rb
The verbose argument to the spider method will show you that it's
actually doing something, and will list the pages that have been
requested.
$ ruby test/integration/spider_test.rb
• Testing happens. Either you test when you're developing a new fea-
ture, or your users test the site when they enter unexpected data
and cause an error on the live site.
• Tests are meaningless unless they start with a failure.
Therefore, the best way to learn the value of testing is to write a fail-
ing test based on a bug that your users found on the site.
First, write a test for the relevant controller or model. Run the tests
until you see a failure (this shows that your test accurately describes
the error condition).
Now, fix the bug. Run the test suite again and it should pass. If it
doesn't, you haven't fixed the bug yet!
74
brainstorm
I usually start the application design process on paper. I write down
feature ideas or lists of what the application needs to do. This is a
perfect start for a test suite!
When you run the examples, you'll see a report with unimplemented
examples identified.
$ spec order_spec.rb
Order
- should credit account after purchase (NOT IMPLEMENTED)
require 'dust'
75
Now, you can write a test as a simple string, making it easier to write
descriptive test names. While there is no tidy report, the unimple-
mented tests will still be present in the source.
use autotest
Part of the pain of testing is running tests. Fortunately, other devel-
opers have had that same pain and have tried to alleviate it!
The ZenTest gem includes a tool called autotest that will run your
tests whenever you make a change to a test or implementation file.
Not only does this make it easier to run your tests more frequently,
but it also makes them run faster. Autotest will run only the tests that
relate to the current file being modified. When your test passes, it will
run all tests for verification.
There are times when I've needed something more fine-grained than
autotest. For example, this PDF minibook is autogenerated with a
Ruby script and I want to generate a copy anytime I save a source
file. I run a script called rstakeout that can watch an arbitrary set of
files and run an arbitrary command when any of them are modified.
76
The source and instructions for using rstakeout (http://nubyonrails.com/
articles/automation-with-rstakeout) are detailed on my blog.
resources
• autotest (http://nubyonrails.com/articles/autotest-rails)
• ZenTest (http://zentest.rubyforge.org/ZenTest)
• spider_test plugin (http://blog.caboo.se/articles/2007/2/21/the-fabulous-spider-
fuzz-plugin)
• rstakeout (http://nubyonrails.com/articles/automation-with-rstakeout)
• PeepCode Screencast on Test::Unit (http://peepcode.com/products/test-
first-development)
• PeepCode Screencast on rSpec (http://peepcode.com/products/rspec-
basics)
77
The Rest of the List
chapter 17
Congratulations! You've made it this far.
79
Revisions
chapter 18
80
“Rails“ and “Ruby on Rails” are trademarks of
David Heinemeier Hansson.
81