Messaging that just works

RabbitMQ


This is the RabbitMQ Server Plugin Development Guide. It is expected that before reading this guide, the reader has a basic understanding of the RabbitMQ plugin mechanism, as described in the Plugins section of the Administration Guide. Readers are also expected to have a basic understanding of Erlang OTP system and design principles.

Why Write A Plugin?

Writing a RabbitMQ plugin provides a number of appealing possibilities:

  • Enable your application to access internal RabbitMQ functionality that is not exposed via the AMQP interface.
  • Running in the same Erlang VM as the broker may increase performance for certain applications.
  • Plugins provide an easy deployment model, since they allow for a single "RabbitMQ Appliance" to be built that acts in a self-contained manner. This can be useful both for easing production deployments, and for producing all-in-one infrastructure for use during development.

Why To Not Write A Plugin

As with any plugin mechanism, consideration should be given when developing functionality as to whether embedding it as a plugin is the most appropriate path to take. Some reasons that you might not want to develop your functionality as a plugin:

  • Depending on undocumented internal Rabbit APIs can result in your application requiring changes when RabbitMQ is updated. If you can do what you need to do without using RabbitMQ internals, then your application will be far more compatible in the future.
  • As mentioned in the Admin Guide, a poorly written plugin can result in the entire broker crashing!

Getting Started

To develop a RabbitMQ plugin, a working RabbitMQ development environment first needs to be configured. An "umbrella" project is now provided to assist in assembling all the necessary repositories. The following steps will walk through the process of checking out and activating a copy of the umbrella project in your local environment.

  • Ensure that you have a working installation of Mercurial.
  • Ensure that the dependencies detailed in the Server Build documentation are installed and functional.
  • Clone the RabbitMQ public umbrella:
    $ hg clone http://hg.rabbitmq.com/rabbitmq-public-umbrella
  • Checkout the sub-projects into the Umbrella:
    $ make co
  • Build all the components under the Umbrella to ensure that your environment is functional:
    $ make

Activating a plugin

Instead of requiring that developers rebuild plugin archives and re-install them each time a change is made, plugins can be operated with in a development-style mode to make it possible to develop plugins in place.

To activate a plugin in a development environment, create a symlink for the plugin development directory in the rabbitmq-server/plugins directory. For example, to activate a development build of the rabbitmq-bql plugin:

$ cd rabbitmq-server/plugins
$ ln -s ../../rabbitmq-bql rabbitmq-bql
$ cd ..
$ scripts/rabbitmq-activate-plugins

Conversely, to disable a plugin, simply remove the symlink and re-run rabbitmq-activate-plugins.

Plugin Quality Tips

As highlighted in the Administration Guide, badly-written plugins can pose a risk to the stability of the broker. The following tips aim to provide a series of best-practices for ensuring that your plugin can safely co-exist with Rabbit.

  • Always install a Supervisor above your application. You should never start your application directly, instead opting to create a (possibly quite trivial) supervisor that will prevent the Erlang VM from shutting down due to a crashed top-level application.

RabbitMQ Plugin Hello World

