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
@commands_ns ~r/RabbitMQ.CLI.(.*).Commands/
def module_map do
Application.get_env(:rabbitmqctl, :commands) || load(%{})
def module_map(opts \\ %{}) do
Application.get_env(:rabbitmqctl, :commands) || load(opts)
end
def is_command?([head | _]), do: is_command?(head)
def is_command?(str), do: module_map()[str] != nil
def module_map_core(opts \\ %{}) do
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
scope = script_scope(opts)
@ -39,8 +47,47 @@ defmodule RabbitMQ.CLI.Core.CommandModules do
scopes[Config.get_option(:script_name, opts)] || :none
end
def load_commands_core(scope) do
make_module_map(ctl_modules(), scope)
end
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) ->
to_string(mod) =~ @commands_ns
and
@ -54,13 +101,6 @@ defmodule RabbitMQ.CLI.Core.CommandModules do
|> Map.new
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
false
end

View File

@ -26,14 +26,25 @@ defmodule RabbitMQ.CLI.Core.Distribution do
def start(options) do
node_name_type = Config.get_option(:longnames, options)
:rabbit_nodes.ensure_epmd()
start(node_name_type, 10, :undefined)
end
def start_as(node_name, opts) do
:rabbit_nodes.ensure_epmd()
node_name_type = Config.get_option(:longnames, opts)
:net_kernel.start([node_name, node_name_type])
def start_as(node_name, options) do
node_name_type = Config.get_option(:longnames, options)
start_with_epmd(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
#
@ -46,7 +57,7 @@ defmodule RabbitMQ.CLI.Core.Distribution do
defp start(node_name_type, attempts, _last_err) do
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
{: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
def require_rabbit(opts) do
try_load_rabbit_code(opts)
end
def require_rabbit_and_plugins(opts) do
with :ok <- try_load_rabbit_code(opts),
:ok <- try_load_rabbit_plugins(opts),
with :ok <- require_rabbit(opts),
:ok <- add_plugins_to_load_path(opts),
do: :ok
end
defp try_load_rabbit_code(opts) do
def require_rabbit(opts) do
home = Config.get_option(:rabbitmq_home, opts)
case home do
nil ->
@ -128,50 +124,56 @@ defmodule RabbitMQ.CLI.Core.Helpers do
end
end
defp try_load_rabbit_plugins(opts) do
def add_plugins_to_load_path(opts) do
with {:ok, plugins_dir} <- plugins_dir(opts)
do
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
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)
do
Enum.filter_map(files,
fn(filename) -> String.ends_with?(filename, [".ez"]) end,
fn(archive) ->
## Check that the .app file is present and take the app name from there
{:ok, ez_files} = :zip.list_dir(String.to_charlist(Path.join([directory_with_plugins_inside_it, archive])))
case find_dot_app(ez_files) do
:not_found -> :ok
dot_app ->
app_name = Path.basename(dot_app, ".app")
ebin_dir = Path.join([directory_with_plugins_inside_it, Path.dirname(dot_app)])
ebin_dir |> Code.append_path()
app_name |> String.to_atom() |> Application.load()
end
end)
Enum.map(files,
fn(filename) ->
cond do
String.ends_with?(filename, [".ez"]) ->
Path.join([directory_with_plugins_inside_it, filename])
|> String.to_charlist
|> add_archive_code_path();
File.dir?(filename) ->
Path.join([directory_with_plugins_inside_it, filename])
|> add_dir_code_path();
true ->
{:error, {:not_a_plugin, filename}}
end
end)
end
end
defp find_dot_app([head | tail]) when Record.is_record(head, :zip_file) do
name = :erlang.element(2, head)
case Regex.match?(~r/(.+)\/ebin\/(.+)\.app$/, to_string name) do
true ->
name
false ->
find_dot_app(tail)
defp add_archive_code_path(ez_dir) do
case :erl_prim_loader.list_dir(ez_dir) do
{:ok, [app_dir]} ->
app_in_ez = :filename.join(ez_dir, app_dir)
add_dir_code_path(app_in_ez);
_ -> {:error, :no_app_dir}
end
end
defp find_dot_app([_head | tail]) do
find_dot_app(tail)
end
defp find_dot_app([]) do
:not_found
defp add_dir_code_path(app_dir) do
case :erl_prim_loader.list_dir(app_dir) do
{:ok, list} ->
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
def require_mnesia_dir(opts) do

View File

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

View File

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

View File

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

View File

@ -110,45 +110,6 @@ defmodule CommandModulesTest do
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 --------------------
test "command_modules has existing commands" do

View File

@ -118,23 +118,25 @@ test "RabbitMQ hostname is properly formed" do
## ------------------- 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")
rabbitmq_home = :rabbit_misc.rpc_call(node(), :code, :lib_dir, [:rabbit])
opts = %{plugins_dir: to_string(plugins_directory_03),
rabbitmq_home: rabbitmq_home}
assert Enum.member?(Application.loaded_applications(), {:mock_rabbitmq_plugins_03, 'New project', '0.1.0'}) == false
@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'})
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")
rabbitmq_home = :rabbit_misc.rpc_call(node(), :code, :lib_dir, [:rabbit])
opts = %{plugins_dir: to_string(plugins_directory_04),
rabbitmq_home: rabbitmq_home}
assert Enum.member?(Application.loaded_applications(), {:mock_rabbitmq_plugins_04, 'New project', 'rolling'}) == false
@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'})
end