Lapin, a RabbitMQ client for Elixir

Description

Lapin is a RabbitMQ client for Elixir which abstracts away a lot of the complexities of interacting with an AMQP broker.

While some advanced features, like publisher confirms, are tied to RabbitMQ implementation specific extensions, Lapin should play well with other broker implementations conforming to the AMQP 0.9.1 specification.

Installation

Just add Lapin as a dependency to your mix.exs:

defp deps() do
  [{:lapin, ">= 0.0.0"}]
end

Quick Start

If you are impatient to try Lapin out, just tweak this basic configuration example:

config :lapin, :connections, [
  [
    handle: :myhandle
    channels: [
      [
        worker: MyApp.SomeWorker,
        role: :consumer,
        exchange: "some_exchange",
        queue: "some_queue"
      ],
      [
        worker: MyApp.SomeWorker,
        role: :producer,
        exchange: "some_exchange",
        queue: "some_queue"
      ]
    ]
  ]
]

and define your worker module as follows:

defmodule MyApp.SomeWorker do
  use Lapin.Worker, patter: Lapin.Pattern.WorkQueue
end

To test your setup make sure RabbitMQ is running and configured correctly, then run your application with iex -S mix and publish a message:

...
iex(1)> Lapin.publish(:myhandle, "some_exchange", "some_queue", %Lapin.Message{payload: "test"})
[debug] Published to 'test'->'test': %Lapin.Message{meta: nil, payload: "test"}
:ok
[debug] Consuming message 1
[debug] Message 1 consumed successfully, with ACK
...

Read on to learn how easy it is to tweak this basic configuration.

Configuration

You can configure multiple connections. Each connection is backed by a worker which can implement a few callbacks to publish/consume messages and handle other type of events from the broker. Each connection can have one or more channels, each one either consuming OR publishing messages.

The default worker implementation simply logs events at log level :debug.

You need to specify a worker module for all channels. To specify a custom module, implement a custom Lapin.Worker module and adding it under the worker key in your channel configuration. For details on implementing a custom worker module check out the Lapin.Worker behaviour documentation.

At a minimum, you need to configure a handle for each connection and role, worker, exchange and queue for each channel. You can find a a complete list of connection configuration settings in the in Lapin.Connection config type specification.

Advanced channel behaviour can be configured in two ways.

One-shot, static configuration

If you are fine with a one shot configuration of your channels, you can specify any settings from the Lapin.Connection channel_config type specification directly in your channel configurations with the default Lapin.Worker module.

This is quick and easy way to start.

lib/myapp/some_worker.ex:

defmodule MyApp.SomeWorker do
  use Lapin.Worker
end

config/config.exs:

config :lapin, :connections, [
  [
    handle: :myhandle,
    channels: [
      [
        worker: MyApp.SomeWorker,
        role: :consumer,
        exchange: "some_exchange",
        queue: "some_queue",
        exchange_type: :fanout,
        queue_durable: false
      ],
      [
        worker: MyApp.SomeWorker,
        role: :producer,
        exchange: "some_exchange",
        queue: "some_queue",
        publisher_persistent: true
      ]
    ]
  ]
]

Reusable, static or dynamic configuration

If you need to configure a lot of channels in the same way, you can use the Lapin.Pattern to define channel settings. A pattern is simply a collection of behaviour callbacks bundled in a module, which you can then reuse in any worker module when you need the same behaviour in any channel.

To do this, you need to define your pattern module by use ing Lapin.Pattern and specify it in your worker module by passing the pattern key when use ing Lapin.Worker.

In fact Lapin bundles a few Lapin.Pattern modules implementing the RabbitMQ tutorials patterns.

lib/myapp/some_pattern.ex:

defmodule MyApp.SomePattern do
  use Lapin.Pattern

  def exchange_type(_channel_config), do: :fanout,
  def queue_durable(_channel_config), do: false  
  def publisher_persistent(_channel_config), do: true
end

lib/myapp/some_worker.ex:

defmodule MyApp.SomeWorker do
  use Lapin.Worker, pattern: MyApp.SomePattern
end

config/config.exs:

config :lapin, :connections, [
  [
    channels: [
      [
        worker: MyApp.SomeWorker,
        role: :consumer,
        exchange: "some_exchange",
        queue: "some_queue"
      ],
      [
        worker: MyApp.SomeWorker,
        role: :producer,
        exchange: "some_exchange",
        queue: "some_queue"
      ]
    ]
  ]
]

Since a Lapin.Pattern is just a collection of overridable callback functions, patterns also allow you to implement any kind of dynamic runtime configuration.

Actually, the one-shot static configuration explained earlier is implemented by the default Lapin.Pattern module implementation which reads the configuration file and tries to provide sensible defaults for unspecified settings.

Usage

Consuming messages

Once you have completed your configuration, connections will be automatically established and the worker modules with :consumer role will start receiving message published on their queues.

You can handle received messages by overriding the Lapin.Worker.handle_deliver/2 callback. The default implementation does nothing.

defmodule MyApp.SomeWorker do
  use Lapin.Worker, pattern: MyApp.SomePattern

  def handle_deliver(channel_config, message) do
    Logger.debug(fn -> "received message: #{inspect message} on channel: #{inspect channel_config}")
    :ok
  end
end

Messages are considered to be successfully consumed if Lapin.Worker.handle_deliver/2 returns :ok.

Publishing messages

To publish messages with workers with :producer role, you can use the Lapin.publish/5 function passing the connection handle for your connection, or directly call Lapin.Connection.publish/5 if you manually started a connection with Lapin.Connection.start_link/1.

config/config.exs:

config :lapin, :connections, [
  [
    handle: :myhandle,
    channels: [
      [
        role: :producer,
        worker: MyApp.SomeWorker,
        exchange: "some_exchange",
        queue: "some_queue"
      ]
    ]
  ]
]

Via Lapin:

:ok = Lapin.publish(:myhandle, "some_exchange", "routing_key", %Lapin.Message{}, [])  

or via Lapin.Connection directly if you are not starting the :lapin Application:

{:ok, connection} = :lapin
|> Application.get(:connections, [])
|> Enum.at(0)
|> Lapin.start_link

:ok = Lapin.Connection.publish(connection, "some_exchange", "routing_key", %Lapin.Message{}, [])