Off the Rails ?

"A gold medal is a wonderful thing, but if you're not enough without the medal, you'll never be enough with it."

I need to have my new Rails application visible and embedded into my running Apache 2 web server. The web server already has a whole bunch of other applications running on it, from Stock Quotes data mining to Monte Carlo simulation. Would it not be nice to ultimately port these applications to Rails ? (only if it proves to be as good as it claims to be, but hey, I come to this with an open mind !).

My Apache server has a whole bunch of different virtual hosts, depending on how and where you hit it from. On this side of the firewall, things obviously look a bit different from your side of the world. The standard snippet in the Rails documentation for an Apache installation is simply for the scenario where your web server just serves this Rails application and absolutely nothing else. In fact, you can not even have a second Rails application on the same server instance using this configuration. Well, this is the real world here, and real web servers have multiple applications running on them, and the way to do it is to use the Apache mod_alias plug in. This should already be installed in your vanilla Mandrake installation, unless you did something non-standard, of course. mod_alias allows you to in effect mount a directory which is not even necessarily under your web server's document root to be accessible. The only prerequisite is this directory has to be visible and readable to the id under which your Apache process runs. On mandrake, this id is usually "apache".

As this is a production server, we definitely do not want the apache id to be able to write to the directories under your document root, unless you specifically say so (most exploits these days will probably try to perform a wget of some undesirable payload from some obscure place on to your server and then executes it, so if the directories can be written to, you are already too obliging). This is however not a good thing for a typical Rails application, as it needs to be able to write to at least the "log" directory and possibly the "public" directory (these directories were created as part of your Rails command above). It is up to you how you want to overcome this. There are a few options, all of which will require that you lower the cold plasma shield a bit in order to accommodate our Rails application. For example you can either set the permissions of certain directories to be more friendly, or you could tweak the ownership of these directories in such a way to allow the running id to be able to write to them.

<IfModule mod_alias.c>
    Alias /myapp /home/david/mmyapp/public/
</IfModule>

<Directory /home/david/myapp/public>
    Options ExecCGI
    AllowOverride All
    <IfModule mod_access.c>
        Order allow,deny
        Allow from all
    </IfModule>
</Directory>

Note that the '/' are important. If you want your default application URL to be http://myserver.mydomain.com/myapp, then you have to make sure there is no trailing '/' on the alias token (the first parameter in the Alias directive), but a trailing '/' for the aliased path (the second parameter).

The above section should be placed into the relevant virtual host section of your /etc/httpd/conf/vhosts/Vhosts.conf file, or if you want the application to be visible in all your virtual hosts, place the section in /etc/httpd/conf/commonhttpd.conf. The restart the web server. On Mandrake, it is the Advanced Extranet server, on other flavours of Linux, it is the apachectl command.

$ sudo advxrun2.0

Once the web server is restarted, every time you point your web server at http://myserver.mydomain.com/myapp, the control will be transferred to our Rails generated "public" directory. We need to have something in this directory to process all the request and translate them into Rails calls. The configuration section above instructs Apache to allow our "public" directory to take over control by the AllowOverride directive. The control is then fine tuned by a file which is automatically generated for us in the public directory, albeit hidden, called .htaccess. The default file looks like:

# General Apache options
AddHandler fastcgi-script .fcgi
AddHandler cgi-script .cgi
Options +FollowSymLinks +ExecCGI

# If you don't want Rails to look in certain directories,
# use the following rewrite rules so that Apache won't rewrite certain requests
#
# Example:
# RewriteCond %{REQUEST_URI} ^/notrails.*
# RewriteRule .* - [L]

# Redirect all requests not available on the filesystem to Rails
# By default the cgi dispatcher is used which is very slow
#
# For better performance replace the dispatcher with the fastcgi one
#
# Example:
# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
RewriteEngine On

# If your Rails application is accessed via an Alias directive,
# then you MUST also set the RewriteBase in this htaccess file.
#
# Example:
# Alias /myrailsapp /path/to/myrailsapp/public
# RewriteBase /myrailsapp

RewriteRule ^$ index.html [QSA]
RewriteRule ^([^.]+)$ $1.html [QSA]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ dispatch.cgi [QSA,L]

