使用 Elixir 编写 RabbitMQ 插件
RabbitMQ 是一个高度可扩展的消息代理,允许用户通过编写插件来扩展服务器的功能。许多代理功能甚至作为插件提供,这些插件在代理安装时默认提供:例如 管理插件,或者 STOMP 支持,仅举几例。虽然这很酷,但插件必须用 Erlang 编写这一事实有时是一个挑战。我决定看看是否可以用另一种针对 Erlang 虚拟机 (EVM) 的语言来编写插件,在这篇文章中我将分享我的进展。
Elixir
在过去的几个月里,我一直关注一种名为 Elixir 的新编程语言,它针对 EVM,并且在上周,它在 Erlang 社区(以及其他圈子)内变得非常流行,因为 Joe Armstrong,Erlang 之父,尝试了该语言,并且非常喜欢它。所以我说,好吧,让我们试试。
我不想花太多时间描述 Elixir 是什么——您最好阅读网站或 Joe Armstrong 关于它的博文。对我来说,它将 Ruby 语言的想法带到了 Erlang 平台。我们本质上可以使用一种 arguably 更好的语法(对于更好的语法的一些定义)编写 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 公共 umbrella 提供的工具、Makefile 和库。您可以按照 这里 的设置说明进行操作。在克隆了 https://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) && $$(foreach EZ,$$(wildcard $(PACKAGE_DIR)/build/dep-ezs/*.ez),unzip -q $$(abspath $$(EZ)) &&) :
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 插件,请分享它并告诉我。