summaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorJean-Sébastien Pédron <jean-sebastien@rabbitmq.com>2019-05-15 16:27:51 +0200
committerJean-Sébastien Pédron <jean-sebastien@rabbitmq.com>2019-12-11 17:48:49 +0100
commit68c30553ccf306325a64b1fe6069e6bcc9c26b41 (patch)
tree1026b9693d79f723c474c3de2e782215fb002926 /apps
parent5e6043ac59abbafd62bae2d3721a01ed232fc5f3 (diff)
downloadrabbitmq-server-git-68c30553ccf306325a64b1fe6069e6bcc9c26b41.tar.gz
Move most of shell scripts to Erlang code
A large part of the rabbitmq-server(8) and CLI scripts, both Bourne-shell and Windows Batch versions, was moved to Erlang code and the RabbitMQ startup procedure was reorganized to be closer to a regular Erlang application. A new application called `rabbitmq_prelaunch` is responsible for: 1. Querying the environment variables to initialize important variables (using the new `rabbit_env` module in rabbitmq-common). 2. Checking the compatibility with the Erlang/OTP runtime. 3. Configuring Erlang distribution. 5. Writing the PID file. The application is started early (i.e. it is started before `rabbit`). The `rabbit` application runs the second half of the prelaunch sequence at the beginning of the application `start()` function. This second phase is responsible for the following steps: 1. Preparing the feature flags registry. 2. Reading and validating the configuration. 3. Configuring logging. 4. Running the various cluster checks. In addition to this prelaunch sequence, the `rabbit` application start procedure ends with a "postlaunch" sequence which takes care of starting enabled plugins. Thanks to this, RabbitMQ can be started with `application:start(rabbit)` as any other Erlang application. The only caveats are: * Mnesia must be stopped at the time `rabbit_prelaunch` is started, and must remain stopped when `rabbit` is started, to allow the Erlang distribution setup and cluster checks. `rabbit` takes care of starting Mnesia. * Likewise for Ra, because it relies on the `ra` application environment to be configured. Transitioning from scripts to Erlang code has the following benefits: * RabbitMQ start behavior should be identical between Unix and Windows. Also, features should be on par now. For instance, RabbitMQ now writes a PID file on Windows, like it always did on Unix-based systems. * The difference between published packages and a development environment are greatly reduced. In fact, we removed all the "if this is a dev working copy, then ..." blocks. As part of that, the `rabbit` application is now treated like its plugins: it is packaged as an `.ez` archive and written to the `plugins` directory (even though it is not technically a plugin). Also in a development copy, the CLI is copied to the top-level project. So when testing a plugin for instance, the CLI to use is `sbin/rabbitmqctl` in the current directory, not the master copy in `rabbit/scripts`. * As a consequence of the previous two points, maintaining and testing on Windows is now made easy. It should even be possible to setup CI on Windows. * There are less issues with paths containing non-US-ASCII characters, which can happen on Windows because RabbitMQ stores its data in user directories by default. This process brings at least one more benefit: we now have early logging during this prelaunch phase, which eases diagnostics and debugging. There are also behavior changes: * The new format configuration files used to be converted to an Erlang-term-based file by the Cuttlefish CLI. To do that, configuration schemas were copied to a temporary directory and the generated configuration file was written to RabbitMQ data directory. Now, Cuttlefish is used as a library: everything happens in memory. No schemas are copied, no generated configuration is written to disk. * The PID file is removed when the Erlang VM exits. * The `rabbit_config` module was trimmed significantly because most of the configuration handling is done in `rabbit_prelaunch_conf` now. * The RabbitMQ nodename does not appear on the command line, therefore it is missing from ps(1) and top(1) output. * The `rabbit:start()` function will probably behave differently in some ways because it defers everything to the Erlang application controller (instead of reimplementing it).
Diffstat (limited to 'apps')
-rw-r--r--apps/rabbitmq_prelaunch/.gitignore12
-rw-r--r--apps/rabbitmq_prelaunch/Makefile11
-rw-r--r--apps/rabbitmq_prelaunch/src/rabbit_prelaunch.erl274
-rw-r--r--apps/rabbitmq_prelaunch/src/rabbit_prelaunch_app.erl11
-rw-r--r--apps/rabbitmq_prelaunch/src/rabbit_prelaunch_dist.erl102
-rw-r--r--apps/rabbitmq_prelaunch/src/rabbit_prelaunch_early_logging.erl61
-rw-r--r--apps/rabbitmq_prelaunch/src/rabbit_prelaunch_erlang_compat.erl47
-rw-r--r--apps/rabbitmq_prelaunch/src/rabbit_prelaunch_errors.erl109
-rw-r--r--apps/rabbitmq_prelaunch/src/rabbit_prelaunch_sup.erl17
9 files changed, 644 insertions, 0 deletions
diff --git a/apps/rabbitmq_prelaunch/.gitignore b/apps/rabbitmq_prelaunch/.gitignore
new file mode 100644
index 0000000000..adca0d7655
--- /dev/null
+++ b/apps/rabbitmq_prelaunch/.gitignore
@@ -0,0 +1,12 @@
+*~
+.sw?
+.*.sw?
+*.beam
+*.coverdata
+/ebin/
+/.erlang.mk/
+/rabbitmq_prelaunch.d
+/xrefr
+
+# Dialyzer
+*.plt
diff --git a/apps/rabbitmq_prelaunch/Makefile b/apps/rabbitmq_prelaunch/Makefile
new file mode 100644
index 0000000000..572f7703d4
--- /dev/null
+++ b/apps/rabbitmq_prelaunch/Makefile
@@ -0,0 +1,11 @@
+PROJECT = rabbitmq_prelaunch
+PROJECT_DESCRIPTION = RabbitMQ prelaunch setup
+PROJECT_VERSION = 1.0.0
+PROJECT_MOD = rabbit_prelaunch_app
+
+DEPS = rabbit_common lager
+
+DEP_PLUGINS = rabbit_common/mk/rabbitmq-build.mk
+
+include ../../rabbitmq-components.mk
+include ../../erlang.mk
diff --git a/apps/rabbitmq_prelaunch/src/rabbit_prelaunch.erl b/apps/rabbitmq_prelaunch/src/rabbit_prelaunch.erl
new file mode 100644
index 0000000000..5c3d56cd50
--- /dev/null
+++ b/apps/rabbitmq_prelaunch/src/rabbit_prelaunch.erl
@@ -0,0 +1,274 @@
+-module(rabbit_prelaunch).
+
+-include_lib("eunit/include/eunit.hrl").
+
+-export([run_prelaunch_first_phase/0,
+ assert_mnesia_is_stopped/0,
+ get_context/0,
+ get_boot_state/0,
+ set_boot_state/1,
+ is_boot_state_reached/1,
+ wait_for_boot_state/1,
+ wait_for_boot_state/2,
+ get_stop_reason/0,
+ set_stop_reason/1,
+ clear_stop_reason/0,
+ is_initial_pass/0,
+ initial_pass_finished/0,
+ shutdown_func/1]).
+
+-define(PT_KEY_CONTEXT, {?MODULE, context}).
+-define(PT_KEY_BOOT_STATE, {?MODULE, boot_state}).
+-define(PT_KEY_INITIAL_PASS, {?MODULE, initial_pass_finished}).
+-define(PT_KEY_SHUTDOWN_FUNC, {?MODULE, chained_shutdown_func}).
+-define(PT_KEY_STOP_REASON, {?MODULE, stop_reason}).
+
+run_prelaunch_first_phase() ->
+ try
+ do_run()
+ catch
+ throw:{error, _} = Error ->
+ rabbit_prelaunch_errors:log_error(Error),
+ set_stop_reason(Error),
+ set_boot_state(stopped),
+ Error;
+ Class:Exception:Stacktrace ->
+ rabbit_prelaunch_errors:log_exception(
+ Class, Exception, Stacktrace),
+ Error = {error, Exception},
+ set_stop_reason(Error),
+ set_boot_state(stopped),
+ Error
+ end.
+
+do_run() ->
+ %% Indicate RabbitMQ is booting.
+ clear_stop_reason(),
+ set_boot_state(booting),
+
+ %% Configure dbg if requested.
+ rabbit_prelaunch_early_logging:enable_quick_dbg(rabbit_env:dbg_config()),
+
+ %% We assert Mnesia is stopped before we run the prelaunch
+ %% phases.
+ %%
+ %% We need this because our cluster consistency check (in the second
+ %% phase) depends on Mnesia not being started before it has a chance
+ %% to run.
+ %%
+ %% Also, in the initial pass, we don't want Mnesia to run before
+ %% Erlang distribution is configured.
+ assert_mnesia_is_stopped(),
+
+ %% Get informations to setup logging.
+ Context0 = rabbit_env:get_context_before_logging_init(),
+ ?assertMatch(#{}, Context0),
+
+ %% Setup logging for the prelaunch phase.
+ ok = rabbit_prelaunch_early_logging:setup_early_logging(Context0, true),
+
+ IsInitialPass = is_initial_pass(),
+ case IsInitialPass of
+ true ->
+ rabbit_log_prelaunch:debug(""),
+ rabbit_log_prelaunch:debug(
+ "== Prelaunch phase [1/2] (initial pass) =="),
+ rabbit_log_prelaunch:debug("");
+ false ->
+ rabbit_log_prelaunch:debug(""),
+ rabbit_log_prelaunch:debug("== Prelaunch phase [1/2] =="),
+ rabbit_log_prelaunch:debug("")
+ end,
+ rabbit_env:log_process_env(),
+
+ %% Load rabbitmq-env.conf, redo logging setup and continue.
+ Context1 = rabbit_env:get_context_after_logging_init(Context0),
+ ?assertMatch(#{}, Context1),
+ ok = rabbit_prelaunch_early_logging:setup_early_logging(Context1, true),
+ rabbit_env:log_process_env(),
+
+ %% Complete context now that we have the final environment loaded.
+ Context2 = rabbit_env:get_context_after_reloading_env(Context1),
+ ?assertMatch(#{}, Context2),
+ store_context(Context2),
+ rabbit_env:log_context(Context2),
+ ok = setup_shutdown_func(),
+
+ Context = Context2#{initial_pass => IsInitialPass},
+
+ rabbit_env:context_to_code_path(Context),
+ rabbit_env:context_to_app_env_vars(Context),
+
+ %% 1. Erlang/OTP compatibility check.
+ ok = rabbit_prelaunch_erlang_compat:check(Context),
+
+ %% 2. Erlang distribution check + start.
+ ok = rabbit_prelaunch_dist:setup(Context),
+
+ %% 3. Write PID file.
+ rabbit_log_prelaunch:debug(""),
+ _ = write_pid_file(Context),
+ ignore.
+
+assert_mnesia_is_stopped() ->
+ ?assertNot(lists:keymember(mnesia, 1, application:which_applications())).
+
+store_context(Context) when is_map(Context) ->
+ persistent_term:put(?PT_KEY_CONTEXT, Context).
+
+get_context() ->
+ case persistent_term:get(?PT_KEY_CONTEXT, undefined) of
+ undefined -> undefined;
+ Context -> Context#{initial_pass => is_initial_pass()}
+ end.
+
+get_boot_state() ->
+ persistent_term:get(?PT_KEY_BOOT_STATE, stopped).
+
+set_boot_state(stopped) ->
+ rabbit_log_prelaunch:debug("Change boot state to `stopped`"),
+ persistent_term:erase(?PT_KEY_BOOT_STATE);
+set_boot_state(BootState) ->
+ rabbit_log_prelaunch:debug("Change boot state to `~s`", [BootState]),
+ ?assert(is_boot_state_valid(BootState)),
+ persistent_term:put(?PT_KEY_BOOT_STATE, BootState).
+
+wait_for_boot_state(BootState) ->
+ wait_for_boot_state(BootState, infinity).
+
+wait_for_boot_state(BootState, Timeout) ->
+ ?assert(is_boot_state_valid(BootState)),
+ wait_for_boot_state1(BootState, Timeout).
+
+wait_for_boot_state1(BootState, infinity = Timeout) ->
+ case is_boot_state_reached(BootState) of
+ true -> ok;
+ false -> wait_for_boot_state1(BootState, Timeout)
+ end;
+wait_for_boot_state1(BootState, Timeout)
+ when is_integer(Timeout) andalso Timeout >= 0 ->
+ case is_boot_state_reached(BootState) of
+ true -> ok;
+ false -> Wait = 200,
+ timer:sleep(Wait),
+ wait_for_boot_state1(BootState, Timeout - Wait)
+ end;
+wait_for_boot_state1(_, _) ->
+ {error, timeout}.
+
+boot_state_idx(stopped) -> 0;
+boot_state_idx(booting) -> 1;
+boot_state_idx(ready) -> 2;
+boot_state_idx(stopping) -> 3;
+boot_state_idx(_) -> undefined.
+
+is_boot_state_valid(BootState) ->
+ is_integer(boot_state_idx(BootState)).
+
+is_boot_state_reached(TargetBootState) ->
+ is_boot_state_reached(get_boot_state(), TargetBootState).
+
+is_boot_state_reached(CurrentBootState, CurrentBootState) ->
+ true;
+is_boot_state_reached(stopping, stopped) ->
+ false;
+is_boot_state_reached(_CurrentBootState, stopped) ->
+ true;
+is_boot_state_reached(stopped, _TargetBootState) ->
+ true;
+is_boot_state_reached(CurrentBootState, TargetBootState) ->
+ boot_state_idx(TargetBootState) =< boot_state_idx(CurrentBootState).
+
+get_stop_reason() ->
+ persistent_term:get(?PT_KEY_STOP_REASON, undefined).
+
+set_stop_reason(Reason) ->
+ case get_stop_reason() of
+ undefined ->
+ rabbit_log_prelaunch:debug("Set stop reason to: ~p", [Reason]),
+ persistent_term:put(?PT_KEY_STOP_REASON, Reason);
+ _ ->
+ ok
+ end.
+
+clear_stop_reason() ->
+ persistent_term:erase(?PT_KEY_STOP_REASON).
+
+is_initial_pass() ->
+ not persistent_term:get(?PT_KEY_INITIAL_PASS, false).
+
+initial_pass_finished() ->
+ persistent_term:put(?PT_KEY_INITIAL_PASS, true).
+
+setup_shutdown_func() ->
+ ThisMod = ?MODULE,
+ ThisFunc = shutdown_func,
+ ExistingShutdownFunc = application:get_env(kernel, shutdown_func),
+ case ExistingShutdownFunc of
+ {ok, {ThisMod, ThisFunc}} ->
+ ok;
+ {ok, {ExistingMod, ExistingFunc}} ->
+ rabbit_log_prelaunch:debug(
+ "Setting up kernel shutdown function: ~s:~s/1 "
+ "(chained with ~s:~s/1)",
+ [ThisMod, ThisFunc, ExistingMod, ExistingFunc]),
+ ok = persistent_term:put(
+ ?PT_KEY_SHUTDOWN_FUNC,
+ ExistingShutdownFunc),
+ ok = record_kernel_shutdown_func(ThisMod, ThisFunc);
+ _ ->
+ rabbit_log_prelaunch:debug(
+ "Setting up kernel shutdown function: ~s:~s/1",
+ [ThisMod, ThisFunc]),
+ ok = record_kernel_shutdown_func(ThisMod, ThisFunc)
+ end.
+
+record_kernel_shutdown_func(Mod, Func) ->
+ application:set_env(
+ kernel, shutdown_func, {Mod, Func},
+ [{persistent, true}]).
+
+shutdown_func(Reason) ->
+ rabbit_log_prelaunch:debug(
+ "Running ~s:shutdown_func() as part of `kernel` shutdown", [?MODULE]),
+ Context = get_context(),
+ remove_pid_file(Context),
+ ChainedShutdownFunc = persistent_term:get(
+ ?PT_KEY_SHUTDOWN_FUNC,
+ undefined),
+ case ChainedShutdownFunc of
+ {ChainedMod, ChainedFunc} -> ChainedMod:ChainedFunc(Reason);
+ _ -> ok
+ end.
+
+write_pid_file(#{pid_file := PidFile}) ->
+ rabbit_log_prelaunch:debug("Writing PID file: ~s", [PidFile]),
+ case filelib:ensure_dir(PidFile) of
+ ok ->
+ OSPid = os:getpid(),
+ case file:write_file(PidFile, OSPid) of
+ ok ->
+ ok;
+ {error, Reason} = Error ->
+ rabbit_log_prelaunch:warning(
+ "Failed to write PID file \"~s\": ~s",
+ [PidFile, file:format_error(Reason)]),
+ Error
+ end;
+ {error, Reason} = Error ->
+ rabbit_log_prelaunch:warning(
+ "Failed to create PID file \"~s\" directory: ~s",
+ [PidFile, file:format_error(Reason)]),
+ Error
+ end;
+write_pid_file(_) ->
+ ok.
+
+remove_pid_file(#{pid_file := PidFile, keep_pid_file_on_exit := true}) ->
+ rabbit_log_prelaunch:debug("Keeping PID file: ~s", [PidFile]),
+ ok;
+remove_pid_file(#{pid_file := PidFile}) ->
+ rabbit_log_prelaunch:debug("Deleting PID file: ~s", [PidFile]),
+ _ = file:delete(PidFile);
+remove_pid_file(_) ->
+ ok.
diff --git a/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_app.erl b/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_app.erl
new file mode 100644
index 0000000000..cef7f05e77
--- /dev/null
+++ b/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_app.erl
@@ -0,0 +1,11 @@
+-module(rabbit_prelaunch_app).
+-behaviour(application).
+
+-export([start/2]).
+-export([stop/1]).
+
+start(_Type, _Args) ->
+ rabbit_prelaunch_sup:start_link().
+
+stop(_State) ->
+ ok.
diff --git a/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_dist.erl b/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_dist.erl
new file mode 100644
index 0000000000..70f99feba6
--- /dev/null
+++ b/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_dist.erl
@@ -0,0 +1,102 @@
+-module(rabbit_prelaunch_dist).
+
+-export([setup/1]).
+
+setup(#{nodename := Node, nodename_type := NameType} = Context) ->
+ rabbit_log_prelaunch:debug(""),
+ rabbit_log_prelaunch:debug("== Erlang distribution =="),
+ rabbit_log_prelaunch:debug("Rqeuested node name: ~s (type: ~s)",
+ [Node, NameType]),
+ case node() of
+ nonode@nohost ->
+ ok = rabbit_nodes_common:ensure_epmd(),
+ ok = dist_port_range_check(Context),
+ ok = dist_port_use_check(Context),
+ ok = duplicate_node_check(Context),
+
+ ok = do_setup(Context);
+ Node ->
+ rabbit_log_prelaunch:debug(
+ "Erlang distribution already running", []),
+ ok;
+ Unexpected ->
+ throw({error, {erlang_dist_running_with_unexpected_nodename,
+ Unexpected, Node}})
+ end,
+ ok.
+
+do_setup(#{nodename := Node, nodename_type := NameType}) ->
+ rabbit_log_prelaunch:debug("Starting Erlang distribution", []),
+ case application:get_env(kernel, net_ticktime) of
+ {ok, Ticktime} when is_integer(Ticktime) andalso Ticktime >= 1 ->
+ %% The value passed to net_kernel:start/1 is the
+ %% "minimum transition traffic interval" as defined in
+ %% net_kernel:set_net_ticktime/1.
+ MTTI = Ticktime * 1000 div 4,
+ {ok, _} = net_kernel:start([Node, NameType, MTTI]);
+ _ ->
+ {ok, _} = net_kernel:start([Node, NameType])
+ end,
+ ok.
+
+%% Check whether a node with the same name is already running
+duplicate_node_check(#{split_nodename := {NodeName, NodeHost}}) ->
+ rabbit_log_prelaunch:debug(
+ "Checking if node name ~s is already used", [NodeName]),
+ PrelaunchName = rabbit_nodes:make(
+ {NodeName ++ "_prelaunch_" ++ os:getpid(),
+ "localhost"}),
+ {ok, _} = net_kernel:start([PrelaunchName, shortnames]),
+ case rabbit_nodes:names(NodeHost) of
+ {ok, NamePorts} ->
+ case proplists:is_defined(NodeName, NamePorts) of
+ true ->
+ throw({error, {duplicate_node_name, NodeName, NodeHost}});
+ false ->
+ net_kernel:stop(),
+ ok
+ end;
+ {error, EpmdReason} ->
+ throw({error, {epmd_error, NodeHost, EpmdReason}})
+ end.
+
+dist_port_range_check(#{erlang_dist_tcp_port := DistTcpPort}) ->
+ rabbit_log_prelaunch:debug(
+ "Checking if TCP port ~b is valid", [DistTcpPort]),
+ case DistTcpPort of
+ _ when DistTcpPort < 1 orelse DistTcpPort > 65535 ->
+ throw({error, {invalid_dist_port_range, DistTcpPort}});
+ _ ->
+ ok
+ end.
+
+dist_port_use_check(#{split_nodename := {_, NodeHost},
+ erlang_dist_tcp_port := DistTcpPort}) ->
+ rabbit_log_prelaunch:debug(
+ "Checking if TCP port ~b is available", [DistTcpPort]),
+ dist_port_use_check_ipv4(NodeHost, DistTcpPort).
+
+dist_port_use_check_ipv4(NodeHost, Port) ->
+ case gen_tcp:listen(Port, [inet, {reuseaddr, true}]) of
+ {ok, Sock} -> gen_tcp:close(Sock);
+ {error, einval} -> dist_port_use_check_ipv6(NodeHost, Port);
+ {error, _} -> dist_port_use_check_fail(Port, NodeHost)
+ end.
+
+dist_port_use_check_ipv6(NodeHost, Port) ->
+ case gen_tcp:listen(Port, [inet6, {reuseaddr, true}]) of
+ {ok, Sock} -> gen_tcp:close(Sock);
+ {error, _} -> dist_port_use_check_fail(Port, NodeHost)
+ end.
+
+-spec dist_port_use_check_fail(non_neg_integer(), string()) ->
+ no_return().
+
+dist_port_use_check_fail(Port, Host) ->
+ {ok, Names} = rabbit_nodes:names(Host),
+ case [N || {N, P} <- Names, P =:= Port] of
+ [] ->
+ throw({error, {dist_port_already_used, Port, not_erlang, Host}});
+ [Name] ->
+ throw({error, {dist_port_already_used, Port, Name, Host}})
+ end.
diff --git a/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_early_logging.erl b/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_early_logging.erl
new file mode 100644
index 0000000000..66883e501b
--- /dev/null
+++ b/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_early_logging.erl
@@ -0,0 +1,61 @@
+-module(rabbit_prelaunch_early_logging).
+
+-export([setup_early_logging/2,
+ enable_quick_dbg/1,
+ use_colored_logging/0,
+ use_colored_logging/1]).
+
+-define(SINK, rabbit_log_prelaunch_lager_event).
+
+setup_early_logging(#{log_levels := undefined} = Context,
+ LagerEventToStdout) ->
+ setup_early_logging(Context#{log_levels => get_default_log_level()},
+ LagerEventToStdout);
+setup_early_logging(Context, LagerEventToStdout) ->
+ case lists:member(?SINK, lager:list_all_sinks()) of
+ true -> ok;
+ false -> do_setup_early_logging(Context, LagerEventToStdout)
+ end.
+
+get_default_log_level() ->
+ #{"prelaunch" => warning}.
+
+do_setup_early_logging(#{log_levels := LogLevels} = Context,
+ LagerEventToStdout) ->
+ LogLevel = case LogLevels of
+ #{"prelaunch" := Level} -> Level;
+ #{global := Level} -> Level;
+ _ -> warning
+ end,
+ Colored = use_colored_logging(Context),
+ ConsoleBackend = lager_console_backend,
+ ConsoleOptions = [{level, LogLevel}],
+ application:set_env(lager, colored, Colored),
+ case LagerEventToStdout of
+ true ->
+ lager_app:start_handler(
+ lager_event, ConsoleBackend, ConsoleOptions);
+ false ->
+ ok
+ end,
+ lager_app:configure_sink(
+ ?SINK,
+ [{handlers, [{ConsoleBackend, ConsoleOptions}]}]),
+ ok.
+
+use_colored_logging() ->
+ use_colored_logging(rabbit_prelaunch:get_context()).
+
+use_colored_logging(#{log_levels := #{color := true},
+ output_supports_colors := true}) ->
+ true;
+use_colored_logging(_) ->
+ false.
+
+enable_quick_dbg(#{dbg_output := Output, dbg_mods := Mods}) ->
+ case Output of
+ stdout -> {ok, _} = dbg:tracer();
+ _ -> {ok, _} = dbg:tracer(port, dbg:trace_port(file, Output))
+ end,
+ {ok, _} = dbg:p(all, c),
+ lists:foreach(fun(M) -> {ok, _} = dbg:tp(M, cx) end, Mods).
diff --git a/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_erlang_compat.erl b/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_erlang_compat.erl
new file mode 100644
index 0000000000..1e8fe2690d
--- /dev/null
+++ b/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_erlang_compat.erl
@@ -0,0 +1,47 @@
+-module(rabbit_prelaunch_erlang_compat).
+
+-export([check/1]).
+
+-define(OTP_MINIMUM, "21.3").
+-define(ERTS_MINIMUM, "10.3").
+
+check(_Context) ->
+ rabbit_log_prelaunch:debug(""),
+ rabbit_log_prelaunch:debug("== Erlang/OTP compatibility check =="),
+
+ ERTSVer = erlang:system_info(version),
+ OTPRel = rabbit_misc:otp_release(),
+ rabbit_log_prelaunch:debug(
+ "Requiring: Erlang/OTP ~s (ERTS ~s)", [?OTP_MINIMUM, ?ERTS_MINIMUM]),
+ rabbit_log_prelaunch:debug(
+ "Running: Erlang/OTP ~s (ERTS ~s)", [OTPRel, ERTSVer]),
+
+ case rabbit_misc:version_compare(?ERTS_MINIMUM, ERTSVer, lte) of
+ true when ?ERTS_MINIMUM =/= ERTSVer ->
+ rabbit_log_prelaunch:debug(
+ "Erlang/OTP version requirement satisfied"),
+ ok;
+ true when ?ERTS_MINIMUM =:= ERTSVer andalso ?OTP_MINIMUM =< OTPRel ->
+ %% When a critical regression or bug is found, a new OTP
+ %% release can be published without changing the ERTS
+ %% version. For instance, this is the case with R16B03 and
+ %% R16B03-1.
+ %%
+ %% In this case, we compare the release versions
+ %% alphabetically.
+ ok;
+ _ ->
+ Msg =
+ "This RabbitMQ version cannot run on Erlang ~s (erts ~s): "
+ "minimum required version is ~s (erts ~s)",
+ Args = [OTPRel, ERTSVer, ?OTP_MINIMUM, ?ERTS_MINIMUM],
+ rabbit_log_prelaunch:error(Msg, Args),
+
+ %% Also print to stderr to make this more visible
+ io:format(standard_error, "Error: " ++ Msg ++ "~n", Args),
+
+ Msg2 = rabbit_misc:format(
+ "Erlang ~s or later is required, started on ~s",
+ [?OTP_MINIMUM, OTPRel]),
+ throw({error, {erlang_version_too_old, Msg2}})
+ end.
diff --git a/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_errors.erl b/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_errors.erl
new file mode 100644
index 0000000000..46392ea499
--- /dev/null
+++ b/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_errors.erl
@@ -0,0 +1,109 @@
+-module(rabbit_prelaunch_errors).
+
+-export([format_error/1,
+ format_exception/3,
+ log_error/1,
+ log_exception/3]).
+
+-define(BOOT_FAILED_HEADER,
+ "\n"
+ "BOOT FAILED\n"
+ "===========\n").
+
+-define(BOOT_FAILED_FOOTER,
+ "\n").
+
+log_error(Error) ->
+ Message = format_error(Error),
+ log_message(Message).
+
+format_error({error, {duplicate_node_name, NodeName, NodeHost}}) ->
+ rabbit_misc:format(
+ "ERROR: node with name ~p already running on ~p",
+ [NodeName, NodeHost]);
+format_error({error, {epmd_error, NodeHost, EpmdReason}}) ->
+ rabbit_misc:format(
+ "ERROR: epmd error for host ~s: ~s",
+ [NodeHost, rabbit_misc:format_inet_error(EpmdReason)]);
+format_error({error, {invalid_dist_port_range, DistTcpPort}}) ->
+ rabbit_misc:format(
+ "Invalid Erlang distribution TCP port: ~b", [DistTcpPort]);
+format_error({error, {dist_port_already_used, Port, not_erlang, Host}}) ->
+ rabbit_misc:format(
+ "ERROR: distribution port ~b in use on ~s "
+ "(by non-Erlang process?)", [Port, Host]);
+format_error({error, {dist_port_already_used, Port, Name, Host}}) ->
+ rabbit_misc:format(
+ "ERROR: distribution port ~b in use by ~s@~s", [Port, Name, Host]);
+format_error({error, {erlang_dist_running_with_unexpected_nodename,
+ Unexpected, Node}}) ->
+ rabbit_misc:format(
+ "Erlang distribution running with another node name (~s) "
+ "than the configured one (~s)",
+ [Unexpected, Node]);
+format_error({bad_config_entry_decoder, missing_passphrase}) ->
+ rabbit_misc:format(
+ "Missing passphrase or missing passphrase read method in "
+ "`config_entry_decoder`");
+format_error({config_decryption_error, {key, Key}, _Msg}) ->
+ rabbit_misc:format(
+ "Error while decrypting key '~p'. Please check encrypted value, "
+ "passphrase, and encryption configuration~n",
+ [Key]);
+format_error({error, {timeout_waiting_for_tables, AllNodes, _}}) ->
+ Suffix =
+ "~nBACKGROUND~n==========~n~n"
+ "This cluster node was shut down while other nodes were still running.~n"
+ "To avoid losing data, you should start the other nodes first, then~n"
+ "start this one. To force this node to start, first invoke~n"
+ "\"rabbitmqctl force_boot\". If you do so, any changes made on other~n"
+ "cluster nodes after this one was shut down may be lost.",
+ {Message, Nodes} =
+ case AllNodes -- [node()] of
+ [] -> {rabbit_misc:format(
+ "Timeout contacting cluster nodes. Since RabbitMQ was"
+ " shut down forcefully~nit cannot determine which nodes"
+ " are timing out.~n" ++ Suffix, []),
+ []};
+ Ns -> {rabbit_misc:format(
+ "Timeout contacting cluster nodes: ~p.~n" ++ Suffix,
+ [Ns]),
+ Ns}
+ end,
+ Message ++ "\n" ++ rabbit_nodes_common:diagnostics(Nodes);
+format_error({error, {cannot_log_to_file, unknown, Reason}}) ->
+ rabbit_misc:format(
+ "failed to initialised logger: ~p~n",
+ [Reason]);
+format_error({error, {cannot_log_to_file, LogFile,
+ {cannot_create_parent_dirs, _, Reason}}}) ->
+ rabbit_misc:format(
+ "failed to create parent directory for log file at '~s', reason: ~s~n",
+ [LogFile, file:format_error(Reason)]);
+format_error({error, {cannot_log_to_file, LogFile, Reason}}) ->
+ rabbit_misc:format(
+ "failed to open log file at '~s', reason: ~s",
+ [LogFile, file:format_error(Reason)]);
+format_error(Error) ->
+ rabbit_misc:format("Error during startup: ~p", [Error]).
+
+log_exception(Class, Exception, Stacktrace) ->
+ Message = format_exception(Class, Exception, Stacktrace),
+ log_message(Message).
+
+format_exception(Class, Exception, Stacktrace) ->
+ rabbit_misc:format(
+ "Exception during startup:~n~s",
+ [lager:pr_stacktrace(Stacktrace, {Class, Exception})]).
+
+log_message(Message) ->
+ Lines = string:split(
+ ?BOOT_FAILED_HEADER ++
+ Message ++
+ ?BOOT_FAILED_FOOTER,
+ [$\n],
+ all),
+ [rabbit_log_prelaunch:error("~s", [Line]) || Line <- Lines],
+ [io:format(standard_error, "~s~n", [Line]) || Line <- Lines],
+ timer:sleep(1000),
+ ok.
diff --git a/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_sup.erl b/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_sup.erl
new file mode 100644
index 0000000000..ec51989fb9
--- /dev/null
+++ b/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_sup.erl
@@ -0,0 +1,17 @@
+-module(rabbit_prelaunch_sup).
+-behaviour(supervisor).
+
+-export([start_link/0]).
+-export([init/1]).
+
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init([]) ->
+ %% `rabbit_prelaunch` does not start a process, it only configures
+ %% the node.
+ Prelaunch = #{id => prelaunch,
+ start => {rabbit_prelaunch, run_prelaunch_first_phase, []},
+ restart => transient},
+ Procs = [Prelaunch],
+ {ok, {{one_for_one, 1, 5}, Procs}}.