# In case Rails experiences terminal errors
# Instead of displaying this message you can supply a file here which will be re
ndered instead
#
# Example:
# ErrorDocument 500 /500.html

ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start p
roperly"

 
This file is no good to us the way it is because by default Rails uses the cgi mechanism, which basically forks a separate process each time a request comes in. If you want to be the next Ebay or Amazon, you have to think big, and your web site will have to be able to handle millions of hits from day one, right ? Also, for our particular purpose, the above file will not work (this is the reason for me to scratch my head most of the second day), and this is because the most crucial line is actually commented out. The magic line is to do with the RewriteBase directive.  Apache does some kind of crazy trickery to hand over control to the "public" directory of your Rails application, and along the way manages to lose itself completely without this RewriteBase directive to remind it of how it got there. Read this if you are more interested on how it works.

So, we will have to dive into this file to make it useable for our purpose. However, before doing that, I would like to switch over to using fastcgi instead of cgi in order to improve the performance of our application once it has to face the real world. Ruby has its own fastcgi technology, which can be installed using gem.

$ sudo gem install fastcgi
Attempting local installation of 'fcgi'
Local gem file not found: fcgi*.gem
Attempting remote installation of 'fcgi'
Building native extensions. This could take a while...
Makefile:111: warning: overriding commands for target `/usr/local/lib/ruby/gems/1.8/gems/fcgi-0.8.6.1/.'
Makefile:109: warning: ignoring old commands for target `/usr/local/lib/ruby/gems/1.8/gems/fcgi-0.8.6.1/.'
Makefile:111: warning: overriding commands for target `/usr/local/lib/ruby/gems/1.8/gems/fcgi-0.8.6.1/.'
Makefile:109: warning: ignoring old commands for target `/usr/local/lib/ruby/gems/1.8/gems/fcgi-0.8.6.1/.'
ruby extconf.rb install fcgi
checking for fcgiapp.h... yes
checking for FCGX_Accept() in -lfcgi... yes
creating Makefile

make
gcc -fPIC -g -O2 -I. -I/usr/local/lib/ruby/1.8/i686-linux -I/usr/local/lib/ruby/1.8/i686-linux -I. -DHAVE_FCGIAPP_H -c fcgi.c
gcc -shared -L'/usr/local/lib' -Wl,-R'/usr/local/lib' -o fcgi.so fcgi.o -lfcgi -ldl -lcrypt -lm -lc

make install
/usr/bin/install -c -m 0755 fcgi.so /usr/local/lib/ruby/gems/1.8/gems/fcgi-0.8.6.1/.
Successfully installed fcgi-0.8.6.1
 

Once fastcgi is installed, you need to now modify the .htaccess file in the "public" directory of the Rails application in order to switch on fastcgi and to add the RewriteBase facility.

# General Apache options
AddHandler fastcgi-script .fcgi
# comment out the cgi, we are using fastcgi
#AddHandler cgi-script .cgi
Options +FollowSymLinks +ExecCGI

# If you don't want Rails to look in certain directories,
# use the following rewrite rules so that Apache won't rewrite certain requests
#
# Example:
# RewriteCond %{REQUEST_URI} ^/notrails.*
# RewriteRule .* - [L]

# Redirect all requests not available on the filesystem to Rails
# By default the cgi dispatcher is used which is very slow
#
# For better performance replace the dispatcher with the fastcgi one
#
# Example:
# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
RewriteEngine On

# If your Rails application is accessed via an Alias directive,
# then you MUST also set the RewriteBase in this htaccess file.
#
# Example:
# Alias /myrailsapp /path/to/myrailsapp/public
# uncomment the line below or mod_rewrite will not work for our Rails app
RewriteBase /myapp

RewriteRule ^$ index.html [QSA]
RewriteRule ^([^.]+)$ $1.html [QSA]
RewriteCond %{REQUEST_FILENAME} !-f
# change from dispatch.cgi to dispatch.fcgi
RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]

# In case Rails experiences terminal errors
# Instead of displaying this message you can supply a file here which will be rendered instead
#
# Example:
# ErrorDocument 500 /500.html

ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start p
roperly"

 
What all this is essentially doing is to invoke the dispatch.fcgi command, with the rest of the query string passed to it as the command line parameter. But where is this dispatch.fcgi script ? dispatch.fcgi is automatically generated for you in the "public" directory. However, the same directory also contains a file called index.html and a file called dispatch.cgi. index.html is normally the boring welcome page you get in the vanilla installtion of Rails using WEBrick as the web server. This page is shown to supposedly tell you that your Rails web server is up and running. This can be a false sense of security for other containers (like Apache) however,  because the index.html file is a static page which does not really exercise any of the Ruby code, or indeed, any of the MVC (Model-View-Controller) objects Rails has generated for us. The dispatch.cgi file is used when you do not want to use  fastcgi. Essentially you need to make up your mind about which route you want to take, and the three files are supposed to be mutually exclusive. Once you have settled on your method, you should move the other two files out of the way, or delete them altogether for clarity.

