Optimize rabbitmqctl startup.

Plugins dirs are added to ERL_LIBS by shell scripts, we try to load
enabled plugins first and scan plugins dir only if some of them
are not found.
When discovering plugins, we can load applications only for enabled ones.
EPMD can be started only after initial `net_kernel:start` call failed.
We don't need to discover all commands every time, only if a command is not
in the `rabbitmqctl` application modules.
[finishes #143025009]
This commit is contained in:
Daniil Fedotov 2017-04-04 14:06:53 +01:00
parent 00a298961a
commit 62a44ac641
8 changed files with 133 additions and 109 deletions

View File

@ -20,12 +20,20 @@ alias RabbitMQ.CLI.Core.Helpers, as: Helpers
defmodule RabbitMQ.CLI.Core.CommandModules do defmodule RabbitMQ.CLI.Core.CommandModules do
@commands_ns ~r/RabbitMQ.CLI.(.*).Commands/ @commands_ns ~r/RabbitMQ.CLI.(.*).Commands/
def module_map do def module_map(opts \\ %{}) do
Application.get_env(:rabbitmqctl, :commands) || load(%{}) Application.get_env(:rabbitmqctl, :commands) || load(opts)
end end
def is_command?([head | _]), do: is_command?(head) def module_map_core(opts \\ %{}) do
def is_command?(str), do: module_map()[str] != nil Application.get_env(:rabbitmqctl, :commands_core) || load_core(opts)
end
def load_core(opts) do
scope = script_scope(opts)
commands = load_commands_core(scope)
Application.put_env(:rabbitmqctl, :commands_core, commands)
commands
end
def load(opts) do def load(opts) do
scope = script_scope(opts) scope = script_scope(opts)
@ -39,8 +47,47 @@ defmodule RabbitMQ.CLI.Core.CommandModules do
scopes[Config.get_option(:script_name, opts)] || :none scopes[Config.get_option(:script_name, opts)] || :none
end end
def load_commands_core(scope) do
make_module_map(ctl_modules(), scope)
end
def load_commands(scope, opts) do def load_commands(scope, opts) do
ctl_and_plugin_modules(opts) make_module_map(plugin_modules(opts) ++ ctl_modules(), scope)
end
def ctl_modules() do
Application.spec(:rabbitmqctl, :modules)
end
def plugin_modules(opts) do
Helpers.require_rabbit(opts)
partitioned =
Enum.group_by(PluginsHelpers.read_enabled(opts), fn(app) ->
case Application.load(app) do
:ok -> :loaded;
{:error, {:already_loaded, ^app}} -> :loaded;
_ -> :not_found
end
end)
loaded = partitioned[:loaded] || []
missing = partitioned[:not_found] || []
## If plugins are not in ERL_LIBS, they should be loaded from plugins_dir
case missing do
[] -> :ok;
_ ->
Helpers.add_plugins_to_load_path(opts)
Enum.each(missing, fn(app) -> Application.load(app) end)
end
Enum.flat_map(loaded ++ missing, fn(app) ->
Application.spec(app, :modules) || []
end)
end
defp make_module_map(modules, scope) do
modules
|> Enum.filter(fn(mod) -> |> Enum.filter(fn(mod) ->
to_string(mod) =~ @commands_ns to_string(mod) =~ @commands_ns
and and
@ -54,13 +101,6 @@ defmodule RabbitMQ.CLI.Core.CommandModules do
|> Map.new |> Map.new
end end
def ctl_and_plugin_modules(opts) do
Helpers.require_rabbit_and_plugins(opts)
enabled_plugins = PluginsHelpers.read_enabled(opts)
[:rabbitmqctl | enabled_plugins]
|> Enum.flat_map(fn(app) -> Application.spec(app, :modules) || [] end)
end
defp module_exists?(nil) do defp module_exists?(nil) do
false false
end end

View File

@ -26,14 +26,25 @@ defmodule RabbitMQ.CLI.Core.Distribution do
def start(options) do def start(options) do
node_name_type = Config.get_option(:longnames, options) node_name_type = Config.get_option(:longnames, options)
:rabbit_nodes.ensure_epmd()
start(node_name_type, 10, :undefined) start(node_name_type, 10, :undefined)
end end
def start_as(node_name, opts) do def start_as(node_name, options) do
:rabbit_nodes.ensure_epmd() node_name_type = Config.get_option(:longnames, options)
node_name_type = Config.get_option(:longnames, opts) start_with_epmd(node_name, node_name_type)
:net_kernel.start([node_name, node_name_type]) end
## Optimization. We try to start EPMD only if distribution fails
def start_with_epmd(node_name, node_name_type) do
case :net_kernel.start([node_name, node_name_type]) do
{:ok, _} = ok -> ok;
{:error, {:already_started, _}} = started -> started;
{:error, {{:already_started, _}, _}} = started -> started;
## EPMD can be stopped. Retry with EPMD
{:error, _} ->
:rabbit_nodes.ensure_epmd()
:net_kernel.start([node_name, node_name_type])
end
end end
# #
@ -46,7 +57,7 @@ defmodule RabbitMQ.CLI.Core.Distribution do
defp start(node_name_type, attempts, _last_err) do defp start(node_name_type, attempts, _last_err) do
candidate = generate_cli_node_name(node_name_type) candidate = generate_cli_node_name(node_name_type)
case :net_kernel.start([candidate, node_name_type]) do case start_with_epmd(candidate, node_name_type) do
{:ok, _} -> :ok {:ok, _} -> :ok
{:error, {:already_started, pid}} -> {:ok, pid}; {:error, {:already_started, pid}} -> {:ok, pid};
{:error, {{:already_started, pid}, _}} -> {:ok, pid}; {:error, {{:already_started, pid}, _}} -> {:ok, pid};

View File

@ -97,17 +97,13 @@ defmodule RabbitMQ.CLI.Core.Helpers do
end end
end end
def require_rabbit(opts) do
try_load_rabbit_code(opts)
end
def require_rabbit_and_plugins(opts) do def require_rabbit_and_plugins(opts) do
with :ok <- try_load_rabbit_code(opts), with :ok <- require_rabbit(opts),
:ok <- try_load_rabbit_plugins(opts), :ok <- add_plugins_to_load_path(opts),
do: :ok do: :ok
end end
defp try_load_rabbit_code(opts) do def require_rabbit(opts) do
home = Config.get_option(:rabbitmq_home, opts) home = Config.get_option(:rabbitmq_home, opts)
case home do case home do
nil -> nil ->
@ -128,50 +124,56 @@ defmodule RabbitMQ.CLI.Core.Helpers do
end end
end end
defp try_load_rabbit_plugins(opts) do def add_plugins_to_load_path(opts) do
with {:ok, plugins_dir} <- plugins_dir(opts) with {:ok, plugins_dir} <- plugins_dir(opts)
do do
String.split(to_string(plugins_dir), separator()) String.split(to_string(plugins_dir), separator())
|> |>
Enum.map(&try_load_plugins_from_directory/1) Enum.map(&add_directory_plugins_to_load_path/1)
:ok :ok
end end
end end
defp try_load_plugins_from_directory(directory_with_plugins_inside_it) do def add_directory_plugins_to_load_path(directory_with_plugins_inside_it) do
with {:ok, files} <- File.ls(directory_with_plugins_inside_it) with {:ok, files} <- File.ls(directory_with_plugins_inside_it)
do do
Enum.filter_map(files, Enum.map(files,
fn(filename) -> String.ends_with?(filename, [".ez"]) end, fn(filename) ->
fn(archive) -> cond do
## Check that the .app file is present and take the app name from there String.ends_with?(filename, [".ez"]) ->
{:ok, ez_files} = :zip.list_dir(String.to_charlist(Path.join([directory_with_plugins_inside_it, archive]))) Path.join([directory_with_plugins_inside_it, filename])
case find_dot_app(ez_files) do |> String.to_charlist
:not_found -> :ok |> add_archive_code_path();
dot_app -> File.dir?(filename) ->
app_name = Path.basename(dot_app, ".app") Path.join([directory_with_plugins_inside_it, filename])
ebin_dir = Path.join([directory_with_plugins_inside_it, Path.dirname(dot_app)]) |> add_dir_code_path();
ebin_dir |> Code.append_path() true ->
app_name |> String.to_atom() |> Application.load() {:error, {:not_a_plugin, filename}}
end end
end) end)
end end
end end
defp find_dot_app([head | tail]) when Record.is_record(head, :zip_file) do defp add_archive_code_path(ez_dir) do
name = :erlang.element(2, head) case :erl_prim_loader.list_dir(ez_dir) do
case Regex.match?(~r/(.+)\/ebin\/(.+)\.app$/, to_string name) do {:ok, [app_dir]} ->
true -> app_in_ez = :filename.join(ez_dir, app_dir)
name add_dir_code_path(app_in_ez);
false -> _ -> {:error, :no_app_dir}
find_dot_app(tail)
end end
end end
defp find_dot_app([_head | tail]) do
find_dot_app(tail) defp add_dir_code_path(app_dir) do
end case :erl_prim_loader.list_dir(app_dir) do
defp find_dot_app([]) do {:ok, list} ->
:not_found case Enum.member?(list, 'ebin') do
true ->
ebin_dir = :filename.join(app_dir, 'ebin')
Code.append_path(ebin_dir)
false -> {:error, :no_ebin}
end;
_ -> {:error, :app_dir_empty}
end
end end
def require_mnesia_dir(opts) do def require_mnesia_dir(opts) do

View File

@ -30,8 +30,6 @@ defmodule RabbitMQ.CLI.Core.Parser do
def parse(input) do def parse(input) do
{parsed_args, options, invalid} = parse_global(input) {parsed_args, options, invalid} = parse_global(input)
CommandModules.load(options)
{command_name, command_module, arguments} = look_up_command(parsed_args, options) {command_name, command_module, arguments} = look_up_command(parsed_args, options)
case command_module do case command_module do
@ -53,10 +51,20 @@ defmodule RabbitMQ.CLI.Core.Parser do
defp look_up_command(parsed_args, options) do defp look_up_command(parsed_args, options) do
case parsed_args do case parsed_args do
[cmd_name | arguments] -> [cmd_name | arguments] ->
module_map = CommandModules.module_map ## This is an optimisation for pluggable command discovery.
command = module_map[cmd_name] || ## Most of the time a command will be from rabbitmqctl application
command_alias(cmd_name, module_map, options) || ## so there is not point in scanning plugins for potential commands
command_suggestion(cmd_name, module_map) CommandModules.load_core(options)
core_commands = CommandModules.module_map_core
command = case core_commands[cmd_name] do
nil ->
CommandModules.load(options)
module_map = CommandModules.module_map
module_map[cmd_name] ||
command_alias(cmd_name, module_map, options) ||
command_suggestion(cmd_name, module_map);
c -> c
end
{cmd_name, command, arguments} {cmd_name, command, arguments}
[] -> [] ->
{"", nil, []} {"", nil, []}

View File

@ -28,14 +28,14 @@ defmodule RabbitMQ.CLI.Ctl.Commands.HelpCommand do
def scopes(), do: [:ctl, :diagnostics, :plugins] def scopes(), do: [:ctl, :diagnostics, :plugins]
def run([command_name], opts) do def run([command_name], opts) do
case CommandModules.is_command?(command_name) do CommandModules.load(opts)
true -> case CommandModules.module_map[command_name] do
command = CommandModules.module_map[command_name] nil ->
all_usage(opts);
command ->
Enum.join([base_usage(command, opts)] ++ Enum.join([base_usage(command, opts)] ++
options_usage() ++ options_usage() ++
input_types(command), "\n"); input_types(command), "\n")
false ->
all_usage(opts)
end end
end end
def run(_, opts) do def run(_, opts) do
@ -51,6 +51,7 @@ defmodule RabbitMQ.CLI.Ctl.Commands.HelpCommand do
end end
def all_usage(opts) do def all_usage(opts) do
CommandModules.load(opts)
Enum.join(tool_usage(program_name(opts)) ++ Enum.join(tool_usage(program_name(opts)) ++
options_usage() ++ options_usage() ++
commands() ++ commands() ++

View File

@ -35,8 +35,7 @@ defmodule RabbitMQCtl do
auto_complete(script_basename, args) auto_complete(script_basename, args)
end end
def main(unparsed_command) do def main(unparsed_command) do
unparsed_command exec_command(unparsed_command, fn(command, output, options) ->
|> exec_command(fn(command, output, options) ->
formatter = get_formatter(command, options) formatter = get_formatter(command, options)
printer = get_printer(options) printer = get_printer(options)

View File

@ -110,45 +110,6 @@ defmodule CommandModulesTest do
end end
## ------------------- is_command?/1 tests --------------------
test "a valid implemented command returns true" do
set_scope(:ctl)
@subject.load(%{})
assert @subject.is_command?("status") == true
end
test "an invalid command returns false" do
set_scope(:ctl)
@subject.load(%{})
assert @subject.is_command?("quack") == false
end
test "a nil returns false" do
set_scope(:ctl)
@subject.load(%{})
assert @subject.is_command?(nil) == false
end
test "an empty array returns false" do
set_scope(:ctl)
@subject.load(%{})
assert @subject.is_command?([]) == false
end
test "an non-empty array tests the first element" do
set_scope(:ctl)
@subject.load(%{})
assert @subject.is_command?(["status", "quack"]) == true
assert @subject.is_command?(["quack", "status"]) == false
end
test "a non-string list returns false" do
set_scope(:ctl)
@subject.load(%{})
assert @subject.is_command?([{"status", "quack"}, {4, "Fantastic"}]) == false
end
## ------------------- commands/0 tests -------------------- ## ------------------- commands/0 tests --------------------
test "command_modules has existing commands" do test "command_modules has existing commands" do

View File

@ -118,23 +118,25 @@ test "RabbitMQ hostname is properly formed" do
## ------------------- require_rabbit/1 tests -------------------- ## ------------------- require_rabbit/1 tests --------------------
test "load plugin with version number in filename" do test "locate plugin with version number in filename" do
plugins_directory_03 = fixture_plugins_path("plugins-subdirectory-03") plugins_directory_03 = fixture_plugins_path("plugins-subdirectory-03")
rabbitmq_home = :rabbit_misc.rpc_call(node(), :code, :lib_dir, [:rabbit]) rabbitmq_home = :rabbit_misc.rpc_call(node(), :code, :lib_dir, [:rabbit])
opts = %{plugins_dir: to_string(plugins_directory_03), opts = %{plugins_dir: to_string(plugins_directory_03),
rabbitmq_home: rabbitmq_home} rabbitmq_home: rabbitmq_home}
assert Enum.member?(Application.loaded_applications(), {:mock_rabbitmq_plugins_03, 'New project', '0.1.0'}) == false assert Enum.member?(Application.loaded_applications(), {:mock_rabbitmq_plugins_03, 'New project', '0.1.0'}) == false
@subject.require_rabbit_and_plugins(opts) @subject.require_rabbit_and_plugins(opts)
Application.load(:mock_rabbitmq_plugins_03)
assert Enum.member?(Application.loaded_applications(), {:mock_rabbitmq_plugins_03, 'New project', '0.1.0'}) assert Enum.member?(Application.loaded_applications(), {:mock_rabbitmq_plugins_03, 'New project', '0.1.0'})
end end
test "load plugin without version number in filename" do test "locate plugin without version number in filename" do
plugins_directory_04 = fixture_plugins_path("plugins-subdirectory-04") plugins_directory_04 = fixture_plugins_path("plugins-subdirectory-04")
rabbitmq_home = :rabbit_misc.rpc_call(node(), :code, :lib_dir, [:rabbit]) rabbitmq_home = :rabbit_misc.rpc_call(node(), :code, :lib_dir, [:rabbit])
opts = %{plugins_dir: to_string(plugins_directory_04), opts = %{plugins_dir: to_string(plugins_directory_04),
rabbitmq_home: rabbitmq_home} rabbitmq_home: rabbitmq_home}
assert Enum.member?(Application.loaded_applications(), {:mock_rabbitmq_plugins_04, 'New project', 'rolling'}) == false assert Enum.member?(Application.loaded_applications(), {:mock_rabbitmq_plugins_04, 'New project', 'rolling'}) == false
@subject.require_rabbit_and_plugins(opts) @subject.require_rabbit_and_plugins(opts)
Application.load(:mock_rabbitmq_plugins_04)
assert Enum.member?(Application.loaded_applications(), {:mock_rabbitmq_plugins_04, 'New project', 'rolling'}) assert Enum.member?(Application.loaded_applications(), {:mock_rabbitmq_plugins_04, 'New project', 'rolling'})
end end