Ruby on Rails: Cleverness! Evil!

I was reading one of my Ruby on Rails texts (Agile Web Development with Rails, 2nd Ed.), and I came upon something that made me laugh out loud:

(code snippet):

user = self.find_by_name(name)

This code uses a clever little Active Record trick. You see that the first line of the method calls find_by_name. But we don’t define a method with that name. However, [the base class, ] ActiveRecord notices the call to an undefined method and spots that it starts with the string find_by and ends with the name of a column. It then dynamically constructs a finder method for us, adding it to our class…

That’s…so cool! And evil! Not only does it dynamically notice that you want the method, and dynamically interrogate the database schema to see that it makes sense as a column name, and do what you want, but it also dynamically alters the class so that it will now have the method. The second time you call the method, it will just be called directly.

We could certainly do the cool part in Java, where the many names are recognized dynamically. We couldn’t do the evil part, where the actual class is altered at runtime, but I don’t think that dynamic delegation is supposed to be that slow. There is the query to the schema, though…hm.

To me, altering the class at runtime feels like having a superpower, something that, if you really need it, could seriously help you out of a jam, but which you constantly have to tell yourself to only use when you can’t help it.

But it is cool. And evil.

Ruby on Rails: Terse, Expressive Beauty

I'm working on a little Ruby on Rails (RoR) learning project, which is just a flashcard system for practice in learning (human) languages. Nothing too terrible, but complex enough that by the time I’ve implemented my project, I’ll know RoR pretty well.

I have a nice example about why I like Ruby on Rails so much, but first I’ll have to show you these MySQL database tables from my project:

mysql> SELECT * FROM languages;
+----+-----------+
| id | name      |
+----+-----------+
|  1 | English   |
|  2 | Français  |
|  3 | Español   |
+----+-----------+
3 rows in set (0.01 sec)

mysql> SELECT * FROM phrases;
+----+-------------+-----------------------------+
| id | language_id | content                     |
+----+-------------+-----------------------------+
|  1 |           1 | the dog                     |
|  2 |           2 | le chien                    |
|  3 |           3 | el perro                    |
|  4 |           1 | the sexually-profligate man |
+----+-------------+-----------------------------+
4 rows in set (0.00 sec)

mysql> SELECT * FROM concepts;
+----+
| id |
+----+
|  1 |
|  2 |
+----+
2 rows in set (0.00 sec)

mysql> SELECT * FROM concepts_phrases;
+------------+-----------+
| concept_id | phrase_id |
+------------+-----------+
|          1 |         1 |
|          1 |         2 |
|          1 |         3 |
|          2 |         1 |
|          2 |         4 |
+------------+-----------+
5 rows in set (0.00 sec)

mysql> SELECT concept_id, phrase_id, language_id, content, languages.name
    -> FROM concepts
    -> INNER JOIN concepts_phrases ON concepts.id=concepts_phrases.concept_id
    -> INNER JOIN phrases ON concepts_phrases.phrase_id=phrases.id
    -> INNER JOIN languages ON language_id=languages.id
    -> ORDER BY concept_id, phrase_id;
+------------+-----------+-------------+-----------------------------+-----------+
| concept_id | phrase_id | language_id | phrase content              | lang name |
+------------+-----------+-------------+-----------------------------+-----------+
|          1 |         1 |           1 | the dog                     | English   |
|          1 |         2 |           2 | le chien                    | Français  |
|          1 |         3 |           3 | el perro                    | Español   |
|          2 |         1 |           1 | the dog                     | English   |
|          2 |         4 |           1 | the sexually-profligate man | English   |
+------------+-----------+-------------+-----------------------------+-----------+
5 rows in set (0.00 sec)

mysql>

Ok, so for my primitive human-language translation project (I just have to translate well enough for flashcards), each unique concept_id represents a separate idea, or concept. The tables above describe two concepts:

  1. a doggy
  2. a man who sleeps around a lot

With each concept represented by a concept_id, then various phrases in various languages can describe that concept. But there is a many-to-many relationship between concepts and phrases: in English, a dog could be the pet animal (concept 1) and or a sexually profligate man (concept 2) [“You dog, you!”], or even a homely young woman, though this part is left as an exercise for the reader.

