Burning ideas in cooperation with Compojure and Backbone.js
Previous week I wrote my first application using Backbone.js and Compojure. Compojure is a web framework written in Clojure. My application, which code name is Burning Ideas, is very simple and it does not even allow to create user accounts neither sessions. It allows multiple users to brainstorm together and somehow rate their ideas. Rating of ideas will decrease with time. All users are brainstorming together (for now ;))
Code base is available at github. I will write about some conclusions from this not yet finished project.
My first attempt was to use SproutCore. After writing some frontend code I resigned as I found it quite inconvenient to use my custom html markup there. I switched to Backbone.js.
Backbone.js is very simple, clear and understandable. Well, at least for somebody with my backgrounds. Out of the box, it talks to restful JSON APIs which are easily implementable using majority of modern web frameworks. It uses jQuery which I also found very cool.
Compojure is also very simple and clear. It is rather Sinatra than Rails but it does its job. For development I used leiningen with some plugins which bootstraps clojure project and allows to perform different tasks on it. We can run our server with simple command:
lein ring server
lein ring uberwar
Bundler and Capistrano - The Right Way
While working on my current project I use Bundler. There are many resources on the web which says how to use it in pair with Capistrano.
Most of them, however, fails when it comes to
- rollback Capistrano transaction as well as
- rollback Capistrano task.
Let’s say you used already a solution similar to this.
namespace :bundler do
task :create_symlink, :roles => :app do
shared_dir = File.join(shared_path, 'bundle')
release_dir = File.join(current_release, '.bundle')
run("mkdir -p #{shared_dir} && ln -s #{shared_dir} #{release_dir}")
end
task :bundle_new_release, :roles => :app do
bundler.create_symlink
run "cd #{release_path} && bundle install --without test"
end
end
1. Rollback Capistrano transaction
Default deployment path includes transaction with update_code and symlink tasks.
task :update do
transaction do
update_code
symlink
end
endIf you hook bundle_new_release task before these tasks or within the transaction and do not provide bundler_install rollback action you will get into trouble.
If any of the tasks ran after bundle_new_release fail you will end up with bundle required / installed versions mismatch.
Why bother? Maybe update_code and symlink are not the most likely tasks to fail but you have probably already plenty hooks placed before and after them which may fail…
2. Rollback Capistrano transaction
You can rollback last Capistrano deployment by simply running
cap deploy:rollbackSimple, huh?
Unfortunately, if you made any change to your bundle in just deployed commits it will not be reverted what will make your application fail to restart.
So now it is time for the solution.
namespace :bundler do
task :create_symlink, :roles => :app do
shared_dir = File.join(shared_path, 'bundle')
release_dir = File.join(release_path, '.bundle')
run("mkdir -p #{shared_dir} && ln -s #{shared_dir} #{release_dir}")
end
task :install, :roles => :app do
run "cd #{release_path} && bundle install"
on_rollback do
if previous_release
run "cd #{previous_release} && bundle install"
else
logger.important "no previous release to rollback to, rollback of bundler:install skipped"
end
end
end
task :bundle_new_release, :roles => :db do
bundler.create_symlink
bundler.install
end
end
after "deploy:rollback:revision", "bundler:install"
after "deploy:update_code", "bundler:bundle_new_release"Here are some points to notice:
- it is useful to put bundle_new_release task within the transaction
- provide the code run on the transaction rollback
- provide the code run on the capistrano deploy:rollback task
As a hint I can also say that I find it useful to hook my BackgrounDRb restart task into transaction in the default deployment path.
In this way I can be sure that when the transaction is finished the bundle is installed correctly and the application boots up. It is important as I use Unicorn and restart it with sending USR2 signal which will make Unicorn to die silently in case of unbootable application…
Caching, ActiveRecord, and disappearing methods
I was very surprised to see that my model is missing some of its attributes after first request in development mode.
The error occurred while evaluating nil.include?
With the backtrace ending with:
/var/lib/gems/1.8/gems/activerecord-2.3.2/lib/active_record/attribute_methods.rb:142:in 'create_time_zone_conversion_attribute?'
Such a magic… ;-) Here is the recipe. Place in your controller:
def index
@posts = Rails.cache.fetch(:all_posts) { Post.all }
endGo to your view page and press refresh.
The reason is that ActiveRecord stores some information in so called class inheritable attributes. These are stored as your model class variable (Post in that case). Let’s see…
>> before_refresh = Post
>> before_refresh.object_id
=> 70091079977500
>> before_refresh.inheritable_attributes
=> {:skip_time_zone_conversion_for_attributes=>[], :record_timestamps=>true, :reject_new_nested_attributes_procs=>{}, :default_scoping=>[], :scopes=>{:scoped=>#<Proc:0x00007f7eb4450c10@/var/lib/gems/1.8/gems/activerecord-2.3.2/lib/active_record/named_scope.rb:87>}}
>> before_refresh.inheritable_attributes.object_id
=> 70091079977380
>> reload!
>> Post.object_id
=> 70091079300300 # different!
>> before_refresh.inheritable_attributes
=> {} # different!
>> before_refresh.inheritable_attributes.object_id
=> 70091099674080 # different! but similar ;-)It looks like ActiveRecord clears inheritable_attributes before reload… But why? I do not know… I am still learning ;-)
Anyway, here are some other reasons why you should not cache your models but described with the problem of requiring models.
If you still want to cache your models (why not?), here is the tip for disabling caching in your development mode (not so easy). You can set memcached as caching store and give it invalid port number. Queries will be missed all the time without any exception raised while writing to cache.
Edit:
Michał Kwiatkowski found better solution.
acts_as_list - null scope attribute value
Some time ago I ran into performance problems connected with acts_as_list.
My acts_as_list in EmailAddress model was defined as follows:
acts_as_list :scope => :userUnfortunately, most email addresses in database had user_id set to null.
Acts_as_list takes all records from table which has null scope attribute and creates one huge list from all of them… Ups… In my situation it means almost
SELECT * FROM email_addressesHere is my fix. It makes possible to write:
acts_as_list :scope => :user, :ignore_nil => trueAfter doing so, newly created EmailAddress will not be added to any list unless user is set to non null.
In my opinion original behaviour is quite odd as acts_as_list supports listable records which are not on any list…
Powered by Typo – Thème Frédéric de Villamil | Photo Glenn
