跳至主内容

使用 Elixir 编写 RabbitMQ 插件

·9 分钟阅读
Álvaro Videla

RabbitMQ 是一个非常可扩展的消息代理,允许用户通过编写插件来扩展服务器的功能。许多代理功能甚至作为插件随代理一起安装:例如,管理插件STOMP 支持。虽然这很酷,但插件必须用 Erlang 编写有时会成为一种挑战。我决定看看是否可以用另一种针对 Erlang 虚拟机 (EVM) 的语言编写插件,在这篇文章中我将分享我的进展。

Elixir

在过去的几个月里,我一直在关注一门名为 Elixir 的新编程语言,它运行在 EVM 上。上周,它在 Erlang 社区(以及其他圈子)中变得异常受欢迎,因为 Erlang 之父 Joe Armstrong 试用了这门语言,并且非常喜欢它。于是我决定,好吧,我也来试试。

我不想花太多时间来描述 Elixir 是什么——最好还是你自己去看看它的网站或者 Joe Armstrong 的博客文章。对我来说,它将 Ruby 语言的思想带到了 Erlang 平台。我们可以用一种(对于某些定义而言)更好的语法来编写 Erlang 程序。

将这个来自 Elixir 网站的 Erlang Hello World 程序进行比较

-module(module_name).
-compile(export_all).

hello() ->
io:format("~s~n", ["Hello world!"]).

和这个同样来自其网站的 Elixir 版本

# module_name.ex

defmodule ModuleName do
def hello do
IO.puts "Hello World"
end
end

虽然这个例子相当简单,但我认为对于从未见过 Erlang 语法的人来说,Elixir 版本更容易阅读。从我的角度来看,我一直很喜欢 Erlang 的语法,因为它能清晰地展现 Erlang 概念的语义,并且非常清楚正在发生什么,但这仍然只是我个人的观点。

考虑到人们可能更倾向于使用 Elixir 编写 RabbitMQ 插件,让我们看看如何做到这一点。

编写 RabbitMQ 插件

要为 RabbitMQ 编写插件,你需要设置你的开发环境,以便使用“rabbitmq 公共伞形项目”提供的工具、Makefile 和库。你可以按照此处的设置说明进行操作。一旦你克隆了 `http://hg.rabbitmq.com/rabbitmq-public-umbrella` 项目并安装了所有依赖项,我们就可以开始编写自己的插件了。要用 Elixir 来实现,你首先需要在你的机器上安装这门语言,这样你就可以使用 Elixir 编译器(`mix`)和语言库。

当你编写 RabbitMQ 插件时,你可能希望在插件中使用一些 Erlang 库。要做到这一点,你需要将它们也“包装”成一个插件,这样在你的项目中声明一个库作为依赖项时,构建环境才能识别它们。在这种情况下,我们的新插件将依赖于 Elixir,所以我们需要将语言库包装成一个插件。我已经完成了这一步,你可以从 GitHub 克隆 elixir_wrapper,并按照其 README 中的说明进行安装。

现在是时候创建我们自己的插件了。作为示例,我将一个名为“recent-history-exchange”的插件移植到了 Elixir。顾名思义,它为 RabbitMQ 添加了一种新的交换器类型。交换器是 RabbitMQ 用来决定消息去向的路由算法。如果你想了解更多关于交换器的信息,请点击此处

新交换器的代码可以在 GitHub 上找到:RabbitMQ Recent History Exchange

新交换器类型的代码位于 `lib/rabbit_exchange_type_recent_history.ex` 文件中,我们在其中实现了 `rabbit_exchange_type` 行为。重写的方法有:`route/2`、`delete/3` 和 `add_binding`。这个交换器所做的就是缓存路由到它的最后 20 条消息。每当有新队列绑定到此交换器时,我们就将这最后 20 条消息传递给它。最后,当交换器被删除时,我们就从数据库中移除这些条目。这有什么用呢?比如说,你用 RabbitMQ 实现了一个聊天室,并且你希望加入房间的人能够收到发送到房间的最后几条消息——这是一个简单的方法来实现这一点。

虽然你可能可以通过先看一份 Elixir 教程 来理解大部分代码,但有一些要点值得注意,因为对我来说,如何将它们移植到 Elixir 并不那么清晰。如果你想看看我从 Erlang 移植的原始项目,请在此处查看。

模块属性

RabbitMQ 使用“启动步骤”的概念来启动代理。这些启动步骤在代理启动时进行扫描,然后插件会自动被服务器拾取。它们被声明为模块属性,所以我遇到的第一个障碍是如何在 Elixir 中添加模块属性。假设我们有以下模块

defmodule RabbitExchangeTypeRecentHistory do
end

要像 RabbitMQ 所期望的那样向其添加属性,我们必须这样做

defmodule RabbitExchangeTypeRecentHistory do

Module.register_attribute __MODULE__,
:rabbit_boot_step,
accumulate: true, persist: true

