Announcing the migrate_war Rails plugin

Posted by mhagedorn on April 17th, 2008 filed in Rails

The MigrateWar Rails plugin makes it easy to create database schema on deployment machines when you deploy via JRuby/War Files. This is especially helpful on Windows machines, where you cannot use Capistrano easily.

Capistrano is a powerful deployment tool that is considered ‘state of the practice’ by anyone deploying Rails applications to Unixy type systems. But what about if you need some of that same Capistrano fu on a windows server. Can it be done? In my case I really needed a way to build my database schema on the deployment target as soon as the application was deployed (cap deploy with migrations basically). This isnt such an easy thing to do as I found. I was going to deploy via JRuby/Goldspike in a War file format (the standard java deployment package), so I dusted off my arcane Java knowledge and came up with a solution to load the schema.rb file as soon as the war file loads. Read on for details.

The trick here, was that I needed the “migrate to current” process to run only once, when the Rails application loads. When you deploy the web application via the Goldspike Plugin, you end up with a pretty standard Java Web Application WAR file (i.e. Web ARchive). WAR files have a very well defined set of hooks for startup and teardown, which of course would directly correspond to the startup of the Rails application that we are deploying inside of that WAR file. So the basic trick, is to leverage the startup hook of the WAR file (In Javaland this is called a Context) and run the appropriate logic for creating database schema on the deployment box. You could of course do arbitrary other things as well, but thats a talk for another time.

We want to do this in such a way that we are writing a minimum of Java code, and pushing most of the work of doing this off to Ruby. The first thing we have to do is create the Java class which runs when the Context loads (i.e. the Rails application). This is done by creating a ServletContextListener, an interface in Java. Here is an excerpt of the class I wrote (showing only the important bits)

`

    public void contextInitialized(ServletContextEvent event) {
  try {
      final ServletContext context = event.getServletContext();
      // create the pool
      initiallizeJrubyEnvironment(context);

      //this is defined in the web.xml
      commandFile = context.getInitParameter("command-file");


      System.out.println("Entering: MigratorContentListener.contextIntialized \n");

      thread = new Thread(new Runnable() {

          public void run() {
              try {
                  // wait for a little while before starting the task
                  // this allow the app server to start serving requests before initializing all tasks
                  Thread.sleep(100);


                  runOnce(context);
                  System.out.println("Exiting: MigratorContentListener.contextIntialized \n");
              } catch (InterruptedException e) {
                  // break out of loop
              } catch (Exception e) {
                  context.log("Could not start " + commandFile, e);
              }
          }
      });
      thread.start();
  } catch (ServletException ex) {
      Logger.getLogger(MigratorContextListener.class.getName()).log(Level.SEVERE, null, ex);
  }}'


private void runOnce(ServletContext context) throws Exception {

    try {
        String rootDir = context.getRealPath("");

        Ruby runtime = null;
        try {

            String script = readFileAsString(rootDir + "/" + commandFile);
            context.log("executing "+script);
            runtime = (Ruby) getRuntimePool().borrowObject();
            runtimeApi.eval(runtime,"ENV['RAILS_ROOT'] = '" + rootDir + "'");
            runtimeApi.eval(runtime, script);
            getRuntimePool().returnObject(runtime);
        } catch (Exception e) {
           context.log("Could not execute: " + commandFile, e);
            getRuntimePool().invalidateObject(runtime);
          context.log(commandFile + " returning JRuby runtime to pool and will restart in 15 minutes.");
            try {
                Thread.sleep(FIFTEEN_MINUTES_IN_MILLIS);
            } catch (InterruptedException ex) {
            // can't do much here ...
            }
        }


    } catch (Exception e) {
        e.printStackTrace();
        context.log("Could not execute: " + commandFile, e);
    }
}`

The J2EE application server will call the contextInitiallized method shown above when the WAR file loads. In the 4th line of that method, notice that I get the name of the ruby file to run, its passed in via a parameter defined in the web.xml file. More on that later. Basically this class executes the “runOnce” method one time, in a separate thread, as soon as the context loads. Really all the runOnce method does is pass the name of the method to run to the Ruby runtime that has been set up via the magic of JRuby.

In order to get this file to get loaded by the Application Server, you need to make two entries in the web.xml file. The first one, which tells it to attach a listener to the Context, looks like this

<listener> <listener-class>org.jruby.webapp.MigratorContextListener</listener-class> </listener>

where the MigratorContextListener is the Java class that contains the logic listed above. The second entry that you need to put into the web.xml file is what the ruby file is named that you want executed. Here is that entry

<context-param> <param-name>command-file</param-name> <param-value>migrator.rb</param-value> <description>Run this file to execute an initial migration (for deployment to new platforms)</description> </context-param>

The migrator.rb file, the one that gets executed on startup (and found in the root of the application), looks like this

load('db/schema.rb')

Once this executes, the production database indicated in the database.yml will get the schema.rb file applied to it. It would have been much cooler to actually run a migration, but by the time you deploy to production, your schema has probably settled down anyway, so applying the schema.rb file is probably sufficient. I guess the bigger point is, you can do arbitrary things at install time with this.

I bundled all of this up into a plugin called “migrate_war”, one of the things this plugin includes is a jar file which contains the ContextListener, you have to tell the GoldSpike plugin about this jar file so that it includes it in the WAR file it generates. To do this edit the config/war.rb file and add this to the end:

include_library 'migrator-rails' , '0.9'

When you install the plugin, it will copy that jar to lib/java and the include_library command will find it there and pull it into the generate WAR file.

Once you have installed the plugin and made the include_library addition shown above, go ahead and generate your WAR file using Goldspike.

After that finishes you can edit the web.xml file found in WEB-INF/web.xml (Goldspike generates the WEB-INF and everything below it) with the xml edits shown above (for the listener, and the command-file). Then you need to run it again to generate the WAR file with the modified web.xml included (if the WEB-INF directory already exists the Goldspike plugin won’t clobber this directory unless you tell it to, so its cool to edit these files before generating again, Goldspike will use whats in WEB-INF).

You can install the plugin by entering

script/plugin install http://svn.silverchairsolutions.com/migrate_war/.

Hope this helps someone!

Update: Its important to note that this will apply the schema.rb file directly to your database. This will blow away the schema contained within (and any data found there). So if you need to save the data, you should export it then reimport it after the schema gets built. It would be much cooler if migrations were run on this. Anyone care to try?


One Response to “Announcing the migrate_war Rails plugin”

  1. Wage web » Something Worth Knowing Says:

    [...] 11.How they Discovered Something Worth Knowing » Announcing the on April 17th, 2008 filed in Rails … The MigrateWar Rails plugin makes it easy to create database schema on deployment machines when you deploy via JRuby/War Files. This is especially helpful on Windows machines, where you cannot use Capistrano easily. … Can it be done? In my case I really needed a way to build my… http://www.silverchairsolutions.com/blog/?p=26 [...]

Leave a Comment

You must be logged in to post a comment.