For our case, we want to use dispatch.fcgi, and we have indicated so in our .htaccess file. Time to delete dispatch.cgi and index.html.

The first time you access your default URL, the Apache web server will immediately start up a process to interpret the dispatch.fcgi file, although unlike the normal cgi, this process will not exit. It will remain there in the background and is essentially designed to avoid the start up overhead associated with spawning a new process to serve each request. This makes all the difference to your perceived speed of response of the site in general (fastcgi has its caveat too, specially if you are writing fastcgi unfriendly code, but we will not cover that here).

The next file that needs to be changed is obviously dispatch,.fcgi. The generated one looks like:

#!/usr/local/bin/ruby
#
# You may specify the path to the FastCGI crash log (a log of unhandled
# exceptions which forced the FastCGI instance to exit, great for debugging)
# and the number of requests to process before running garbage collection.
#
# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log
# and the GC period is nil (turned off). A reasonable number of requests
# could range from 10-100 depending on the memory footprint of your app.
#
# Example:
# # Default log path, normal GC behavior.
# RailsFCGIHandler.process!
#
# # Default log path, 50 requests between GC.
# RailsFCGIHandler.process! nil, 50
#
# # Custom log path, normal GC behavior.
# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log'
#
require File.dirname(__FILE__) + "/../config/environment"
require 'fcgi_handler'

RailsFCGIHandler.process!
 

As you can see, in order to optimise the fastcgi mechanism, you need to switch on the GC and the logging. You do this by changing the file to:
#!/usr/local/bin/ruby
#
# You may specify the path to the FastCGI crash log (a log of unhandled
# exceptions which forced the FastCGI instance to exit, great for debugging)
# and the number of requests to process before running garbage collection.
#
# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log
# and the GC period is nil (turned off). A reasonable number of requests
# could range from 10-100 depending on the memory footprint of your app.
#
# Example:
# # Default log path, normal GC behavior.
# RailsFCGIHandler.process!
#
# # Default log path, 50 requests between GC.
# RailsFCGIHandler.process! nil, 50
#
# # Custom log path, normal GC behavior.
#
require File.dirname(__FILE__) + "/../config/environment"
require 'fcgi_handler'

RailsFCGIHandler.process! '/home/david/myapp/log/myapp_fcgi_crash.log', 50
 

The thing to watch out for here is obviously to ensure the log directory you are specifying can be written to by the "apache" id, or you will not get any log output when the dispatch.fcgi script crashes.

dispatch.fcgi is underneath just a Ruby script which performs the Rails magic in order to map your request into the appropriate Rails generated controller object. If you do not have a default controller object, Rails will complain. So, let's create a welcome controller

$ cd /home/david/myapp
$ ./script/generate controller welcome
exists app/controllers/
exists app/helpers/
create app/views/welcome
exists test/functional/
create app/controllers/welcome_controller.rb
create test/functional/welcome_controller_test.rb
create app/helpers/welcome_helper.rb
 

The generated controller file app/controller/welcome_controller.rb contains simply

class WelcomeController < ApplicationController
end

Let's change it to:

class WelcomeController < ApplicationController
    def index
        render_text "Hello World !"
    end
end

Which essentially creates a new action called "index".  This action however needs to be mapped into the default action for our application in order for it to be called upon in the absence of anything trailing action on the request URL. We do this by modifying a file under myapp/config/routes.rb

ActionController::Routing::Routes.draw do |map|
# Add your own custom routes here.
# The priority is based upon order of creation: first created -> highest priority.

# Here's a sample route:
# map.connect 'products/:id', :controller => 'catalog', :action => 'view'
# Keep in mind you can assign values other than :controller and :action

# You can have the root of your site routed by hooking up ''
# -- just remember to delete public/index.html.
map.connect '', :controller => "welcome", :action => "index"

# Allow downloading Web Service WSDL as a file with an extension
# instead of a file named 'wsdl'
map.connect ':controller/service.wsdl', :action => 'wsdl'

# Install the default route as the lowest priority.
map.connect ':controller/:action/:id'
 