But none of that has anything to do with Ruby — it’s just my project’s database tables.

Now, after the database above is defined, how much work did I have to do to define my models in Ruby? Here they are, in their entirety:

class Language < ActiveRecord::Base
  has_many :phrases
 
  validates_presence_of :name

  validates_uniqueness_of :name
end

class Phrase < ActiveRecord::Base
  belongs_to :language
  has_and_belongs_to_many :concepts
 
  validates_presence_of :language_id, :content
 
  validates_uniqueness_of :content
end

class Concept < ActiveRecord::Base
  has_and_belongs_to_many :phrases
end

That’s it, the whole thing — not a snippet. The rest of the model’s intelligence comes from the base class, ActiveRecord::Base, which learns about life by reviewing the schema of the associated database table — not too much tedious glue connecting the language to the database!

The whole language is as terse and expressive:

Suppose that for the tables above, you want to see the content field, 'phrase.content', for all of the phrases, in all of the languages, that are part of concept #1? I'll execute it here, from the ruby console, right before your eyes:

>> concept1 = Concept.find(1)

Reply:
=> #<Concept:0x349fdec @attributes={"id"=>"1"}>

RoR looked up the record for concept #1 in the database, and returned a Ruby object. Boring, I know. But what about that concept’s phrases’ phrase.content values?

>> concept1.phrases.collect { |ph| ph.content }

Reply:
=> ["the dog", "le chien", "el perro"]

And there we are, the phrases for concept #1.

That’s…so cool! In those first few words, concept1.phrases, rails did a join on three database tables:

    concepts X concepts_phrases X phrases

…and plucked out the matching phrases for the concept.

The rest of the line means, “Make a new list, working from each phrase in the old list. For each phrase ph in the old list, make the new list have ph.content, the content field for that phrase.

What’s that? You’re a little sad that we don’t have the name of the language for each phrase? The phrase rows have the language_id, but we want language.name, which is in a whole separate table -- won’t that be a lot of code? No, of course not! It’s only about another 20 characters to make [phrase.content, language.name] pairs:

>> concept1.phrases.collect { |ph| [ph.content, ph.language.name] }

Reply:
=> [["the dog", "English"], ["le chien", "Français"], ["el perro", "Español"]]

There, an array of arrays, with just the few things we wanted. We did joins on four tables:

    concepts X concepts_phrases X phrases X languages

…and extracted just what we wanted, in darn few characters.

You can't say that isn't cool, because it is cool.

Ruby on Rails: [Annoyed Grunt] Stupid Fixtures!

So, as an example of just the latest stumbling block in my Ruby on Rails learning project, I wanted to set up my database tables with proper Foreign Key constraints. But I keep seeing signs that the RoR mainline developers just totally don’t bother with this. Signs like: it’s not really supported very well. But I’m getting ahead of myself.

The Pragmatic Programmers’ Agile Web Development with Rails book had had a link to some guys who had a plugin, several levels of plugins, actually, for supporting FK constraints in migrations. So I took the lowest-level core one, the one that did the least, and hooked it in, and voila, Foreign Key constraints.

And I wrote a controller functional test, which operated on a ‘child’ table (a table that had a foreign key to a ‘parent’ table), and ran it through rake test, and everything was great! The plugin guys had said that if you listed the test fixtures in order from parent table to child table, that everything would work, and it did!

And I wrote another functional test, for the parent this time, and some unit tests for the models…and the world exploded. There were, for one thing, problems tearing down the test data, but (and this was weird), the data was being torn down at the beginning of the tests, rather than at the end of each test. Some time spent with the world’s premier debugging and research tool (starts with a G) turned up a Wiki entry that someone had posted about suppressing FK constraints while tearing down and setting up data. Hm, sounded a little skanky, but at least my actual tests would run with the FK constraints enabled, just like the actual program would. OK.

Retool, retry, and…hey, it almost worked, just one test, of about 60, failed! This was feeling like progress! So, what was the one thing left that was failing? The test_destroy method for the controller that used the parent table. It was trying to delete a record that that was being pointed to by the child table. But WTF? I didn’t even declare the child table’s fixture in my parent table’s controller test!