@rabbit_boot_step { __MODULE__,
[{:description, "exchange type x-recent-history"},
{:mfa, {:rabbit_registry, :register,
[:exchange, <<"x-recent-history">>, __MODULE__]}},
{:requires, :rabbit_registry},
{:enables, :kernel_ready}]
end

首先,我们必须通过调用 `Module.register_attribute` 来注册我们的属性,然后我们就可以在代码中像这个例子一样使用它了。

行为

在我们的模块中声明行为非常简单。我们只需要像这样向我们的模块添加一个行为属性

@behaviour :rabbit_exchange_type

Erlang 记录

当你开发 RabbitMQ 插件时(并且可能在任何时候与 Erlang 库进行互操作时),你需要使用库中定义的记录。这不像我预期的那么直接。我们必须将记录定义导入到 Elixir 中。例如,要拥有 RabbitMQ 的 `#exchange` 记录,我们必须这样做

defmodule RabbitExchangeTypeRecentHistory do  
defrecord :exchange, Record.extract(:exchange, from_lib: "rabbit_common/include/rabbit.hrl")
end

请注意,这里我只展示了代码片段。你不必每次都定义 `RabbitExchangeTypeRecentHistory` 模块。

构建插件

一旦我们完成了插件的实现,我们可能会想构建它,毕竟我们花了这么多时间在它上面是有原因的!要做到这一点,我们需要在我们的项目文件夹中添加两个 `Makefile` 以便与 RabbitMQ 的构建系统集成。

第一个叫做 `Makefile`,它只包含一行

include ../umbrella.mk

第二个则更复杂一些。在这里,我们指定了我们项目的依赖项,并告诉 RabbitMQ 构建系统如何编译我们的 Elixir 代码。

DEPS:=rabbitmq-server rabbitmq-erlang-client elixir_wrapper
RETAIN_ORIGINAL_VERSION:=true
ORIGINAL_VERSION:=0.1
DO_NOT_GENERATE_APP_FILE:=

CONSTRUCT_APP_PREREQS:=mix-compile
define construct_app_commands
mkdir -p $(APP_DIR)/ebin
cp $(PACKAGE_DIR)/ebin/* $(APP_DIR)/ebin
endef

define package_rules

$(PACKAGE_DIR)/deps/.done:
rm -rf $$(@D)
mkdir -p $$(@D)
@echo [elided] unzip ezs
@cd $$(@D) &amp;&amp; $$(foreach EZ,$$(wildcard $(PACKAGE_DIR)/build/dep-ezs/*.ez),unzip -q $$(abspath $$(EZ)) &amp;&amp;) :
touch $$@

mix-compile: $(PACKAGE_DIR)/deps/.done
mix clean
ERL_LIBS=$(PACKAGE_DIR)/deps mix compile

endef

我不会逐行解释这段代码,只讲有趣的部分。在第一行,我们声明了我们插件的依赖项。在这种情况下,我们依赖于 `rabbitmq server` 和 `rabbitmq-erlang-client`,以便能够访问我们的插件所需的所有行为和记录。当然,我们也依赖于将与我们的插件一起发布的 Elixir 库。

接下来,我们定义了一些 make 目标来编译我们的插件,并将其打包成一个 `.ez` 文件(RabbitMQ 插件以 .ez 文件形式分发)。`mix-compile` 目标将构建我们的 Elixir 代码。正如你可能已经注意到的,它将 `ERL_LIBS` 变量设置为我们插件的 `./deps` 文件夹。为了让这生效,我们首先需要将之前构建的依赖项解压到那里,所以 make 规则 `$(PACKAGE_DIR)/deps/.done` 会将之前构建的依赖项解压到该文件夹。

最后,我们的 `define construct_app_commands` 将我们的 `.beam` 文件复制到目标文件夹,这样 RabbitMQ 构建系统就能找到它们并将它们包含在我们的插件 `.ez` 文件中。

一旦一切就绪,就可以实际构建我们的插件了。我们可以通过在插件文件夹中调用 `make` 来实现。

构建过程完成后,我们可以在 `dist` 文件夹中找到 `.ez` 文件

ls dist/
amqp_client-3.3.1.ez
elixir-0.9.2.dev-rmq3.3.1-git7c379aa.ez
rabbit_common-3.3.1.ez
rabbitmq_recent_history_exchange_elixir-0.1-rmq3.3.1.ez

从这些文件中,我们只需要将 `rabbitmq_recent_history_exchange_elixir-0.1-rmq3.3.1.ez` 和 `elixir-0.9.2.dev-rmq3.3.1-git7c379aa.ez` 作为我们插件的一部分进行分发。

安装插件

将文件 `rabbitmq_recent_history_exchange_elixir-0.1-rmq3.3.1.ez` 和 `elixir-0.9.2.dev-rmq3.3.1-git7c379aa.ez` 复制到你的 RabbitMQ 安装的 `plugins` 文件夹,然后运行以下命令激活插件

./sbin/rabbitmq-plugins enable rabbitmq_recent_history_exchange_elixir

一旦我们启动代理,我们就可以在管理界面看到,我们现在可以添加 `x-recent-history` 类型的交换器了。

尾声

就是这样。我们可以用 Elixir 构建 RabbitMQ 插件!大部分的“修补工作”已经完成,我们只需要使用 `elixir_wrapper` 并为我们的插件创建适当的 `package.mk` 文件。从那时起,就只是调用 `make` 的问题了。请在评论区分享你的想法,如果你用 Elixir 构建了 RabbitMQ 插件,请分享它并让我知道。

© . This site is unofficial and not affiliated with VMware.