trapperkeeper-ruby

This project isn't really production-ready; it's got a few things that might be useful and could probably be brought up to production standards without much more work, but it at least needs some tests, docs, etc.

The intent of the project is to illustrate some possible ways to run ruby code inside of trapperkeeper. This includes:

  • Running a ruby Rack application inside of trapperkeeper
  • Using a ruby library to provide a trapperkeeper service that other services can consume
  • Calling a clojure function provided by another service from a ruby-based service

== Running a Rack application from inside of trapperkeeper

Provided in this project is a trapperkeeper service called rack-webserver-service. It provides the same API as the built-in trapperkeeper webserver-service, with one additional function called add-rack-handler. To use it, you simply call that function and point it at the config.ru for your rack application.

The one caveat is that you need to bundle all of the gems that your app depends on so that they can be included in the jar. More info on this a bit later.

The service is defined in src/clojure/puppetlabs/trapperkeeper/services/rack_jetty_service.clj.

You can find an example sinatra app in test/clojure/examples/rack/rack_example.clj and test/ruby/hello-sinatra. To run the code:

lein trampoline run --bootstrap-config ./test/clojure/examples/rack/bootstrap.cfg \
                    --config ./test/clojure/examples/rack/config.ini

The rack-webserver-service could be useful in production and could be brought up to production quality without too much effort.

== Using a ruby library to provide a trapperkeeper service

There is an example of using Ruby code to provide a trapperkeeper service in test/clojure/examples/ruby_provider/ruby_provider_example.clj. It includes examples of simply executing ruby scripts, and also of instantiating classes that are defined in ruby code and calling methods on them. The corresponding ruby code is in test/ruby/service-provider.

Note that the example ruby code has gem dependencies, and that our current approach for resolving them is to use bundler to install the gems into the source tree so that they can be packaged directly in the jar file. For more information, see the section on gems below.

== Calling a clojure function provided by a trapperkeeper service from a rack app

This involves a bit more interop than I'm really proud of, so I'm not sure whether it's wise to do in production... but it turns out to be possible, and not all that difficult. The basic steps:

  • Write a Java class/interface that specifies the method signatures corresponding to the clojure service's functions that you wish to call
  • Write a clojure trapperkeeper service that creates an instance of the java interface, and then makes that instance accessible as a singleton
  • Grab a reference to the singleton from the Ruby code.

For a simple example of this, see the following files:

test/ruby/sinatra-service-consumer/sinatra-consumer.rb
test/clojure/examples/rack/counter/count_service_java_bridge.clj
test/java/examples/rack/counter/CountService.java

gems

This repo contains an example of integrating with a ruby codebase that contains a Gemfile. Here's how you can make this work:

Step 0 (optional)

Install bundler if you don't already have it installed. You can do this via jruby:

lein run -m org.jruby.Main -S gem install bundler --install-dir /tmp/bundler

If you are going this way, you should prepend the following to all of the lein commands below so it can find bundler:

PATH=/tmp/bundler/bin:$PATH GEM_HOME=/tmp/bundler
Step 1

Tell bundler to download all of the gems in Gemfile and add them to the source tree:

lein run -m org.jruby.Main -S bundle install --gemfile src/ruby/Gemfile --path gems

The example above uses src/ruby/Gemfile and downloads the gems to src/ruby/gems. If you are not specifying a temporary bundler executable (as explained it step 0), it will use the system bundler (from $PATH).

Step 2

Tell bundler to generate a ruby script that will add the gems to the load path:

lein run -m org.jruby.Main -S bundle install --gemfile src/ruby/Gemfile --standalone
Step 3

Bundler outputs this script in kind of a weird location (gems/bundler/setup.rb), so move it into the normal source tree structure:

mv gems/bundler src/ruby/gems
Step 4

Execute that script in the jruby execution container that in which you want the gems to be available. See the implementation of :gem-test-fn in wrapper.clj.

This commit contains the end result of this process - a simple Gemfile specifying a dependency on the awesome-print gem, a ruby script that uses awesome-print, and a trapperkeeper service that runs that executes the bunder-generated setup.rb script to add the depencies into the the ruby load path before executing the script which depends on them.

Note about ruby objects

The ruby scripts used in the jruby example only returns "raw" values (strings and numbers); these cases are handled nicely by jruby and automatically converted into the appropriate java objects (java.lang.String, java.lang.Integer, etc.). However, if the result of the ruby script is not a primitive type, it will come back to JVM land as a RubyObject (or RubyHash, RubyArray, etc.). In this case, I believe it will be necessary to manually convert the RubyObject to whatever data structure is needed in JVM-land (in the case of things like arrays and hashes, there may be automatic conversion utliites but I haven't found them yet.)