A great deal of time passed, and in the end, it came down to one committer deciding that if he left a known problem broken, no, if he actually reverted a fix, so that the data from unwanted fixtures that had been used by previous tests would still be left lying around, then he could make the tests twice as fast! And all he had to do was leave it broken!

His justification was that fixtures are really just relics anyway, and that the easy workaround to their being broken was not to use them. I’m not sure, in that case, why it was so vital to speed them up while breaking them again, but then again, I’m not a fricking genius. I am, however, bitter about the hours of my life that I’ll never see again.

Here is the relevant bug ticket. Geez, Louise.

[To be fair, the workarounds that are available are…adequate. I totally have FK constraints in my application, and I’m definitely running rake tests. It wasn’t too awful to work around the problem. Just very annoying.]

How to Create and Deploy a Ruby on Rails App

[This post is in progress. I already know several things that need to be improved in it (actually include link to source to new_proj.rb script, document setting up the /etc/sudoers file for the mongrel user, maybe use capistrano’s ssh deployment method), but I need to finish this version first.]

[edit: so, I’m probably never going to improve this page, because I’ve drifted away from RoR development, so I’m going ahead and publishing it as-is. May it prove helpful! It was written on Feb 22, 2007, so if some of the info is so dated that it no longer works, well…sorry]

Suppose that you have all of the necessary RoR development and deployment software installed and configured (“First: get a million dollors…”). So, on both your local development machine and your remote server, you’ve got Ruby, Rails, MySQL 5, and Subversion installed, on your local machine you’ve installed Capistrano, and on your remote server you’ve installed Apache 2.2 and Mongrel.

Now you would like to create your new project and deploy it on the production server. What are the steps? There are actually quite a few, but you’re creating a new program, checking it into source code control, and setting it up for automatic deployment to a production server, after all.

I set out to write a checklist for the procedure, and this has evolved into somewhat more than that, but is still somewhat short of being a script that you could follow to just do it, if you didn’t know much at all about the subject. Still, as a checklist, it’s getting there.

In the following…

  • [L] means: “on the local development computer”
  • [R] means: “on the remote server computer”
  • [B] means: “on both”

And since I’ve got to make some assumptions or I’ll never finish, I’ll assume:

  • [B] Your application’s name is myapp.
  • [R] The Remote Server is at myapp.com.
  • [R] Apache 2.2 runs as user apache, group apache.
  • [R] Your application will be deployed to /var/www/rails/myapp-com/myapp.
  • [R] Mongrel Cluster runs as user mongrel, group mongrel.
  • [R] Connections over https to subversion will be done under the authority of web user mongrel (which must often be defined separately in Apache and is not covered here, but may be…someday)
  • [R] You will run with 2 mongrel processes in your cluster.
  • [R] Your mongrel processes will listen on ports starting at 8100.
  • [R] Subversion repository root is on filesystem at /var/svn-repos.
  • [R] Subversion repository root is on internet at https://svn.myapp.com/svn-repos.
  • [R] Your server-side databases will all use the same database username, myapp, with password myapp_remote_pass. This isn’t necessaily ideal, and especially you don’t want to use that ridiculous password, but it makes my life easier here.
  • [L] Your local databases will all use the same database username, myapp, with password myapp_local_pass. Again, this just makes my life easier here.
  • [L] You will create your rails application at /home/tom/rails/myapp.
  • [L] Your development database will be named myapp_devl, and can be accessed by user devl_user, with password devl_pass.
  • [L] Your test database will be named myapp_test, and can be accessed by user test_user, with password test_pass.

Here are the steps to creating and deploying your new do-nothing application:

  1. [B] You may want to update your version of rails before generating your application. Or not.
  2. [R] Create the subversion repository that will hold your application:
    [tom@remote]$ sudo svnadmin create /var/svn-repos/myapp
    [tom@remote]$ sudo chown -R apache:apache /var/svn-repos/myapp
    [tom@remote]$ 
    
  3. [L] Run the proj_new.rb script to create your new project and check it into subversion, minus the “inappropriate” parts such as config/database.yml that shouldn’t be checked into the project’s source code control. The script prompts you for four parameters, and produces lots of output:
    local: $ cd /home/tom/rails
    local: $ ruby proj_new.rb
    ...
    #######################################
    Enter svn username: 
    mongrel
    Enter the svn url: 
    https://svn.myapp.com/svn-repos/myapp/trunk
    Enter Rails Application Path:(eg: /home/akhil/ror): 
    /home/tom/rails
    Enter Application Name: 
    myapp
    ######################################
    Please verify the following variables: 
    Svn Username: mongrel
    Svn URL: https://svn.myapp.com/svn-repos/myapp/trunk
    Application Path: /home/tom/rails
    Application name: myapp
    Proceed (y/n)
    y
    Rails 1.2.2
    Generating rails project: (/home/tom/rails/myapp)
          create  
          create  app/controllers
          create  app/helpers
          create  app/models
    ...
          create  log/production.log
          create  log/development.log
          create  log/test.log
    SVNinitial import: 
    Adding         /home/tom/rails/myapp/test
    Adding         /home/tom/rails/myapp/test/unit
    Adding         /home/tom/rails/myapp/test/test_helper.rb
    ...
    Adding         /home/tom/rails/myapp/public/.htaccess
    Adding         /home/tom/rails/myapp/public/stylesheets
    Adding         /home/tom/rails/myapp/public/favicon.ico
    
    Committed revision 1.
    Checking out from svn: 
    A    /home/tom/rails/myapp/test
    A    /home/tom/rails/myapp/test/unit
    A    /home/tom/rails/myapp/test/test_helper.rb
    ...
    A    /home/tom/rails/myapp/public/.htaccess
    A    /home/tom/rails/myapp/public/stylesheets
    A    /home/tom/rails/myapp/public/favicon.ico
    Checked out revision 1.
    cd /home/tom/rails/myapp
    Removing all log files from SVN
    D         log/development.log
    D         log/production.log
    D         log/server.log
    D         log/test.log
    commiting...
    Deleting       log/development.log
    Deleting       log/production.log
    Deleting       log/server.log
    Deleting       log/test.log
    
    Committed revision 2.
    Ignoring all log files under log dir
    property 'svn:ignore' set on 'log'
    Updating and commiting...
    At revision 2.
    Sending        log
    
    Committed revision 3.
    property 'svn:ignore' set on 'tmp'
    property 'svn:ignore' set on 'tmp/sockets'
    property 'svn:ignore' set on 'tmp/sessions'
    property 'svn:ignore' set on 'tmp/pids'
    property 'svn:ignore' set on 'tmp/cache'
    Sending        tmp
    Sending        tmp/cache
    Sending        tmp/pids
    Sending        tmp/sessions
    Sending        tmp/sockets
    
    Committed revision 4.
    Moving database.yml to database.example
    A         config/database.example
    D         config/database.yml
    commiting...
    Adding         config/database.example
    Deleting       config/database.yml
    
    Committed revision 5.
    Ignoring database.yml , updating and commiting...
    property 'svn:ignore' set on 'config'
    At revision 5.
    Sending        config
    
    Committed revision 6.
    Finished.
    local: $ 

    The new application skeleton is created at /home/tom/rails/myapp, checked into subversion, and checked out for modification.

  4. [L] Create the local databases that you will use for development and testing, and find out the one-way hash that corresponds to your database user’s password:
    local: $ mysql -u root -p
    Enter password: 
    Welcome to the MySQL monitor.  Commands end with ; or \g.
    Your MySQL connection id is 25 to server version: 5.0.27-standard
    
    Type 'help;' or '\h' for help. Type '\c' to clear the buffer.
    
    mysql> CREATE DATABASE myapp_devl;
    Query OK, 1 row affected (0.00 sec)
    
    mysql> CREATE DATABASE myapp_test;
    Query OK, 1 row affected (0.00 sec)
    
    mysql> SELECT PASSWORD('myapp_local_pass');
    +-------------------------------------------+
    | PASSWORD('myapp_local_pass')              |
    +-------------------------------------------+
    | *2A3E4076F43DFCE81E4BCC1F1890DE35ADF3C2CD | 
    +-------------------------------------------+
    1 row in set (0.00 sec)
    
    mysql> GRANT ALL ON myapp_devl.* TO 'myapp'@'localhost'              
        -> IDENTIFIED BY 'myapp_local_pass' WITH GRANT OPTION;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> GRANT ALL ON myapp_test.* TO 'myapp'@'localhost'
        -> IDENTIFIED BY 'myapp_local_pass' WITH GRANT OPTION;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> quit
    local: $
    
  5. [L] Make a copy of your project’s myapp/config/database.example file and name it myapp/config/database.yml:
    local: $ cd /home/tom/rails/myapp/config
    local: $ cp database.example database.yml
    local: $ 
    
  6. [L] Edit the myapp/config/database.yml file to match your local database’s configuration. For the password, use the hash that you discovered in the previous step. If this file were compromised, someone could still access your database, until you changed the password, but they wouldn’t see your password in the clear, which can be more damaging.
    # MySQL (default setup).  Versions 4.1 and 5.0 are recommended.
    #
    # Install the MySQL driver:
    #   gem install mysql
    # On MacOS X:
    #   gem install mysql -- --include=/usr/local/lib
    # On Windows:
    #   gem install mysql
    #       Choose the win32 build.
    #       Install MySQL and put its /bin directory on your path.
    #
    # And be sure to use new-style password hashing:
    #   http://dev.mysql.com/doc/refman/5.0/en/old-client.html
    development:
      adapter: mysql
      database: myapp_devl
      username: myapp
      password: *2A3E4076F43DFCE81E4BCC1F1890DE35ADF3C2CD
      host: localhost
    
    # Warning: The database defined as 'test' will be erased and
    # re-generated from your development database when you run 'rake'.
    # Do not set this db to the same as development or production.
    test:
      adapter: mysql
      database: myapp_test
      database: myapp
      password: *2A3E4076F43DFCE81E4BCC1F1890DE35ADF3C2CD
      host: localhost
    
    production:
      adapter: mysql
      database: not-applicable
      username: not-applicable
      password: not-applicable
      host: localhost
    
  7. [L] Run your application in development mode:
    local: $ cd myapp
    local: $ script/server        
    => Booting Mongrel (use 'script/server webrick' to force WEBrick)
    => Rails application starting on http://0.0.0.0:3000
    => Call with -d to detach
    => Ctrl-C to shutdown server
    ** Starting Mongrel listening at 0.0.0.0:3000
    ** Starting Rails with development environment...
    ** Rails loaded.
    ** Loading any Rails specific GemPlugins
    ** Signals ready.  TERM => stop.  USR2 => restart.
        INT => stop (no restart).
    ** Rails signals registered.  HUP => reload (without restart).
        It might not work well.
    ** Mongrel available at 0.0.0.0:3000
    ** Use CTRL-C to stop.
    
  8. [L] Point a web browser at http://localhost:3000, and make sure that you see the Rails welcome screen.
  9. [L] In the command/shell window running script/server, type Ctrl-C to kill the development server.

Okay, good start! You can run your application locally, and it’s checked into source code control, too. (One step that we haven’t tried yet is to connect to the database with your application, so you haven’t proven that you’ve set up your local database correctly, yet. Probably a good idea, but I’m getting sick of writing this page, so you’ll have to find out how to do that elsewhere).

Here’s how to deploy that application to your Unix/Linux server:

  1. [R] Create the root document folder for your rails app:
    [tom@remote]$ sudo mkdir -p /var/www/rails/myapp-com/myapp
    Password:
    [tom@remote]$ sudo chown -R mongrel:mongrel /var/www/rails/myapp-com/myapp
    
  2. [R] Edit your Apache configuration and add the lines for your new rails web site. I put these in several files:
    • [R] Edit your /etc/httpd/conf/httpd.conf file and add the following lines,
      in the same section as the other VirtualHost directives:

      Include conf/myapp-cluster.conf
      
      <VirtualHost *:80>
        ServerName      myapp.com
        ServerAlias www.myapp.com
      
        Include conf/myapp-rails.conf
      </VirtualHost>
      
      # If you have purchased an SSL key for your myapp.com site:
      <VirtualHost numeric-IP-address-of-myapp.com:443>
        ServerName  myapp.com
      
        Include conf/myapp-rails.conf
      
        #SSL setup:
      
        # Tell Apache about SSL.
        SSLEngine On
        SSLCertificateFile    /etc/httpd/conf/ssl.crt/myapp.com.crt
        SSLCertificateKeyFile /etc/httpd/conf/ssl.key/myapp.com.key
      
        # Tell RoR about SSL.
        RequestHeader set X_FORWARDED_PROTO 'https'
      </VirtualHost>
      
    • [R] Create a new file at /etc/httpd/conf/myapp-rails:
      # Rails application "myapp"
      DocumentRoot /var/www/rails/myapp-com/myapp/current/public
      
      <Directory "/var/www/rails/myapp-com/myapp/current/public">
        Options FollowSymLinks
        AllowOverride None
        Order allow,deny
        Allow from all
      </Directory>
      
      ###
      ### The following commented-out directives are causing problems with
      ### Capistrano deployment, apparently due to the symbolic links,
      ### e.g. "current" is a symbolic link.
      ###
      
      ### ProxyPass        / balancer://myapp_cluster/
      ### ProxyPassReverse / balancer://myapp_cluster/
      
      ### # These directories should always be served up by Apache,
      ### # since they contain static content.  Or just let rails do it.
      ### ProxyPass /images !
      ### ProxyPass /stylesheets !
      ### ProxyPass /javascripts !
      ### ProxyPass /favicon.ico !
      ###
      
      # Allow URL rewriting here.
      RewriteEngine On
      
      # Permanently redirect www.myapp.com -> myapp.com.
      RewriteCond %{HTTP_HOST} ^www\.myapp\.com
      RewriteRule ^(.*)$ http://myapp.com$1 [R=301,L]
      
      ###
      ### Start rewrite rules from "Agile Web Development with Rails 2nd ed."
      ###
      
      # Check for maintenance file and redirect all requests.
      RewriteCond @{DOCUMENT_ROOT}/system/maintenance.html -f
      RewriteCond @{SCRIPT_FILENAME} !maintenance.html
      RewriteRule ^.*$ /system/maintenance.html [L]
      
      # Rewrite index to check for static
      RewriteRule ^/$ /index.html [QSA]
      
      # Rewrite to check for Rails cached page
      RewriteRule ^([^.]+)$ $1.html [QSA]
      
      # Redirect all non-static requests to appropriate cluster.
      RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
      RewriteRule ^/(.*)$ balancer://myapp_cluster%{REQUEST_URI} [P,QSA,L]
      
      ###
      ### End rewrite rules from "Agile Web Development with Rails 2nd ed."
      ###
      
      # Important rule to prevent exposure of subversion files,
      # if you are deploying with Capistrano !
      RewriteRule ^(.*/)?.svn/ - [F,L]
      
    • [R] Create a new file at /etc/httpd/conf/myapp-cluster:
      # Configure mongrel cluster "myapp_cluster" for 2 mongrel processes,
      # starting at port 8100.
      <Proxy balancer://myapp_cluster>
        BalancerMember http://127.0.0.1:8100
        BalancerMember http://127.0.0.1:8101
      </Proxy>
      
  3. [L] Configure the pack of 2 mongrels that will run your production ruby code:
    local: $ cd /home/tom/rails/myapp
    local: $ mongrel_rails cluster::configure -e production \
      -p 8100 -N 2 \
      -c /var/www/rails/myapp-com/myapp/current \
      -a 127.0.0.1 --user mongrel \ --group mongrel
    /usr/local/bin/mongrel_rails:17:Warning: require_gem is obsolete.
    Writing configuration file to config/mongrel_cluster.yml.
    local: $ 
    
  4. [L] Run capistrano to create the initial deploment files and rake tasks:
    local: $ cap --apply-to /home/tom/rails/myapp myapp
          exists  config
          create  config/deploy.rb
          exists  lib/tasks
          create  lib/tasks/capistrano.rake
    local: $ 
    
  5. [L] Edit the capistrano deployment file myapp/config/deploy.rb:
    • Add an include directive at the top of the file:
      require 'mongrel_cluster/recipes'
      
    • In the REQUIRED VARIABLES section, edit the line that has the URL to subverison to be:
      set :application, "myapp"
      set :repository, "https://svn.myapp.com/svn-repos/#{application}/trunk"
      
    • In the ROLES section, edit the role lines to be:
      role :web, "myapp.com"
      role :app, "myapp.com"
      role :db,  "myapp.com", :primary => true
      
    • In the OPTIONAL VARIABLES section, the lines should read:
      set :deploy_to,    "/var/www/rails/myapp-com/#{application}"
      set :user,         "mongrel"
      set :mongrel_conf, "#{current_path}/config/mongrel_cluster.yml"
      set :shared_dir,   "shared"
      
    • At the end, add these additional tasks:
      desc "A task demonstrating the use of transactions."
      task :long_deploy do
        transaction do
          update_code
          disable_web
          symlink
          migrate
        end
      
        restart
        enable_web
      end
      
      desc "Link in the production database.yml"
      task :after_update_code do
        run "ln -nfs #{deploy_to}/#{shared_dir}/config/database.yml
          #{release_path}/config/database.yml"
      end
      
      desc "Restart the web server."
      task :restart, :roles => :app do
        sudo "/usr/sbin/apachectl graceful"
      end
      
  6. [L] Commit your changes to subversion over HTTPS:
    local: $ cd /home/tom/rails/myapp
    local: $ svn status
    ?      config/mongrel_cluster.yml
    ?      config/deploy.rb
    ?      lib/tasks/capistrano.rake
    local: $ svn add . --force
    A         config/deploy.rb
    A         config/mongrel_cluster.yml
    A         lib/tasks/capistrano.rake
    local: $ svn commit -m "Added cluster/deployment set up." 
    Adding         config/deploy.rb
    Adding         config/mongrel_cluster.yml
    Adding         lib/tasks/capistrano.rake
    Transmitting file data ...
    Committed revision 7.
    local: $ 
    
  7. [L] Tell capistrano to connect via ssh to your deployment server and to create a few files and directories under /var/www/rails/myapp-com/myapp (the deployment path you set up in config/deploy.rb):
    local: $ cd /home/tom/rails/myapp
    local: $ cap setup
      * executing task setup
      * executing "umask 02 &&\n...
    ...
        servers: ["myapp.com"]
    Password: Enter mongrel's ssh password to Unix/Linux
        [myapp.com] executing command
        command finished
    
  8. [R] One time only, you have to try to check out from the subversion repository using the mongrel account. The SSL password for the web user mongrel will then be cached for the UNIX/Linux mongrel account, so that you don’t have to reauthenticate to SSL when logged in as mongrel.
    $ su mongrel
    $ cd
    $ svn co https://svn.myapp.com/svn-repos/myapp/trunk
    

    (When prompted for a password, enter the password for the web user ‘mongrel’).

  9. [L] Tell capistrano to do a first-time “cold deploy” of the application to the server. This will not quite be successful, because the database.yml isn’t set up yet, but it will allow you to proceed to the next step easily.
    local: $ cd /home/tom/rails/myapp
    local: $ cap cold_deploy
      * executing task cold_deploy
      * executing task update
     ** transaction: start
      * executing task update_code
      * querying latest revision...
      * executing "if [[ ! -d /var/www/rails/myapp-com/myapp...
    ...
        [myapp.com] executing command
     ** [out :: myapp.com] Starting 2 Mongrel servers...
        command finished
    local: $
    
    
  10. [R] The deploy will have created a symbolic link on the server side so that /var/www/rails/sampleapp-com/sampleapp/current/config/database.yml will point to /var/www/rails/sampleapp-com/sampleapp/shared/config/database.yml. Copy the database.example file to be the database.yml file in application’s shared subtree the server side, and make it accessable only by mongrel:
    $ su mongrel
    $ cd /var/www/rails/sampleapp-com/sampleapp
    $ mkdir shared/config
    $ cp current/config/database.example shared/config/database.yml
    $ chmod 600 shared/config/database.yml
    
  11. [R] Create the production database for your application, and grant access to the appropriate database user:
    $ mysql -u root -p
    Enter password:
    Welcome to the MySQL monitor.  Commands end with ; or \g.
    Your MySQL connection id is 17 to server version: 5.0.27-standard
    
    Type 'help;' or '\h' for help. Type '\c' to clear the buffer.
    
    mysql> CREATE DATABASE myapp_prod;
    Query OK, 1 row affected (0.03 sec)
    
    mysql> GRANT ALL ON myapp_prod.* TO 'myapp'@'localhost'
        IDENTIFIED BY 'myapp_remote_pass' WITH GRANT OPTION;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> quit
    Bye
    $
    
  12. [R] Edit the shared/config/database.yml on the server side, to be appropriate for your production server:
    development:
      adapter: mysql
      database: myapp_devl
      username: myapp
      password: myapp_remote_pass (actually, use hash, as before)
      host: localhost
    
    # Warning: The test database MUST be different from the development
    # and production databases. You have been warned.
    test:
      adapter: mysql
      database: myapp_test
      username: myapp
      password: myapp_remote_pass (actually, use hash, as before)
      host: localhost
    
    production:
      adapter: mysql
      database: myapp_prod
      username: myapp
      password: myapp_remote_pass (actually, use hash, as before)
      host: localhost
    
  13. [R] Arrange for the mongrel_cluster to start on every system reboot:
    $ cd /var/www/rails/myapp-com/myapp/current/config
    $ sudo ln -s mongrel_cluster.yml /etc/mongrel_cluster/myapp.yml
    

See, easy! You’re already being more productive!

All right, I know. But now, when you’re ready to push a change from the local development server to the production server, it’s like this:

  1. [L] Commit the changes to source code control:
    $ cd /home/tom/rails/myapp
    $ svn add . --force
    $ svn commit
    
  2. [L] Deploy the changes from source code control to the server farm, and restart the mongrels and Apache:
    $ cd /home/tom/rails/myapp
    $ cap deploy
    
  3. “There is no step 3.”

And say you got a little overexcited with your deployment and want to fall back to the previous release, or the one before that?

  1. [L] Roll back to the previous version:
    $ cd /home/tom/rails/myapp
    $ cap rollback
    
  2. [L] …or the one before that:
    $ cap rollback
    

Our Fine Host

I recently began a campaign to learn Ruby on Rails in earnest (more on that later).

The hosting company that I had been using does not directly support Ruby (though I’m free to install whatever I want on the VPS). So, I tried that, and it worked for a while, but then my little learning-Ruby project kept locking up whenever there was an extended period of inactivity. I actually tried two different servers, from two different companies, both with the same result. There was speculation that Ruby wasn’t noticing that MySql had dropped its connection, and I tried various proposed fixes, but to no avail.

I ended up moving to RimuHosting.com. They’ve been supporting J2EE-based sites for four years, and Ruby-based sites for six months, so I had them install a JBOSS (J2EE) stack and a Ruby stack on my VPS, and so far, everything’s been working great. Ruby is so new that I’m a little bit on the edge, running Fedora Core 6, Apache 2.2, PHP 5.1.6, MySQL 5.0.27, and Rails 1.2.1, but as I say, so far, so good, and the staff could not have been more helpful.

They’re based in Lovely New Zealand, which raises interesting support questions — at least, in my mind, because although of course they offer support 24/7, I imagine them to be more highly staffed during their ‘regular working hours’.

But what are those hours? New Zealand is 20 hours ahead of California (or, thought of another way, four hours behind, but tomorrow). So when I get home from work and want to work on my project, they’re still at work, ready to help, in full force. And when I’m here on Sunday, why, they’re already at work on Monday. And of course, many, if not most or all, of their servers are based in the U.S., (mine is in Texas, presumably at RackShack), and they naturally have support staff based here, too.

So far, the little Ruby learning project, the blog, and the gallery are all running well on the new server. Next up: get everything else off of the old servers and consolidated to the new server, and get those other servers shut down!