Seeing as no development guide would be complete without a Hello World example, the following tries to provide the basics of how your would build your very own RabbitMQ plugin. The following example details how you might build a simple plugin that acts like a metronome. Every second, it fires a message that has a routing key in the form yyyy.MM.dd.dow.hh.mm.ss to a topic exchange called "metronome". Applications can attach queues to this exchange with various routing keys in order to be invoked at regular intervals. For example, to receive a message every second, a binding of "*.*.*.*.*.*.*" could be applied. To recieve a message every minute, a binding of "*.*.*.*.*.*.00" could be applied instead.

  • The first thing to do is create the basic skeleton of your project. Within your rabbitmq-public-umbrella working copy, create a new directory (for the purposes of this example, we'll call it rabbit_metronome).
  • Create your basic Makefile, containing:
    PACKAGE=rabbitmq-metronome
    DEPS=rabbitmq-server rabbitmq-erlang-client
    
    include ../include.mk
    The top-level include.mk file aims to make your application's Makefile as declarative as possible. Your plugin's Makefile is still a real Makefile however, so you can still do any kind of normal Make tasks.
  • Create your ebin and source directories:
    $ mkdir ebin
    $ mkdir src
  • Create a simple .app file in the ebin directory. Again, for the purposes of this example, call it ebin/rabbit_metronome.app:
    {application, rabbit_metronome,
     [{description, "Embedded Rabbit Metronome"},
      {vsn, "0.01"},
      {modules, [
        rabbit_metronome,
        rabbit_metronome_sup,
        rabbit_metronome_worker
      ]},
      {registered, []},
      {mod, {rabbit_metronome, []}},
      {env, []},
      {applications, [kernel, stdlib, rabbit, amqp_client]}]}.
    This .app file is declaring that, alongside the normal Erlang dependencies, the plugin requires rabbit and amqp_client to be available and started before starting the plugin. This information helps the Erlang infrastructure to correctly order the Rabbit startup sequence.
  • Now create your application entry point (src/rabbit_metronome.erl):
    -module(rabbit_metronome).
    
    -export([start/0, stop/0, start/2, stop/1]).
    
    start() -> 
        rabbit_metronome_sup:start_link(), ok.
    
    stop() -> 
        ok.
    
    start(normal, []) ->
        rabbit_metronome_sup:start_link().
    
    stop(_State) ->
        ok.
  • Followed by your very basic supervisor (src/rabbit_metronome_sup.erl):
    -module(rabbit_metronome_sup).
    -behaviour(supervisor).
    
    -export([start_link/0, init/1]).
    
    start_link() ->
      supervisor:start_link({local, ?MODULE}, ?MODULE, _Arg = []).
    
    init([]) ->
        {ok, {{one_for_one, 3, 10},
              [{rabbit_metronome_worker,
                {rabbit_metronome_worker, start_link, []},
                permanent,
                10000,
                worker,
                [rabbit_metronome_worker]}
              ]}}.
  • Finally, the core of the plugin - the worker. The worker will connect internally to the broker, then create a task that will be triggered every second (src/rabbit_metronome_worker.erl):
    -module(rabbit_metronome_worker).
    -behaviour(gen_server).
    
    -export([start/0, start/2, stop/0, stop/1, start_link/0]).
    -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
    
    -include_lib("amqp_client/include/amqp_client.hrl").
    
    -record(state, {channel}).
    -define(RKFormat, "~4.10.0B.~2.10.0B.~2.10.0B.~1.10.0B.~2.10.0B.~2.10.0B.~2.10.0B").
    
    start() ->
      start_link(),
      ok.
    
    start(normal, []) ->
      start_link().
    
    stop() ->
      ok.
    
    stop(_State) ->
      stop().
    
    start_link() ->
      gen_server:start_link({global, ?MODULE}, ?MODULE, [], []).
    
    %---------------------------
    % Gen Server Implementation
    % --------------------------
    
    init([]) ->
      Connection = amqp_connection:start_direct(),
      Channel = amqp_connection:open_channel(Connection),
      amqp_channel:call(Channel, #'exchange.declare'{exchange = <<"metronome">>,
                                                     type = <<"topic">>}),
    
      timer:apply_after(1000, gen_server, call, [{global, ?MODULE}, fire]),
      {ok, #state{channel = Channel}}.
    
    handle_call(Msg,_From,State = #state{channel = Channel}) ->
      case Msg of
        fire ->
          Properties = #'P_basic'{content_type = <<"text/plain">>, delivery_mode = 1},
          {Date={Year,Month,Day},{Hour, Min,Sec}} = erlang:universaltime(),
          DayOfWeek = calendar:day_of_the_week(Date),
          RoutingKey = list_to_binary(io_lib:format(?RKFormat, [Year, Month, Day, 
                                                                DayOfWeek, Hour, Min, Sec])),
          Message = RoutingKey,
          BasicPublish = #'basic.publish'{exchange = <<"metronome">>,
                                          routing_key = RoutingKey},
          Content = #amqp_msg{props = Properties, payload = Message},
          amqp_channel:call(Channel, BasicPublish, Content),
    
          timer:apply_after(1000, gen_server, call, [{global, ?MODULE}, fire]),
          {reply, ok, State};
        _ ->
          {reply, unknown_command, State}
      end.
    
    handle_cast(_,State) -> 
        {noreply,State}.
    
    handle_info(_Info, State) -> 
        {noreply, State}.
    
    terminate(_,#state{channel = Channel}) -> 
        amqp_channel:call(Channel, #'channel.close'{}),
        ok.
    
    code_change(_OldVsn, State, _Extra) -> 
        {ok, State}.
  • Run make to build your application:
    make
    Note: If you receive an error similar to:
    src/rabbit_metronome_worker.erl:8: can't find include file "rabbit_framing.hrl"
    src/rabbit_metronome_worker.erl:44: record 'P_basic' undefined
    It means that your rabbitmq-server isn't built. Change to ../rabbitmq-server, and execute make there first.
  • Next, associate your plugin (and its dependencies) with the server:
    cd ../rabbitmq-server/plugins
    ln -s ../../rabbitmq-erlang-client
    ln -s ../../rabbit_metronome
    cd ..
    scripts/rabbitmq-activate-plugins
  • Start up your Rabbit broker:
    make run
  • To ensure that your new plugin is up and running, execute the following in the Erlang shell the broker is running within:
    application:which_applications().
    If your plugin has loaded successfully, you should see an entry in the returned list that looks like:
    {rabbit_metronome,"Embedded Rabbit Metronome","0.01"}

Testing Your Plugin

One of the key challenges in developing a plugin is often finding an appropriate (and manageable) way of testing it. Alongside the standard build targets, the include.mk file defines targets that will allow the execution of test cases against your plugin - including starting a broker instance running your plugin.

  • To begin with testing your plugin, create a test/ subdirectory to contain your test code:
    mkdir test
  • Create some test cases for your plugin (for this example, create the file test/rabbit_metronome_tests.erl):
    -module(rabbit_metronome_tests).
    
    -include_lib("eunit/include/eunit.hrl").
    -include_lib("amqp_client/include/amqp_client.hrl").
    
    receive_tick_test() ->
      Connection = amqp_connection:start_direct(),
      Channel = amqp_connection:open_channel(Connection),
      #'queue.declare_ok'{queue = Q}
        = amqp_channel:call(Channel, #'queue.declare'{exclusive = true, auto_delete = true}),
      #'queue.bind_ok'{}
        = amqp_channel:call(Channel, #'queue.bind'{queue = Q,
                                                   exchange = <<"metronome">>,
                                                   routing_key = <<"#">>}),
      timer:sleep(2000),
      case amqp_channel:call(Channel, #'basic.get'{queue = Q, no_ack = true}) of
        {'basic.get_empty', _} -> exit(metronome_didnt_send_message);
        {_, #amqp_msg{}} ->
          ok
      end,
      ok.
    
  • Now that you've written your test, configure the Makefile to execute it by adding the following before the include statement:
    TEST_APPS=amqp_client rabbit_metronome
    TEST_COMMANDS=rabbit_metronome_tests:test()
    START_RABBIT_IN_TESTS=true
    This instructs the Makefile to start Rabbit for your tests, add the rabbit_metronome plugin, and execute the given commands to run your tests. Note that further test commands should be space-separated.
  • You can now run your tests by issuing:
    make test

Packaging Your Plugin

Whilst it is entirely possible to distribute your plugin by having users checkout from your source repository, it is often easier to package your built application into a single archive. As detailed in the Administration Guide, Rabbit supports plugins installed as .ez archives in the plugins/ directory. The standard include.mk file includes a default packaging goal, which can be invoked with:

make package
And will produce an archive under the dist directory.