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:
parent
00a298961a
commit
62a44ac641
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, []}
|
||||
|
|
|
|||
|
|
@ -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() ++
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue