|
"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.
|
Home
Contact us
|