I decided to write an agent plugin in Clojure. If you are not familiar with RHQ plugins or what is involved with implementing one, check out this excellent tutorial from my colleague Heiko. Right now, I am just doing exploratory work. I have a few goals in mind though as I go down this path.
First, I have no desire to wind up writing Java in Clojure. By that I mean that I do not want to get bogged down dealing with mutable objects. One of the big draws to Clojure for me is that it is a purely functional language with immutable data structures; so, as I continue my exploration of integrating Clojure with RHQ, I want to write idiomatic Clojure code to the greatest extent possible.
Secondly, I want to preserve what I like to think of as the Clojure development experience. Clojure is a very dynamic language in which functions and name spaces can be loaded and reloaded on the fly. The REPL is an invaluable tool. It provides instant feedback. In my experience Test-Driven Development usually results in short, quick development iterations. TDD + REPL produces extremely fast development iterations.
Lastly, I want to build on the aforementioned goals in order to create a framework for writing RHQ plugins in Clojure. For instance, I want to be able to run and test my plugin in a running plugin container directly from the REPL. And then when I make a change to some function in my plugin, I want to be able to just reload that code without having to rebuild or redeploy the plugin.
Now that I have provided a little background on where I hope to go, let's take a look at where I am currently. Here is the first cut at my Clojure plugin.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns rhq.plugin | |
(:require [rhq.config :as config]) | |
(:import [org.rhq.core.pluginapi.inventory | |
ResourceDiscoveryComponent ResourceDiscoveryContext ClassLoaderFacet | |
DiscoveredResourceDetails ResourceComponent ResourceContext] | |
[org.rhq.core.domain.measurement AvailabilityType]) | |
(:gen-class | |
:name rhq.plugin.PluginComponent | |
:implements [org.rhq.core.pluginapi.inventory.ResourceDiscoveryComponent | |
org.rhq.core.pluginapi.inventory.ResourceComponent])) | |
(defn ^java.util.Set -discoverResources [this ^ResourceDiscveryContext context] | |
(.println System/out "DISCOVER")) | |
(defn ^AvailabilityType -getAvailability [this] nil) | |
(defn -start [this ^ResourceContext context] | |
(.println System/out "STARTING PLUGIN") | |
(print "STARTING PLUGIN")) | |
(defn -stop [this] nil) |
I am using gen-class to generate the plugin component classes. As you can see, this is just a skeleton implementation. Here is the plugin descriptor.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<plugin displayName="Clojure Test Plugin" | |
name="clj-test" | |
package="rhq.clj.test" | |
version="1.0" | |
xmlns:c="urn:xmlns:rhq-configuration" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xmlns="urn:xmlns:rhq-plugin"> | |
<server class="rhq.plugin.PluginComponent" | |
discovery="rhq.plugin.PluginComponent" | |
name="CLJTestServer"/> | |
</plugin> |
I have run into some problems though when I deploy the plugin. When the plugin container attempts to instantiate the plugin component to perform a discovery scan, the following error is thrown:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
java.lang.ExceptionInInitializerError | |
at clojure.lang.Namespace.<init>(Namespace.java:34) | |
at clojure.lang.Namespace.findOrCreate(Namespace.java:176) | |
at clojure.lang.Var.internPrivate(Var.java:94) | |
at rhq.plugin.PluginComponent.<clinit>(Unknown Source) | |
at java.lang.Class.forName0(Native Method) | |
at java.lang.Class.forName(Class.java:247) | |
at org.rhq.core.pc.plugin.PluginComponentFactory.instantiateClass(PluginComponentFactory.java:246) | |
at org.rhq.core.pc.plugin.PluginComponentFactory.getDiscoveryComponent(PluginComponentFactory.java:113) | |
at org.rhq.core.pc.inventory.AutoDiscoveryExecutor.pluginDiscovery(AutoDiscoveryExecutor.java:170) | |
at org.rhq.core.pc.inventory.AutoDiscoveryExecutor.call(AutoDiscoveryExecutor.java:104) | |
at org.rhq.core.pc.inventory.AutoDiscoveryExecutor.run(AutoDiscoveryExecutor.java:92) | |
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:441) | |
at java.util.concurrent.FutureTask$Sync.innerRunAndReset(FutureTask.java:317) | |
at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:150) | |
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$101(ScheduledThreadPoolExecutor.java:98) | |
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.runPeriodic(ScheduledThreadPoolExecutor.java:180) | |
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:204) | |
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886) | |
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908) | |
at java.lang.Thread.run(Thread.java:680) | |
Caused by: java.lang.RuntimeException: java.io.FileNotFoundException: Could not locate clojure/core__init.class or clojure/core.clj on classpath: | |
at clojure.lang.RT.<clinit>(RT.java:305) | |
... 20 more | |
Caused by: java.io.FileNotFoundException: Could not locate clojure/core__init.class or clojure/core.clj on classpath: | |
at clojure.lang.RT.load(RT.java:412) | |
at clojure.lang.RT.load(RT.java:381) | |
at clojure.lang.RT.doInit(RT.java:416) | |
at clojure.lang.RT.<clinit>(RT.java:302) |
I was not entirely surprised to see such an error because I have heard about some of the complexities involved with trying to run Clojure in an OSGi container, and the RHQ plugin container shares some similarities with OSGi. There is a lot of class loader magic that goes on with the plugin container. For instance, each plugin has its own class loader, plugins can be reloaded at runtime, and the container limits the visibility of certain classes and packages. I came across this Clojure Jira ticket, CLJ-260, which talks about setting the context class loader. Unfortunately this did not help my situation because the context class loader is already set to the plugin class loader.
After spinning my wheels a bit, I decided to try a different approach. I implemented my plugin component class in Java, and it delegates to a Clojure script. Here is the code for it.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package rhq.clj.test; | |
import clojure.lang.RT; | |
import clojure.lang.Var; | |
import org.rhq.core.domain.measurement.AvailabilityType; | |
import org.rhq.core.pluginapi.inventory.*; | |
import java.util.HashSet; | |
import java.util.Set; | |
public class ClojurePluginComponent | |
implements ResourceDiscoveryComponent, ResourceComponent { | |
public Set<DiscoveredResourceDetails> discoverResources(ResourceDiscoveryContext context) | |
throws Exception { | |
RT.loadResourceScript("rhq/clj/test.clj"); | |
Var discover = RT.var("rhq.clj.test", "discover"); | |
discover.invoke(); | |
return new HashSet<DiscoveredResourceDetails>(); | |
} | |
public void start(ResourceContext context) throws Exception { | |
} | |
public void stop() { | |
} | |
public AvailabilityType getAvailability() { | |
return null; | |
} | |
} |
And here is the Clojure script,
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns rhq.clj.test) | |
(defn discover [] (.println System/out "DISCOVER")) |
This version deploys without error. I have not fully grokked the class loading issues, but at least for now, I am going to stick with a thin Java layer that delegates to my Clojure code. Up until now, I have been using leiningen to build my code, but now that I am looking at a mixed code base, I may consider switching over to Maven. I use Emacs and Par Edit for Clojure, but I use IntelliJ for Java. The IDE support for Maven will come in handy when I am working on the Java side of things.
I really appreciate your post and it was superb .Thanks for sharing information.
ReplyDeleteRegards:
http://www.blackitsoft.com/inventory-pos-software.aspx