Messaging that just works
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.
Writing a RabbitMQ plugin provides a number of appealing possibilities:
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:
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.
$ hg clone http://hg.rabbitmq.com/rabbitmq-public-umbrella
$ make co
$ make
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.
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.
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.
PACKAGE=rabbitmq-metronome DEPS=rabbitmq-server rabbitmq-erlang-client include ../include.mkThe 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.
$ mkdir ebin $ mkdir src
{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.
-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.
-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]}
]}}.
-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}.
makeNote: 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' undefinedIt means that your rabbitmq-server isn't built. Change to ../rabbitmq-server, and execute make there first.
cd ../rabbitmq-server/plugins ln -s ../../rabbitmq-erlang-client ln -s ../../rabbit_metronome cd .. scripts/rabbitmq-activate-plugins
make run
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"}
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.
mkdir test
-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.
TEST_APPS=amqp_client rabbit_metronome TEST_COMMANDS=rabbit_metronome_tests:test() START_RABBIT_IN_TESTS=trueThis 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.
make test
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 packageAnd will produce an archive under the dist directory.