All software has errors. Duh. However, some of them occur more than others.
As the leading error-tracking service, we have good insight into some of the most common errors in Rails applications.
The startup you've been working at has just crossed the one million user mark and you can't figure out why your servers are using vast amounts of memory. The problem could be the massive queries that worked great when you had a few thousand users, but don't cut it with a million+. Here's an example to illustrate this point:
User.has_subscribed(true).each do |subscriber|
subscriber.grant_role(:subscriber)
end
Imagine that each User object requires 600 bytes. This code is acceptable as long as you have relatively few users. However, having a million users means, at runtime, this code takes up 600MB in memory. A much better alternative would be to use find_each in this way:
User.has_subscribed(true).find_each do |subscriber|
subscriber.grant_role(:subscriber)
end
This is because find_each uses find_in_batches, which pulls 1000 records at a time. This dramatically lowers the memory requirements at runtime. Even better, if your code is written in this way from the beginning it will make it much easier to scale as more users begin to use your product.
One aspect of continuous integration and agile development is to always look ahead and try to solve problems before they affect your service. Writing code that scales easily is one part of that process. It's a tough balance though - writing too much in terms of future scalability can actually work to slow your team down.
Helper methods are a common way to keep the view layer clean in Rails. However, it's very important to know how to create tags inside helpers. This is often done with interpolation or concatenation which leads to messy code:
link = "<li class='house_list'> "
link += link_to("#{house.name.upcase} Listing", show_all_styles_path(house.id, house.url_name))
link += " </li>"
link.html_safe
That's ugly and confusing even if you know what it's doing. Additionally, code like this creates cross-site scripting (XSS) security holes. Instead, using content_tag can help clean up this mess:
content_tag :li, :class => 'house_list' do
link_to("#{house.name.upcase} Listing", show_all_styles_path(house.id, name.url_title))
end
The best solution is to use helper methods that will take blocks. Using nested blocks fits well with nested HTML. The code is easy to follow and it plugs some of those security holes. Continuous deployment means that you, or another programmer, maybe reviewing and revising this code at some point in the near future. That job will be much easier if the code you write now is easy to read and dissect.
Rails developers can forget that Rails is just Ruby. They focus on creating MVC-oriented code, which is a good habit that makes reading code much easier, but makes it possible to create objects in ways that Rails may not endorse.
One great way to fix this is to create facades for 3rd-party services. This allows you to limit the features of the service to only those you actually need. Additionally, it allows you to build a mock facade for testing purposes. Rather than sending requests to the 3rd-party services, the mock facade can deliver a predefined response. This can also be very helpful with exception tracking and exception handling.
The core of any Rails application is the data model. This data will corrode due to bugs in the codebase if you don't create schema constraints. We'll look at two examples of an appointment schema to illustrate this:
create_table "appointment" do |t|
t.integer "user_id"
t.string "title"
t.string "date"
t.string "description"
end
We'll presume that an Appointment must have a title and belong_to a User. In the example code below, we'll use database constraints to guarantee this. Adding :null +> false, ensures the model remains consistent regardless of any bugs that may exist in the validation. This is because the database will not save a model if it fails these constraints:
create_table "appointment" do |t|
t.integer "user_id", :null => false
t.string "title", :null => false
t.string "date"
t.string "description"
end
Another thing to consider would be using :limit => N to restrict the size of string columns. By default, strings can have up to 255 characters. This is quite a few more than you would require for a date or phone number.
Have any other common errors you've solved several times before? Let me know in the comments!