diff options
| author | Jean-Sébastien Pédron <jean-sebastien@rabbitmq.com> | 2019-05-15 16:27:51 +0200 |
|---|---|---|
| committer | Jean-Sébastien Pédron <jean-sebastien@rabbitmq.com> | 2019-12-11 17:48:49 +0100 |
| commit | 68c30553ccf306325a64b1fe6069e6bcc9c26b41 (patch) | |
| tree | 1026b9693d79f723c474c3de2e782215fb002926 /apps | |
| parent | 5e6043ac59abbafd62bae2d3721a01ed232fc5f3 (diff) | |
| download | rabbitmq-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/.gitignore | 12 | ||||
| -rw-r--r-- | apps/rabbitmq_prelaunch/Makefile | 11 | ||||
| -rw-r--r-- | apps/rabbitmq_prelaunch/src/rabbit_prelaunch.erl | 274 | ||||
| -rw-r--r-- | apps/rabbitmq_prelaunch/src/rabbit_prelaunch_app.erl | 11 | ||||
| -rw-r--r-- | apps/rabbitmq_prelaunch/src/rabbit_prelaunch_dist.erl | 102 | ||||
| -rw-r--r-- | apps/rabbitmq_prelaunch/src/rabbit_prelaunch_early_logging.erl | 61 | ||||
| -rw-r--r-- | apps/rabbitmq_prelaunch/src/rabbit_prelaunch_erlang_compat.erl | 47 | ||||
| -rw-r--r-- | apps/rabbitmq_prelaunch/src/rabbit_prelaunch_errors.erl | 109 | ||||
| -rw-r--r-- | apps/rabbitmq_prelaunch/src/rabbit_prelaunch_sup.erl | 17 |
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}}. |
