killbill-memoizeit

jruby: implement restart mechanism One can now `touch tmp/restart.txt'

5/13/2013 6:01:53 PM

Details

diff --git a/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyActivator.java b/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyActivator.java
index 0e5cd9a..3fc78f5 100644
--- a/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyActivator.java
+++ b/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyActivator.java
@@ -16,9 +16,13 @@
 
 package com.ning.billing.osgi.bundles.jruby;
 
+import java.io.File;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
 
 import org.jruby.embed.ScriptingContainer;
 import org.osgi.framework.BundleContext;
@@ -34,21 +38,24 @@ import com.google.common.base.Objects;
 
 public class JRubyActivator extends KillbillActivatorBase {
 
-    private static final String jrubyPluginsConfDir = System.getProperty("com.ning.billing.osgi.bundles.jruby.conf.dir");
+    private static final String JRUBY_PLUGINS_CONF_DIR = System.getProperty("com.ning.billing.osgi.bundles.jruby.conf.dir");
+    private static final int JRUBY_PLUGINS_RESTART_DELAY_SECS = Integer.parseInt(System.getProperty("com.ning.billing.osgi.bundles.jruby.restart.delay.secs", "5"));
+
+    private static final String TMP_DIR_NAME = "tmp";
+    private static final String RESTART_FILE_NAME = "restart.txt";
 
     private JRubyPlugin plugin = null;
+    private ScheduledFuture<?> restartFuture = null;
 
-    private final static String KILLBILL_PLUGIN_JPAYMENT = "Killbill::Plugin::JPayment";
-    private final static String KILLBILL_PLUGIN_JNOTIFICATION = "Killbill::Plugin::JNotification";
+    private static final String KILLBILL_PLUGIN_JPAYMENT = "Killbill::Plugin::JPayment";
+    private static final String KILLBILL_PLUGIN_JNOTIFICATION = "Killbill::Plugin::JNotification";
 
     public void start(final BundleContext context) throws Exception {
-
         super.start(context);
 
         withContextClassLoader(new PluginCall() {
             @Override
             public void doCall() {
-
                 logService.log(LogService.LOG_INFO, "JRuby bundle activated");
 
                 // Retrieve the plugin config
@@ -69,19 +76,57 @@ public class JRubyActivator extends KillbillActivatorBase {
                 }
 
                 // Validate and instantiate the plugin
+                startPlugin(rubyConfig, pluginMain, context);
+            }
+        }, this.getClass().getClassLoader());
+    }
 
-                final Map<String, Object> killbillServices = retrieveKillbillApis(context);
-                killbillServices.put("root", rubyConfig.getPluginVersionRoot().getAbsolutePath());
-                killbillServices.put("logger", logService);
-                // Default to the plugin root dir if no jruby plugins specific configuration directory was specified
-                killbillServices.put("conf_dir", Objects.firstNonNull(jrubyPluginsConfDir, rubyConfig.getPluginVersionRoot().getAbsolutePath()));
-                plugin.instantiatePlugin(killbillServices, pluginMain);
+    private void startPlugin(final PluginRubyConfig rubyConfig, final String pluginMain, final BundleContext context) {
+        final Map<String, Object> killbillServices = retrieveKillbillApis(context);
+        killbillServices.put("root", rubyConfig.getPluginVersionRoot().getAbsolutePath());
+        killbillServices.put("logger", logService);
+        // Default to the plugin root dir if no jruby plugins specific configuration directory was specified
+        killbillServices.put("conf_dir", Objects.firstNonNull(JRUBY_PLUGINS_CONF_DIR, rubyConfig.getPluginVersionRoot().getAbsolutePath()));
+
+        // Initial start
+        doStartPlugin(pluginMain, context, killbillServices);
+
+        // Setup the restart mechanism. This is useful for hotswapping plugin code
+        // The principle is similar to the one in Phusion Passenger:
+        // http://www.modrails.com/documentation/Users%20guide%20Apache.html#_redeploying_restarting_the_ruby_on_rails_application
+        final File tmpDirPath = new File(rubyConfig.getPluginVersionRoot().getAbsolutePath() + "/" + TMP_DIR_NAME);
+        if (!tmpDirPath.exists()) {
+            if (!tmpDirPath.mkdir()) {
+                logService.log(LogService.LOG_WARNING, "Unable to create directory " + tmpDirPath + ", the restart mechanism is disabled");
+                return;
+            }
+        }
+        if (!tmpDirPath.isDirectory()) {
+            logService.log(LogService.LOG_WARNING, tmpDirPath + " is not a directory, the restart mechanism is disabled");
+            return;
+        }
 
-                logService.log(LogService.LOG_INFO, "Starting JRuby plugin " + plugin.getPluginMainClass());
-                plugin.startPlugin(context);
+        // TODO Switch to failsafe once in killbill-commons
+        restartFuture = Executors.newSingleThreadScheduledExecutor().scheduleWithFixedDelay(new Runnable() {
+            long lastRestartMillis = System.currentTimeMillis();
 
+            @Override
+            public void run() {
+                final File restartFile = new File(tmpDirPath + "/" + RESTART_FILE_NAME);
+                if (!restartFile.isFile()) {
+                    return;
+                }
+
+                if (restartFile.lastModified() > lastRestartMillis) {
+                    logService.log(LogService.LOG_INFO, "Restarting JRuby plugin " + plugin.getPluginMainClass());
+
+                    doStopPlugin(context);
+                    doStartPlugin(pluginMain, context, killbillServices);
+
+                    lastRestartMillis = restartFile.lastModified();
+                }
             }
-        }, this.getClass().getClassLoader());
+        }, JRUBY_PLUGINS_RESTART_DELAY_SECS, JRUBY_PLUGINS_RESTART_DELAY_SECS, TimeUnit.SECONDS);
     }
 
     private PluginRubyConfig retrievePluginRubyConfig(final BundleContext context) {
@@ -103,13 +148,24 @@ public class JRubyActivator extends KillbillActivatorBase {
         withContextClassLoader(new PluginCall() {
             @Override
             public void doCall() {
-                plugin.stopPlugin(context);
+                restartFuture.cancel(true);
+                doStopPlugin(context);
                 killbillAPI.close();
                 logService.close();
             }
         }, this.getClass().getClassLoader());
     }
 
+    private void doStartPlugin(final String pluginMain, final BundleContext context, final Map<String, Object> killbillServices) {
+        logService.log(LogService.LOG_INFO, "Starting JRuby plugin " + plugin.getPluginMainClass());
+        plugin.instantiatePlugin(killbillServices, pluginMain);
+        plugin.startPlugin(context);
+    }
+
+    private void doStopPlugin(final BundleContext context) {
+        plugin.stopPlugin(context);
+    }
+
     // We make the explicit registration in the start method by hand as this would be called too early
     // (see OSGIKillbillEventDispatcher)
     @Override
@@ -145,6 +201,7 @@ public class JRubyActivator extends KillbillActivatorBase {
 
 
     private static interface PluginCall {
+
         public void doCall();
     }
 
diff --git a/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPlugin.java b/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPlugin.java
index 9f4f391..8993df1 100644
--- a/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPlugin.java
+++ b/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPlugin.java
@@ -39,7 +39,7 @@ import com.ning.billing.payment.plugin.api.PaymentPluginApiException;
 // Bridge between the OSGI bundle and the ruby plugin
 public abstract class JRubyPlugin {
 
-    private final static Logger log = LoggerFactory.getLogger(JRubyPlugin.class);
+    private static final Logger log = LoggerFactory.getLogger(JRubyPlugin.class);
 
     // Killbill gem base classes
     private static final String KILLBILL_PLUGIN_BASE = "Killbill::Plugin::PluginBase";