The line highlighted in red is the one which has to be uncommented and edited to add the "index" action, everything else is from the original auto-generated file.

At this point, if all is well, you should be able to point your browser at http://myserver.mydomain.com/myapp and see the "Hello World !" string displayed on the web page.

So you can see now that it is not just three commands as alleged on the Ruby On Rails download page that will get you going in the real world, but the only consolation you should draw from this is I have been through the pain so you do not have to. And also, the second prize is you should have a fully integrated Ruby enabled web application in the existing container, which should co-exist with your other applications.

OK, one last warning for another caveat, which left me scratching my head for the third day. I got the application up and running fine while in the office. Once I got home and tried it there, the application consistently came back with http error code 500, which implies Internal Server Error. Where do I start ? not a lot of information to go on. I checked in the Apache log, the application log etc. None gives me any clue. In fact, the application did not even get as far as my Ruby log, as it shows that the dispatch.fcgi process immediately exits with error 500 in the Apache error log. What a pain !

OK, time for some forensics, the first thing I do in this situation is to go into dispatch.fcgi and change the first shebang line form "#!/usr/local/bin/ruby" into "#!/usr/local/bin/ruby -d" to switch on debug. Sure enough, the Apache log file came back with tons of junk. The reason why I say 'junk' is because there is nothing obvious in these trace messages to tell you what the error is. Worse than that, coming from a Java background, I am really annoyed at the lines which indicate that there is an error, only for the rest of the message saying it is in fact a warning (as highlighted in red below). This is a cardinal sin, I tell you. Errors and Warnings just do not mix !

[Wed Nov 22 00:00:00 2005] [error] [client xxx.xxx.xxx.xxx] FastCGI: server "/home/david/myapp/public/dispatch.fcgi" stderr: /usr/local/lib/ruby/gems/1.8/gems/actionpack-1.11.0/lib/action_controller/routing.rb:94: warning: instance variable @optional not initialized, referer: http://www.myserver.mydomain.com/myapp

So, it does not help too much here looking at tons and tons of nearly identical messages. One interesting thing I notice looking at the warning and error messages, Rails (or Ruby) certainly performs a lot of if ... else stuff in order to second guess your intentions and indeed, itself. I keep seeing loads of MissingResource etc. messages, and yet ultimately the whole blackbox seems to arrive at the correct decision and executes the code regardless. It seems to do a lot of "I am looking for this, if it not there, I will fall back to something else up the hierachy, until I find the thing I figured you wanted me to do in the first place". Wonder anyone has tried using this technology to build a nuclear power station or an air traffic control system :-)

But I am still in the dark, why does this application work for me from one browser but not the other ? What is different ? Is it because of all my virtual host settings ? given that Apache redirects differently depending on whether I am behind or in front of the firewall, but the stack trace seems to indicate it is going through the same sort of decisions etc. in order to get to the different outcomes. This is another one of those instances that I mentioned from the beginning that Rails makes certain assumptions about your security model, and the examples which are covered by most on line articles do not cater for this particular, and crazy, setup.

I discovered the answer by sheer persistence and dogged determination not to let this technology beat me (many before it have tried, you name it). I zeroed in on one particular error which looks different between the successful and the failed requests.

[Wed Nov 22 00:00:00 2005] [error] [client xxx.xxx.xxx.xxx] FastCGI: server "/home/david/myapp/public/dispatch.fcgi" stderr: Exception `PStore::Error' at /usr/local/lib/ruby/1.8/pstore.rb:28 - file /tmp/ruby_sess.be48972f6a80d49b not readable, referer: http://www.myserver.mydomain.com/myapp
 

Ah ha ! this looks promising. I looked into the PStore module to see what it is trying to do, and from what I can tell, it seems to be to do with some kind of session tracking used by the CGI mechanism. Fine, but it works flawlessly once, why not the other ?

Another look at the /tmp directory, where these ruby_sess files are stored. I am not sure how these files are generated or managed, but found my answer by looking at the ownership of the particular file it complains about, and sure enough, the file is actually owned by a different id to "apache". Could it have been generated when I was running up the dispatch.fcgi manually in the last step ? I was investigating why my fastcgi mechanism was not working, and probably did not realise I got as far as I did. Anyway, changing the ownership of this file finally solved the mystery.

Previous page Give me a place to stand and with a lever I will move the whole world.

Next page A horse, a horse, my Kingdom for a horse !