使用 Elixir 编写 RabbitMQ 插件
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 public umbrella 提供的工具、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) && $$(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 插件!大部分 yak shaving 已经完成,我们只需要使用 elixir_wrapper
并为我们的插件创建正确的 package.mk
文件即可。从那里,只需调用 make
即可。请在评论区分享您的想法,如果您使用 Elixir 构建了 RabbitMQ 插件,请分享它并告诉我。