Ruby in Elixir?! Yes, that’s right you can have the best of both worlds. The Elixir ecosystem is growing by leaps and bounds, but there are some RubyGems that are well developed and don’t have a comparable Hex package. If you find yourself needing some functionality from a gem, but can’t find a comparable Hex package or don’t have the time write the functionality in Elixr, then Export can help.
Export is just a handy wrapper around the Erlang ErlPort package. I don’t want to go into too much detail around ErlPort since there’s better resources on the topic. Basically ErlPort starts a Ruby (or Python) process which can send and receive messages via Erlang Ports.
Alright, I can see how this might be controversial, but there’s a time and a place for everything. In my case I was working on porting over a major API endpoint over to Elixir, but couldn’t find a good substitution for the IceCube gem.
My first approach was to try to use Ruby to do the scheduling logic; which was already well tested in Ruby, then send the IceCube data to the Elixir microservice (OTP Application). Long story short, I decided against that approach because it would require a good deal of rework on the Ruby side, and made it harder to port the existing logic to Elixir. Also, I discovered Export after stumbling across these blogs:
While those blogs are great resources to get started, there really wasn’t much there to help with how to actually use a Rubygem, bundler, Gemfile, etc. Bundler is pretty much an essential tool of Rubygem management. Almost every host/deployment strategy uses bundler, and my situation was no exception. In the rest of this post, I’ll walk through an example of how I got export to use bundler via bundle exec.
Create a new Elixir application
Add Export as a dependency
Fetch dependencies
Create a directory for the ruby code to live in. priv is ideal since it’s included in the compilation process by default.
The steps above are standard affair, and resemble those other blog posts. This is the part where I start to diverge.
Let’s say you need to use the IceCube gem like I did. Since priv/ruby is where all the ruby goodness goes…let’s create a Gemfile for bundler.
After adding IceCube and ActiveSupport to the Gemfile bundler created it looks like what I have below. Note: I added ActiveSupport because of the utilities it provides in parsing Dates and Times.
Your terminal shell should already be in export_ruby/priv/ruby then we need to get the gems.
Note that I’m using a specific branch of IceCube that is fetched from Github via bundler. At the time of this writing, we needed full timezone support and the changes needed were not yet in a released version.
Now it’s time to get coding. We’re going to build a IceCube::Schedule to get series of dates and times. That means we need a ruby}
file to execute the IceCube code.
While in the export_ruby/priv/ruby directory, use bundler to install the gem dependencies.
You should see ice_cube installed with bundle list, but not when you run gem list. This is an important distinction. That’s because bundler is managing the dependency from Github, and the gem install installed in a different path from the system gems.
Since this is an Elixir application, we need Elixir code to call the Ruby code.
Now that all the parts are setup, let’s give it a try. Remember to pass -S mix to iex.
What just happened, you ask? As I mentioned earlier, the bundler managed paths are not loaded when ruby is started by export so require fails to load ice_cube.
While pairing with a coworker of mine, Luke Imhoff, we started poking around the export source code and found the ruby option:
## Ruby options
- ruby: Path to the Ruby interpreter executable
The first thought was to simply set the ruby: option to point to a bash script that used bundle exec. Long story short, it wasn’t that simple. After digging into erlport (the erlang lib that actually does the heavy lifting), Luke found that erlport is passing a series of flags to ruby.
Putting the pieces together, we can point erlport to a bash script that runs bundle exec, but how do we get the flags that erlport needs to bundle exec in the bash script? As it turns out just use @ in the bash script after the bundle exec. Here’s how it works.
Time to create the bash script. Remember to make it executable.
Important: you want to use quotes so the white space around the flags erlport is maintained "$@".
Now that the bash script is in place let’s modify the elixir code to point to the bash script.
Once those changes are in place, you can recompile the source code without restarting iex.
Presto…you are using ruby with bundler to build a schedule via ice_cube in your Elixir app! Sorta gives you that special kind of feeling like…
In closing, if you want to maximize speed you need to make sure to use a Supervisor to keep the ruby process running instead of starting and stopping on every call. I’ll write another shorter post on how I setup a Supervisor and include some benchmarks. At the time of writing this post, we haven’t deployed the feature to production, but I just started testing locally and everything works great. As long as you’re passing simple data types, export/erlport should be considered when you need a ruby gem but can’t find an equivalent hex package, or simply don’t have the time to write it in Elixir.
I hope this post helps others and saves you the many days I spent working to get export running in my Elixir app.