diff options
| author | Jean-Sébastien Pédron <jean-sebastien.pedron@dumbbell.fr> | 2019-02-01 17:50:49 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-02-01 17:50:49 +0100 |
| commit | ed3dd4d6257df0925ce78ad94de099304346da4c (patch) | |
| tree | 5467b45fdb1ce8b7e362519c10f724b7637e9ffd | |
| parent | a8bc2fc210130a8d989b7d7d877892466f2b802b (diff) | |
| parent | 3562975dc1b59bf4d9ddde909a36c3c98127c575 (diff) | |
| download | rabbitmq-server-git-ed3dd4d6257df0925ce78ad94de099304346da4c.tar.gz | |
Merge pull request #1818 from rabbitmq/backward-compatible-amqqueue
Feature flags support + make #amqqueue{} private
117 files changed, 7867 insertions, 2089 deletions
@@ -136,7 +136,7 @@ define PROJECT_ENV ] endef -LOCAL_DEPS = sasl mnesia os_mon inets +LOCAL_DEPS = sasl mnesia os_mon inets compiler public_key crypto ssl syntax_tools BUILD_DEPS = rabbitmq_cli syslog DEPS = ranch lager rabbit_common ra sysmon_handler TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers amqp_client meck proper @@ -231,6 +231,10 @@ ifdef CREDIT_FLOW_TRACING RMQ_ERLC_OPTS += -DCREDIT_FLOW_TRACING=true endif +ifdef DEBUG_FF +RMQ_ERLC_OPTS += -DDEBUG_QUORUM_QUEUE_FF=true +endif + ifndef USE_PROPER_QC # PropEr needs to be installed for property checking # http://proper.softlab.ntua.gr/ diff --git a/include/amqqueue.hrl b/include/amqqueue.hrl new file mode 100644 index 0000000000..7d187738d1 --- /dev/null +++ b/include/amqqueue.hrl @@ -0,0 +1,136 @@ +%% The contents of this file are subject to the Mozilla Public License +%% Version 1.1 (the "License"); you may not use this file except in +%% compliance with the License. You may obtain a copy of the License +%% at http://www.mozilla.org/MPL/ +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and +%% limitations under the License. +%% +%% The Original Code is RabbitMQ. +%% +%% The Initial Developer of the Original Code is GoPivotal, Inc. +%% Copyright (c) 2018-2019 Pivotal Software, Inc. All rights reserved. +%% + +-include("amqqueue_v1.hrl"). +-include("amqqueue_v2.hrl"). + +-define(is_amqqueue(Q), + (?is_amqqueue_v2(Q) orelse + ?is_amqqueue_v1(Q))). + +-define(amqqueue_is_auto_delete(Q), + ((?is_amqqueue_v2(Q) andalso + ?amqqueue_v2_field_auto_delete(Q) =:= true) orelse + (?is_amqqueue_v1(Q) andalso + ?amqqueue_v1_field_auto_delete(Q) =:= true))). + +-define(amqqueue_is_durable(Q), + ((?is_amqqueue_v2(Q) andalso + ?amqqueue_v2_field_durable(Q) =:= true) orelse + (?is_amqqueue_v1(Q) andalso + ?amqqueue_v1_field_durable(Q) =:= true))). + +-define(amqqueue_exclusive_owner_is(Q, Owner), + ((?is_amqqueue_v2(Q) andalso + ?amqqueue_v2_field_exclusive_owner(Q) =:= Owner) orelse + (?is_amqqueue_v1(Q) andalso + ?amqqueue_v1_field_exclusive_owner(Q) =:= Owner))). + +-define(amqqueue_exclusive_owner_is_pid(Q), + ((?is_amqqueue_v2(Q) andalso + is_pid(?amqqueue_v2_field_exclusive_owner(Q))) orelse + (?is_amqqueue_v1(Q) andalso + is_pid(?amqqueue_v1_field_exclusive_owner(Q))))). + +-define(amqqueue_state_is(Q, State), + ((?is_amqqueue_v2(Q) andalso + ?amqqueue_v2_field_state(Q) =:= State) orelse + (?is_amqqueue_v1(Q) andalso + ?amqqueue_v1_field_state(Q) =:= State))). + +-define(amqqueue_v1_type, classic). + +-define(amqqueue_is_classic(Q), + ((?is_amqqueue_v2(Q) andalso + ?amqqueue_v2_field_type(Q) =:= classic) orelse + ?is_amqqueue_v1(Q))). + +-define(amqqueue_is_quorum(Q), + (?is_amqqueue_v2(Q) andalso + ?amqqueue_v2_field_type(Q) =:= quorum) orelse + false). + +-define(amqqueue_has_valid_pid(Q), + ((?is_amqqueue_v2(Q) andalso + is_pid(?amqqueue_v2_field_pid(Q))) orelse + (?is_amqqueue_v1(Q) andalso + is_pid(?amqqueue_v1_field_pid(Q))))). + +-define(amqqueue_pid_runs_on_local_node(Q), + ((?is_amqqueue_v2(Q) andalso + node(?amqqueue_v2_field_pid(Q)) =:= node()) orelse + (?is_amqqueue_v1(Q) andalso + node(?amqqueue_v1_field_pid(Q)) =:= node()))). + +-define(amqqueue_pid_equals(Q, Pid), + ((?is_amqqueue_v2(Q) andalso + ?amqqueue_v2_field_pid(Q) =:= Pid) orelse + (?is_amqqueue_v1(Q) andalso + ?amqqueue_v1_field_pid(Q) =:= Pid))). + +-define(amqqueue_pids_are_equal(Q0, Q1), + ((?is_amqqueue_v2(Q0) andalso ?is_amqqueue_v2(Q1) andalso + ?amqqueue_v2_field_pid(Q0) =:= ?amqqueue_v2_field_pid(Q1)) orelse + (?is_amqqueue_v1(Q0) andalso ?is_amqqueue_v1(Q1) andalso + ?amqqueue_v1_field_pid(Q0) =:= ?amqqueue_v1_field_pid(Q1)))). + +-define(amqqueue_field_name(Q), + case ?is_amqqueue_v2(Q) of + true -> ?amqqueue_v2_field_name(Q); + false -> case ?is_amqqueue_v1(Q) of + true -> ?amqqueue_v1_field_name(Q) + end + end). + +-define(amqqueue_field_pid(Q), + case ?is_amqqueue_v2(Q) of + true -> ?amqqueue_v2_field_pid(Q); + false -> case ?is_amqqueue_v1(Q) of + true -> ?amqqueue_v1_field_pid(Q) + end + end). + +-define(amqqueue_v1_vhost(Q), element(2, ?amqqueue_v1_field_name(Q))). +-define(amqqueue_v2_vhost(Q), element(2, ?amqqueue_v2_field_name(Q))). + +-define(amqqueue_vhost_equals(Q, VHost), + ((?is_amqqueue_v2(Q) andalso + ?amqqueue_v2_vhost(Q) =:= VHost) orelse + (?is_amqqueue_v1(Q) andalso + ?amqqueue_v1_vhost(Q) =:= VHost))). + +-ifdef(DEBUG_QUORUM_QUEUE_FF). +-define(enable_quorum_queue_if_debug, + begin + rabbit_log:info( + "---- ENABLING quorum_queue as part of " + "?try_mnesia_tx_or_upgrade_amqqueue_and_retry() ----"), + ok = rabbit_feature_flags:enable(quorum_queue) + end). +-else. +-define(enable_quorum_queue_if_debug, noop). +-endif. + +-define(try_mnesia_tx_or_upgrade_amqqueue_and_retry(Expr1, Expr2), + try + ?enable_quorum_queue_if_debug, + Expr1 + catch + throw:{error, {bad_type, T}} when ?is_amqqueue(T) -> + Expr2; + throw:{aborted, {bad_type, T}} when ?is_amqqueue(T) -> + Expr2 + end). diff --git a/include/amqqueue_v1.hrl b/include/amqqueue_v1.hrl new file mode 100644 index 0000000000..04b2d72850 --- /dev/null +++ b/include/amqqueue_v1.hrl @@ -0,0 +1,20 @@ +-define(is_amqqueue_v1(Q), is_record(Q, amqqueue, 19)). + +-define(amqqueue_v1_field_name(Q), element(2, Q)). +-define(amqqueue_v1_field_durable(Q), element(3, Q)). +-define(amqqueue_v1_field_auto_delete(Q), element(4, Q)). +-define(amqqueue_v1_field_exclusive_owner(Q), element(5, Q)). +-define(amqqueue_v1_field_arguments(Q), element(6, Q)). +-define(amqqueue_v1_field_pid(Q), element(7, Q)). +-define(amqqueue_v1_field_slave_pids(Q), element(8, Q)). +-define(amqqueue_v1_field_sync_slave_pids(Q), element(9, Q)). +-define(amqqueue_v1_field_recoverable_slaves(Q), element(10, Q)). +-define(amqqueue_v1_field_policy(Q), element(11, Q)). +-define(amqqueue_v1_field_operator_policy(Q), element(12, Q)). +-define(amqqueue_v1_field_gm_pids(Q), element(13, Q)). +-define(amqqueue_v1_field_decorators(Q), element(14, Q)). +-define(amqqueue_v1_field_state(Q), element(15, Q)). +-define(amqqueue_v1_field_policy_version(Q), element(16, Q)). +-define(amqqueue_v1_field_slave_pids_pending_shutdown(Q), element(17, Q)). +-define(amqqueue_v1_field_vhost(Q), element(18, Q)). +-define(amqqueue_v1_field_options(Q), element(19, Q)). diff --git a/include/amqqueue_v2.hrl b/include/amqqueue_v2.hrl new file mode 100644 index 0000000000..37cd7ba2a8 --- /dev/null +++ b/include/amqqueue_v2.hrl @@ -0,0 +1,22 @@ +-define(is_amqqueue_v2(Q), is_record(Q, amqqueue, 21)). + +-define(amqqueue_v2_field_name(Q), element(2, Q)). +-define(amqqueue_v2_field_durable(Q), element(3, Q)). +-define(amqqueue_v2_field_auto_delete(Q), element(4, Q)). +-define(amqqueue_v2_field_exclusive_owner(Q), element(5, Q)). +-define(amqqueue_v2_field_arguments(Q), element(6, Q)). +-define(amqqueue_v2_field_pid(Q), element(7, Q)). +-define(amqqueue_v2_field_slave_pids(Q), element(8, Q)). +-define(amqqueue_v2_field_sync_slave_pids(Q), element(9, Q)). +-define(amqqueue_v2_field_recoverable_slaves(Q), element(10, Q)). +-define(amqqueue_v2_field_policy(Q), element(11, Q)). +-define(amqqueue_v2_field_operator_policy(Q), element(12, Q)). +-define(amqqueue_v2_field_gm_pids(Q), element(13, Q)). +-define(amqqueue_v2_field_decorators(Q), element(14, Q)). +-define(amqqueue_v2_field_state(Q), element(15, Q)). +-define(amqqueue_v2_field_policy_version(Q), element(16, Q)). +-define(amqqueue_v2_field_slave_pids_pending_shutdown(Q), element(17, Q)). +-define(amqqueue_v2_field_vhost(Q), element(18, Q)). +-define(amqqueue_v2_field_options(Q), element(19, Q)). +-define(amqqueue_v2_field_type(Q), element(20, Q)). +-define(amqqueue_v2_field_quorum_nodes(Q), element(21, Q)). diff --git a/scripts/rabbitmq-env b/scripts/rabbitmq-env index f4b06c171b..2b06dec2cd 100755 --- a/scripts/rabbitmq-env +++ b/scripts/rabbitmq-env @@ -278,6 +278,10 @@ rmq_normalize_path_var RABBITMQ_PID_FILE [ "x" = "x$RABBITMQ_BOOT_MODULE" ] && RABBITMQ_BOOT_MODULE=${BOOT_MODULE} +[ "x" != "x$RABBITMQ_FEATURE_FLAGS_FILE" ] && RABBITMQ_FEATURE_FLAGS_FILE_source=environment +[ "x" = "x$RABBITMQ_FEATURE_FLAGS_FILE" ] && RABBITMQ_FEATURE_FLAGS_FILE=${RABBITMQ_MNESIA_BASE}/${RABBITMQ_NODENAME}-feature_flags +rmq_normalize_path_var RABBITMQ_FEATURE_FLAGS_FILE + [ "x" = "x$RABBITMQ_PLUGINS_EXPAND_DIR" ] && RABBITMQ_PLUGINS_EXPAND_DIR=${PLUGINS_EXPAND_DIR} [ "x" = "x$RABBITMQ_PLUGINS_EXPAND_DIR" ] && RABBITMQ_PLUGINS_EXPAND_DIR=${RABBITMQ_MNESIA_BASE}/${RABBITMQ_NODENAME}-plugins-expand rmq_normalize_path_var RABBITMQ_PLUGINS_EXPAND_DIR @@ -310,6 +314,7 @@ rmq_check_if_shared_with_mnesia \ RABBITMQ_CONFIG_FILE \ RABBITMQ_LOG_BASE \ RABBITMQ_PID_FILE \ + RABBITMQ_FEATURE_FLAGS_FILE \ RABBITMQ_PLUGINS_EXPAND_DIR \ RABBITMQ_ENABLED_PLUGINS_FILE \ RABBITMQ_PLUGINS_DIR \ @@ -320,21 +325,29 @@ rmq_check_if_shared_with_mnesia \ ## Development-specific environment. if [ "${RABBITMQ_DEV_ENV}" ]; then - if [ "$RABBITMQ_PLUGINS_DIR_source" != 'environment' -o \ + if [ "$RABBITMQ_FEATURE_FLAGS_FILE_source" != 'environment' -o \ + "$RABBITMQ_PLUGINS_DIR_source" != 'environment' -o \ "$RABBITMQ_ENABLED_PLUGINS_FILE_source" != 'environment' ]; then # We need to query the running node for the plugins directory # and the "enabled plugins" file. eval $( (${RABBITMQ_SCRIPTS_DIR}/rabbitmqctl eval \ - '{ok, P} = application:get_env(rabbit, plugins_dir), + '{ok, F} = application:get_env(rabbit, feature_flags_file), + {ok, P} = application:get_env(rabbit, plugins_dir), {ok, E} = application:get_env(rabbit, enabled_plugins_file), B = os:getenv("RABBITMQ_MNESIA_BASE"), M = os:getenv("RABBITMQ_MNESIA_DIR"), io:format( + "feature_flags_file=\"~s\"~n" "plugins_dir=\"~s\"~n" "enabled_plugins_file=\"~s\"~n" "mnesia_base=\"~s\"~n" - "mnesia_dir=\"~s\"~n", [P, E, B, M]).' \ - 2>/dev/null | grep -E '^(plugins_dir|enabled_plugins_file|mnesia_base|mnesia_dir)=') || :) + "mnesia_dir=\"~s\"~n", [F, P, E, B, M]).' \ + 2>/dev/null | grep -E '^(feature_flags_file|plugins_dir|enabled_plugins_file|mnesia_base|mnesia_dir)=') || :) + + if [ "${feature_flags_file}" -a \ + "$RABBITMQ_FEATURE_FLAGS_FILE_source" != 'environment' ]; then + RABBITMQ_FEATURE_FLAGS_FILE="${feature_flags_file}" + fi if [ "${plugins_dir}" -a \ "$RABBITMQ_PLUGINS_DIR_source" != 'environment' ]; then RABBITMQ_PLUGINS_DIR="${plugins_dir}" diff --git a/scripts/rabbitmq-env.bat b/scripts/rabbitmq-env.bat index 912668c8ac..783fc55287 100644 --- a/scripts/rabbitmq-env.bat +++ b/scripts/rabbitmq-env.bat @@ -284,6 +284,15 @@ if "!RABBITMQ_BOOT_MODULE!"=="" ( )
)
+REM [ "x" = "x$RABBITMQ_FEATURE_FLAGS_FILE" ] && RABBITMQ_FEATURE_FLAGS_FILE=${RABBITMQ_MNESIA_BASE}/${RABBITMQ_NODENAME}-feature_flags
+if "!RABBITMQ_FEATURE_FLAGS_FILE!"=="" (
+ if "!FEATURE_FLAGS_FILE!"=="" (
+ set RABBITMQ_FEATURE_FLAGS_FILE=!RABBITMQ_MNESIA_BASE!\!RABBITMQ_NODENAME!-feature_flags
+ ) else (
+ set RABBITMQ_FEATURE_FLAGS_FILE=!FEATURE_FLAGS_FILE!
+ )
+)
+
REM [ "x" = "x$RABBITMQ_PLUGINS_EXPAND_DIR" ] && RABBITMQ_PLUGINS_EXPAND_DIR=${PLUGINS_EXPAND_DIR}
REM [ "x" = "x$RABBITMQ_PLUGINS_EXPAND_DIR" ] && RABBITMQ_PLUGINS_EXPAND_DIR=${RABBITMQ_MNESIA_BASE}/${RABBITMQ_NODENAME}-plugins-expand
if "!RABBITMQ_PLUGINS_EXPAND_DIR!"=="" (
@@ -397,6 +406,13 @@ if defined RABBITMQ_DEV_ENV ( if "!SCRIPT_NAME!" == "rabbitmq-plugins" (
REM We may need to query the running node for the plugins directory
REM and the "enabled plugins" file.
+ if not "%RABBITMQ_FEATURE_FLAGS_FILE_source%" == "environment" (
+ for /f "delims=" %%F in ('!SCRIPT_DIR!\rabbitmqctl eval "{ok, P} = application:get_env(rabbit, feature_flags_file), io:format(""~s~n"", [P])."') do @set feature_flags_file=%%F
+ if exist "!feature_flags_file!" (
+ set RABBITMQ_FEATURE_FLAGS_FILE=!feature_flags_file!
+ )
+ REM set feature_flags_file=
+ )
if not "%RABBITMQ_PLUGINS_DIR_source%" == "environment" (
for /f "delims=" %%F in ('!SCRIPT_DIR!\rabbitmqctl eval "{ok, P} = application:get_env(rabbit, plugins_dir), io:format(""~s~n"", [P])."') do @set plugins_dir=%%F
if exist "!plugins_dir!" (
@@ -476,6 +492,7 @@ exit /b REM Environment cleanup
set BOOT_MODULE=
set CONFIG_FILE=
+set FEATURE_FLAGS_FILE=
set ENABLED_PLUGINS_FILE=
set LOG_BASE=
set MNESIA_BASE=
diff --git a/scripts/rabbitmq-server b/scripts/rabbitmq-server index efff38c3af..aeac38e46f 100755 --- a/scripts/rabbitmq-server +++ b/scripts/rabbitmq-server @@ -195,6 +195,7 @@ RABBITMQ_DIST_PORT=$RABBITMQ_DIST_PORT \ -s rabbit_prelaunch \ ${RABBITMQ_NAME_TYPE} ${RABBITMQ_PRELAUNCH_NODENAME} \ -conf_advanced "${RABBITMQ_ADVANCED_CONFIG_FILE}" \ + -rabbit feature_flags_file "\"$RABBITMQ_FEATURE_FLAGS_FILE\"" \ -rabbit enabled_plugins_file "\"$RABBITMQ_ENABLED_PLUGINS_FILE\"" \ -rabbit plugins_dir "\"$RABBITMQ_PLUGINS_DIR\"" \ -extra "${RABBITMQ_NODENAME}" @@ -302,6 +303,7 @@ start_rabbitmq_server() { -rabbit lager_log_root "\"$RABBITMQ_LOG_BASE\"" \ -rabbit lager_default_file "$RABBIT_LAGER_HANDLER" \ -rabbit lager_upgrade_file "$RABBITMQ_LAGER_HANDLER_UPGRADE" \ + -rabbit feature_flags_file "\"$RABBITMQ_FEATURE_FLAGS_FILE\"" \ -rabbit enabled_plugins_file "\"$RABBITMQ_ENABLED_PLUGINS_FILE\"" \ -rabbit plugins_dir "\"$RABBITMQ_PLUGINS_DIR\"" \ -rabbit plugins_expand_dir "\"$RABBITMQ_PLUGINS_EXPAND_DIR\"" \ diff --git a/scripts/rabbitmq-server.bat b/scripts/rabbitmq-server.bat index 9039243c62..d417091732 100644 --- a/scripts/rabbitmq-server.bat +++ b/scripts/rabbitmq-server.bat @@ -146,6 +146,7 @@ if "!RABBITMQ_CONFIG_FILE_NOEX!.conf" == "!RABBITMQ_CONFIG_FILE!" ( -s rabbit_prelaunch ^
!RABBITMQ_NAME_TYPE! rabbitmqprelaunch!RANDOM!!TIME:~9!@localhost ^
-conf_advanced "!RABBITMQ_ADVANCED_CONFIG_FILE!" ^
+ -rabbit feature_flags_file "!RABBITMQ_FEATURE_FLAGS_FILE!" ^
-rabbit enabled_plugins_file "!RABBITMQ_ENABLED_PLUGINS_FILE!" ^
-rabbit plugins_dir "!RABBITMQ_PLUGINS_DIR!" ^
-extra "!RABBITMQ_NODENAME!"
@@ -247,6 +248,7 @@ if "!ENV_OK!"=="false" ( -rabbit lager_log_root \""!RABBITMQ_LOG_BASE:\=/!"\" ^
-rabbit lager_default_file !RABBIT_LAGER_HANDLER! ^
-rabbit lager_upgrade_file !RABBITMQ_LAGER_HANDLER_UPGRADE! ^
+-rabbit feature_flags_file \""!RABBITMQ_FEATURE_FLAGS_FILE:\=/!"\" ^
-rabbit enabled_plugins_file \""!RABBITMQ_ENABLED_PLUGINS_FILE:\=/!"\" ^
-rabbit plugins_dir \""!RABBITMQ_PLUGINS_DIR:\=/!"\" ^
-rabbit plugins_expand_dir \""!RABBITMQ_PLUGINS_EXPAND_DIR:\=/!"\" ^
diff --git a/scripts/rabbitmq-service.bat b/scripts/rabbitmq-service.bat index 08cc29f2c8..7bb1f124b5 100644 --- a/scripts/rabbitmq-service.bat +++ b/scripts/rabbitmq-service.bat @@ -229,6 +229,7 @@ if "!RABBITMQ_CONFIG_FILE_NOEX!.conf" == "!RABBITMQ_CONFIG_FILE!" ( -s rabbit_prelaunch ^
!RABBITMQ_NAME_TYPE! rabbitmqprelaunch!RANDOM!!TIME:~9!@localhost ^
-conf_advanced "!RABBITMQ_ADVANCED_CONFIG_FILE!" ^
+ -rabbit feature_flags_file "!RABBITMQ_FEATURE_FLAGS_FILE!" ^
-rabbit enabled_plugins_file "!RABBITMQ_ENABLED_PLUGINS_FILE!" ^
-rabbit plugins_dir "!RABBITMQ_PLUGINS_DIR!" ^
-extra "!RABBITMQ_NODENAME!"
@@ -320,6 +321,7 @@ set ERLANG_SERVICE_ARGUMENTS= ^ -rabbit lager_log_root \""!RABBITMQ_LOG_BASE:\=/!"\" ^
-rabbit lager_default_file !RABBIT_LAGER_HANDLER! ^
-rabbit lager_upgrade_file !RABBITMQ_LAGER_HANDLER_UPGRADE! ^
+-rabbit feature_flags_file \""!RABBITMQ_FEATURE_FLAGS_FILE:\=/!"\" ^
-rabbit enabled_plugins_file \""!RABBITMQ_ENABLED_PLUGINS_FILE:\=/!"\" ^
-rabbit plugins_dir \""!RABBITMQ_PLUGINS_DIR:\=/!"\" ^
-rabbit plugins_expand_dir \""!RABBITMQ_PLUGINS_EXPAND_DIR:\=/!"\" ^
diff --git a/src/amqqueue.erl b/src/amqqueue.erl new file mode 100644 index 0000000000..83b65cd048 --- /dev/null +++ b/src/amqqueue.erl @@ -0,0 +1,682 @@ +%% The contents of this file are subject to the Mozilla Public License +%% Version 1.1 (the "License"); you may not use this file except in +%% compliance with the License. You may obtain a copy of the License +%% at http://www.mozilla.org/MPL/ +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and +%% limitations under the License. +%% +%% The Original Code is RabbitMQ. +%% +%% The Initial Developer of the Original Code is GoPivotal, Inc. +%% Copyright (c) 2018-2019 Pivotal Software, Inc. All rights reserved. +%% + +-module(amqqueue). %% Could become amqqueue_v2 in the future. + +-include_lib("rabbit_common/include/rabbit.hrl"). +-include("amqqueue.hrl"). + +-export([new/9, + new_with_version/10, + fields/0, + fields/1, + field_vhost/0, + record_version_to_use/0, + upgrade/1, + upgrade_to/2, + % arguments + get_arguments/1, + set_arguments/2, + % decorators + get_decorators/1, + set_decorators/2, + % exclusive_owner + get_exclusive_owner/1, + % gm_pids + get_gm_pids/1, + set_gm_pids/2, + get_leader/1, + % name (#resource) + get_name/1, + set_name/2, + % operator_policy + get_operator_policy/1, + set_operator_policy/2, + get_options/1, + % pid + get_pid/1, + set_pid/2, + % policy + get_policy/1, + set_policy/2, + % policy_version + get_policy_version/1, + set_policy_version/2, + % quorum_nodes + get_quorum_nodes/1, + set_quorum_nodes/2, + % recoverable_slaves + get_recoverable_slaves/1, + set_recoverable_slaves/2, + % slave_pids + get_slave_pids/1, + set_slave_pids/2, + % slave_pids_pending_shutdown + get_slave_pids_pending_shutdown/1, + set_slave_pids_pending_shutdown/2, + % state + get_state/1, + set_state/2, + % sync_slave_pids + get_sync_slave_pids/1, + set_sync_slave_pids/2, + get_type/1, + get_vhost/1, + is_amqqueue/1, + is_auto_delete/1, + is_durable/1, + is_classic/1, + is_quorum/1, + pattern_match_all/0, + pattern_match_on_name/1, + pattern_match_on_type/1, + reset_mirroring_and_decorators/1, + set_immutable/1, + qnode/1, + macros/0]). + +-define(record_version, amqqueue_v2). + +-record(amqqueue, { + name :: rabbit_amqqueue:name() | '_', %% immutable + durable :: boolean() | '_', %% immutable + auto_delete :: boolean() | '_', %% immutable + exclusive_owner = none :: pid() | none | '_', %% immutable + arguments = [] :: rabbit_framing:amqp_table() | '_', %% immutable + pid :: pid() | ra_server_id() | none | '_', %% durable (just so we + %% know home node) + slave_pids = [] :: [pid()] | none | '_', %% transient + sync_slave_pids = [] :: [pid()] | none| '_',%% transient + recoverable_slaves = [] :: [atom()] | none | '_', %% durable + policy :: binary() | none | undefined | '_', %% durable, implicit + %% update as above + operator_policy :: binary() | none | undefined | '_', %% durable, + %% implicit + %% update + %% as above + gm_pids = [] :: [pid()] | none | '_', %% transient + decorators :: [atom()] | none | undefined | '_', %% transient, + %% recalculated + %% as above + state = live :: atom() | none | '_', %% durable (have we crashed?) + policy_version = 0 :: non_neg_integer() | '_', + slave_pids_pending_shutdown = [] :: [pid()] | '_', + vhost :: rabbit_types:vhost() | undefined | '_', %% secondary index + options = #{} :: map() | '_', + type = ?amqqueue_v1_type :: atom() | '_', + quorum_nodes = [] :: [node()] | '_' + }). + +-type amqqueue() :: amqqueue_v1:amqqueue_v1() | amqqueue_v2(). +-type amqqueue_v2() :: #amqqueue{ + name :: rabbit_amqqueue:name(), + durable :: boolean(), + auto_delete :: boolean(), + exclusive_owner :: pid() | none, + arguments :: rabbit_framing:amqp_table(), + pid :: pid() | ra_server_id() | none, + slave_pids :: [pid()] | none, + sync_slave_pids :: [pid()] | none, + recoverable_slaves :: [atom()] | none, + policy :: binary() | none | undefined, + operator_policy :: binary() | none | undefined, + gm_pids :: [pid()] | none, + decorators :: [atom()] | none | undefined, + state :: atom() | none, + policy_version :: non_neg_integer(), + slave_pids_pending_shutdown :: [pid()], + vhost :: rabbit_types:vhost() | undefined, + options :: map(), + type :: atom(), + quorum_nodes :: [node()] + }. + +-type ra_server_id() :: {Name :: atom(), Node :: node()}. + +-type amqqueue_pattern() :: amqqueue_v1:amqqueue_v1_pattern() | + amqqueue_v2_pattern(). +-type amqqueue_v2_pattern() :: #amqqueue{ + name :: rabbit_amqqueue:name() | '_', + durable :: '_', + auto_delete :: '_', + exclusive_owner :: '_', + arguments :: '_', + pid :: '_', + slave_pids :: '_', + sync_slave_pids :: '_', + recoverable_slaves :: '_', + policy :: '_', + operator_policy :: '_', + gm_pids :: '_', + decorators :: '_', + state :: '_', + policy_version :: '_', + slave_pids_pending_shutdown :: '_', + vhost :: '_', + options :: '_', + type :: atom() | '_', + quorum_nodes :: '_' + }. + +-export_type([amqqueue/0, + amqqueue_v2/0, + amqqueue_pattern/0, + amqqueue_v2_pattern/0, + ra_server_id/0]). + +-spec new(rabbit_amqqueue:name(), + pid() | ra_server_id() | none, + boolean(), + boolean(), + pid() | none, + rabbit_framing:amqp_table(), + rabbit_types:vhost() | undefined, + map(), + atom()) -> amqqueue(). + +new(#resource{kind = queue} = Name, + Pid, + Durable, + AutoDelete, + Owner, + Args, + VHost, + Options, + Type) + when (is_pid(Pid) orelse is_tuple(Pid) orelse Pid =:= none) andalso + is_boolean(Durable) andalso + is_boolean(AutoDelete) andalso + (is_pid(Owner) orelse Owner =:= none) andalso + is_list(Args) andalso + (is_binary(VHost) orelse VHost =:= undefined) andalso + is_map(Options) andalso + is_atom(Type) -> + case record_version_to_use() of + ?record_version -> + new_with_version( + ?record_version, + Name, + Pid, + Durable, + AutoDelete, + Owner, + Args, + VHost, + Options, + Type); + _ -> + amqqueue_v1:new( + Name, + Pid, + Durable, + AutoDelete, + Owner, + Args, + VHost, + Options) + end. + +-spec new_with_version +(amqqueue_v1 | amqqueue_v2, + rabbit_amqqueue:name(), + pid() | ra_server_id() | none, + boolean(), + boolean(), + pid() | none, + rabbit_framing:amqp_table(), + rabbit_types:vhost() | undefined, + map(), + atom()) -> amqqueue(). + +new_with_version(?record_version, + #resource{kind = queue} = Name, + Pid, + Durable, + AutoDelete, + Owner, + Args, + VHost, + Options, + Type) + when (is_pid(Pid) orelse is_tuple(Pid) orelse Pid =:= none) andalso + is_boolean(Durable) andalso + is_boolean(AutoDelete) andalso + (is_pid(Owner) orelse Owner =:= none) andalso + is_list(Args) andalso + (is_binary(VHost) orelse VHost =:= undefined) andalso + is_map(Options) andalso + is_atom(Type) -> + #amqqueue{name = Name, + durable = Durable, + auto_delete = AutoDelete, + arguments = Args, + exclusive_owner = Owner, + pid = Pid, + vhost = VHost, + options = Options, + type = Type}; +new_with_version(Version, + Name, + Pid, + Durable, + AutoDelete, + Owner, + Args, + VHost, + Options, + ?amqqueue_v1_type) -> + amqqueue_v1:new_with_version( + Version, + Name, + Pid, + Durable, + AutoDelete, + Owner, + Args, + VHost, + Options). + +-spec is_amqqueue(any()) -> boolean(). + +is_amqqueue(#amqqueue{}) -> true; +is_amqqueue(Queue) -> amqqueue_v1:is_amqqueue(Queue). + +-spec record_version_to_use() -> amqqueue_v1 | amqqueue_v2. + +record_version_to_use() -> + case rabbit_feature_flags:is_enabled(quorum_queue) of + true -> ?record_version; + false -> amqqueue_v1:record_version_to_use() + end. + +-spec upgrade(amqqueue()) -> amqqueue(). + +upgrade(#amqqueue{} = Queue) -> Queue; +upgrade(OldQueue) -> upgrade_to(record_version_to_use(), OldQueue). + +-spec upgrade_to +(amqqueue_v2, amqqueue()) -> amqqueue_v2(); +(amqqueue_v1, amqqueue_v1:amqqueue_v1()) -> amqqueue_v1:amqqueue_v1(). + +upgrade_to(?record_version, #amqqueue{} = Queue) -> + Queue; +upgrade_to(?record_version, OldQueue) -> + Fields = erlang:tuple_to_list(OldQueue) ++ [?amqqueue_v1_type, + undefined], + #amqqueue{} = erlang:list_to_tuple(Fields); +upgrade_to(Version, OldQueue) -> + amqqueue_v1:upgrade_to(Version, OldQueue). + +% arguments + +-spec get_arguments(amqqueue()) -> rabbit_framing:amqp_table(). + +get_arguments(#amqqueue{arguments = Args}) -> + Args; +get_arguments(Queue) -> + amqqueue_v1:get_arguments(Queue). + +-spec set_arguments(amqqueue(), rabbit_framing:amqp_table()) -> amqqueue(). + +set_arguments(#amqqueue{} = Queue, Args) -> + Queue#amqqueue{arguments = Args}; +set_arguments(Queue, Args) -> + amqqueue_v1:set_arguments(Queue, Args). + +% decorators + +-spec get_decorators(amqqueue()) -> [atom()] | none | undefined. + +get_decorators(#amqqueue{decorators = Decorators}) -> + Decorators; +get_decorators(Queue) -> + amqqueue_v1:get_decorators(Queue). + +-spec set_decorators(amqqueue(), [atom()] | none | undefined) -> amqqueue(). + +set_decorators(#amqqueue{} = Queue, Decorators) -> + Queue#amqqueue{decorators = Decorators}; +set_decorators(Queue, Decorators) -> + amqqueue_v1:set_decorators(Queue, Decorators). + +-spec get_exclusive_owner(amqqueue()) -> pid() | none. + +get_exclusive_owner(#amqqueue{exclusive_owner = Owner}) -> + Owner; +get_exclusive_owner(Queue) -> + amqqueue_v1:get_exclusive_owner(Queue). + +-spec get_gm_pids(amqqueue()) -> [pid()] | none. + +get_gm_pids(#amqqueue{gm_pids = GMPids}) -> + GMPids; +get_gm_pids(Queue) -> + amqqueue_v1:get_gm_pids(Queue). + +-spec set_gm_pids(amqqueue(), [pid()] | none) -> amqqueue(). + +set_gm_pids(#amqqueue{} = Queue, GMPids) -> + Queue#amqqueue{gm_pids = GMPids}; +set_gm_pids(Queue, GMPids) -> + amqqueue_v1:set_gm_pids(Queue, GMPids). + +-spec get_leader(amqqueue_v2()) -> node(). + +get_leader(#amqqueue{type = quorum, pid = {_, Leader}}) -> Leader. + +% operator_policy + +-spec get_operator_policy(amqqueue()) -> binary() | none | undefined. + +get_operator_policy(#amqqueue{operator_policy = OpPolicy}) -> OpPolicy; +get_operator_policy(Queue) -> amqqueue_v1:get_operator_policy(Queue). + +-spec set_operator_policy(amqqueue(), binary() | none | undefined) -> + amqqueue(). + +set_operator_policy(#amqqueue{} = Queue, Policy) -> + Queue#amqqueue{operator_policy = Policy}; +set_operator_policy(Queue, Policy) -> + amqqueue_v1:set_operator_policy(Queue, Policy). + +% name + +-spec get_name(amqqueue()) -> rabbit_amqqueue:name(). + +get_name(#amqqueue{name = Name}) -> Name; +get_name(Queue) -> amqqueue_v1:get_name(Queue). + +-spec set_name(amqqueue(), rabbit_amqqueue:name()) -> amqqueue(). + +set_name(#amqqueue{} = Queue, Name) -> + Queue#amqqueue{name = Name}; +set_name(Queue, Name) -> + amqqueue_v1:set_name(Queue, Name). + +-spec get_options(amqqueue()) -> map(). + +get_options(#amqqueue{options = Options}) -> Options; +get_options(Queue) -> amqqueue_v1:get_options(Queue). + +% pid + +-spec get_pid +(amqqueue_v2()) -> pid() | ra_server_id() | none; +(amqqueue_v1:amqqueue_v1()) -> pid() | none. + +get_pid(#amqqueue{pid = Pid}) -> Pid; +get_pid(Queue) -> amqqueue_v1:get_pid(Queue). + +-spec set_pid +(amqqueue_v2(), pid() | ra_server_id() | none) -> amqqueue_v2(); +(amqqueue_v1:amqqueue_v1(), pid() | none) -> amqqueue_v1:amqqueue_v1(). + +set_pid(#amqqueue{} = Queue, Pid) -> + Queue#amqqueue{pid = Pid}; +set_pid(Queue, Pid) -> + amqqueue_v1:set_pid(Queue, Pid). + +% policy + +-spec get_policy(amqqueue()) -> binary() | none | undefined. + +get_policy(#amqqueue{policy = Policy}) -> Policy; +get_policy(Queue) -> amqqueue_v1:get_policy(Queue). + +-spec set_policy(amqqueue(), binary() | none | undefined) -> amqqueue(). + +set_policy(#amqqueue{} = Queue, Policy) -> + Queue#amqqueue{policy = Policy}; +set_policy(Queue, Policy) -> + amqqueue_v1:set_policy(Queue, Policy). + +% policy_version + +-spec get_policy_version(amqqueue()) -> non_neg_integer(). + +get_policy_version(#amqqueue{policy_version = PV}) -> + PV; +get_policy_version(Queue) -> + amqqueue_v1:get_policy_version(Queue). + +-spec set_policy_version(amqqueue(), non_neg_integer()) -> amqqueue(). + +set_policy_version(#amqqueue{} = Queue, PV) -> + Queue#amqqueue{policy_version = PV}; +set_policy_version(Queue, PV) -> + amqqueue_v1:set_policy_version(Queue, PV). + +% recoverable_slaves + +-spec get_recoverable_slaves(amqqueue()) -> [atom()] | none. + +get_recoverable_slaves(#amqqueue{recoverable_slaves = Slaves}) -> + Slaves; +get_recoverable_slaves(Queue) -> + amqqueue_v1:get_recoverable_slaves(Queue). + +-spec set_recoverable_slaves(amqqueue(), [atom()] | none) -> amqqueue(). + +set_recoverable_slaves(#amqqueue{} = Queue, Slaves) -> + Queue#amqqueue{recoverable_slaves = Slaves}; +set_recoverable_slaves(Queue, Slaves) -> + amqqueue_v1:set_recoverable_slaves(Queue, Slaves). + +% quorum_nodes (new in v2) + +-spec get_quorum_nodes(amqqueue()) -> [node()]. + +get_quorum_nodes(#amqqueue{quorum_nodes = Nodes}) -> Nodes; +get_quorum_nodes(_) -> []. + +-spec set_quorum_nodes(amqqueue(), [node()]) -> amqqueue(). + +set_quorum_nodes(#amqqueue{} = Queue, Nodes) -> + Queue#amqqueue{quorum_nodes = Nodes}; +set_quorum_nodes(Queue, _Nodes) -> + Queue. + +% slave_pids + +-spec get_slave_pids(amqqueue()) -> [pid()] | none. + +get_slave_pids(#amqqueue{slave_pids = Slaves}) -> + Slaves; +get_slave_pids(Queue) -> + amqqueue_v1:get_slave_pids(Queue). + +-spec set_slave_pids(amqqueue(), [pid()] | none) -> amqqueue(). + +set_slave_pids(#amqqueue{} = Queue, SlavePids) -> + Queue#amqqueue{slave_pids = SlavePids}; +set_slave_pids(Queue, SlavePids) -> + amqqueue_v1:set_slave_pids(Queue, SlavePids). + +% slave_pids_pending_shutdown + +-spec get_slave_pids_pending_shutdown(amqqueue()) -> [pid()]. + +get_slave_pids_pending_shutdown(#amqqueue{slave_pids_pending_shutdown = Slaves}) -> + Slaves; +get_slave_pids_pending_shutdown(Queue) -> + amqqueue_v1:get_slave_pids_pending_shutdown(Queue). + +-spec set_slave_pids_pending_shutdown(amqqueue(), [pid()]) -> amqqueue(). + +set_slave_pids_pending_shutdown(#amqqueue{} = Queue, SlavePids) -> + Queue#amqqueue{slave_pids_pending_shutdown = SlavePids}; +set_slave_pids_pending_shutdown(Queue, SlavePids) -> + amqqueue_v1:set_slave_pids_pending_shutdown(Queue, SlavePids). + +% state + +-spec get_state(amqqueue()) -> atom() | none. + +get_state(#amqqueue{state = State}) -> State; +get_state(Queue) -> amqqueue_v1:get_state(Queue). + +-spec set_state(amqqueue(), atom() | none) -> amqqueue(). + +set_state(#amqqueue{} = Queue, State) -> + Queue#amqqueue{state = State}; +set_state(Queue, State) -> + amqqueue_v1:set_state(Queue, State). + +% sync_slave_pids + +-spec get_sync_slave_pids(amqqueue()) -> [pid()] | none. + +get_sync_slave_pids(#amqqueue{sync_slave_pids = Pids}) -> + Pids; +get_sync_slave_pids(Queue) -> + amqqueue_v1:get_sync_slave_pids(Queue). + +-spec set_sync_slave_pids(amqqueue(), [pid()] | none) -> amqqueue(). + +set_sync_slave_pids(#amqqueue{} = Queue, Pids) -> + Queue#amqqueue{sync_slave_pids = Pids}; +set_sync_slave_pids(Queue, Pids) -> + amqqueue_v1:set_sync_slave_pids(Queue, Pids). + +%% New in v2. + +-spec get_type(amqqueue()) -> atom(). + +get_type(#amqqueue{type = Type}) -> Type; +get_type(Queue) when ?is_amqqueue(Queue) -> ?amqqueue_v1_type. + +-spec get_vhost(amqqueue()) -> rabbit_types:vhost() | undefined. + +get_vhost(#amqqueue{vhost = VHost}) -> VHost; +get_vhost(Queue) -> amqqueue_v1:get_vhost(Queue). + +-spec is_auto_delete(amqqueue()) -> boolean(). + +is_auto_delete(#amqqueue{auto_delete = AutoDelete}) -> + AutoDelete; +is_auto_delete(Queue) -> + amqqueue_v1:is_auto_delete(Queue). + +-spec is_durable(amqqueue()) -> boolean(). + +is_durable(#amqqueue{durable = Durable}) -> Durable; +is_durable(Queue) -> amqqueue_v1:is_durable(Queue). + +-spec is_classic(amqqueue()) -> boolean(). + +is_classic(Queue) -> + get_type(Queue) =:= ?amqqueue_v1_type. + +-spec is_quorum(amqqueue()) -> boolean(). + +is_quorum(Queue) -> + get_type(Queue) =:= quorum. + +fields() -> + case record_version_to_use() of + ?record_version -> fields(?record_version); + _ -> amqqueue_v1:fields() + end. + +fields(?record_version) -> record_info(fields, amqqueue); +fields(Version) -> amqqueue_v1:fields(Version). + +field_vhost() -> + case record_version_to_use() of + ?record_version -> #amqqueue.vhost; + _ -> amqqueue_v1:field_vhost() + end. + +-spec pattern_match_all() -> amqqueue_pattern(). + +pattern_match_all() -> + case record_version_to_use() of + ?record_version -> #amqqueue{_ = '_'}; + _ -> amqqueue_v1:pattern_match_all() + end. + +-spec pattern_match_on_name(rabbit_amqqueue:name()) -> amqqueue_pattern(). + +pattern_match_on_name(Name) -> + case record_version_to_use() of + ?record_version -> #amqqueue{name = Name, _ = '_'}; + _ -> amqqueue_v1:pattern_match_on_name(Name) + end. + +-spec pattern_match_on_type(atom()) -> amqqueue_pattern(). + +pattern_match_on_type(Type) -> + case record_version_to_use() of + ?record_version -> #amqqueue{type = Type, _ = '_'}; + _ when Type =:= classic -> amqqueue_v1:pattern_match_all(); + %% FIXME: We try a pattern which should never match when the + %% `quorum_queue` feature flag is not enabled yet. Is there + %% a better solution? + _ -> amqqueue_v1:pattern_match_on_name( + rabbit_misc:r(<<0>>, queue, <<0>>)) + end. + +-spec reset_mirroring_and_decorators(amqqueue()) -> amqqueue(). + +reset_mirroring_and_decorators(#amqqueue{} = Queue) -> + Queue#amqqueue{slave_pids = [], + sync_slave_pids = [], + gm_pids = [], + decorators = undefined}; +reset_mirroring_and_decorators(Queue) -> + amqqueue_v1:reset_mirroring_and_decorators(Queue). + +-spec set_immutable(amqqueue()) -> amqqueue(). + +set_immutable(#amqqueue{} = Queue) -> + Queue#amqqueue{pid = none, + slave_pids = [], + sync_slave_pids = none, + recoverable_slaves = none, + gm_pids = none, + policy = none, + decorators = none, + state = none}; +set_immutable(Queue) -> + amqqueue_v1:set_immutable(Queue). + +-spec qnode(amqqueue() | pid() | ra_server_id()) -> node(). + +qnode(Queue) when ?is_amqqueue(Queue) -> + QPid = get_pid(Queue), + qnode(QPid); +qnode(QPid) when is_pid(QPid) -> + node(QPid); +qnode({_, Node}) -> + Node. + +% private + +macros() -> + io:format( + "-define(is_~s(Q), is_record(Q, amqqueue, ~b)).~n~n", + [?record_version, record_info(size, amqqueue)]), + %% The field number starts at 2 because the first element is the + %% record name. + macros(record_info(fields, amqqueue), 2). + +macros([Field | Rest], I) -> + io:format( + "-define(~s_field_~s(Q), element(~b, Q)).~n", + [?record_version, Field, I]), + macros(Rest, I + 1); +macros([], _) -> + ok. diff --git a/src/amqqueue_v1.erl b/src/amqqueue_v1.erl new file mode 100644 index 0000000000..0b35418d1d --- /dev/null +++ b/src/amqqueue_v1.erl @@ -0,0 +1,403 @@ +%% The contents of this file are subject to the Mozilla Public License +%% Version 1.1 (the "License"); you may not use this file except in +%% compliance with the License. You may obtain a copy of the License +%% at http://www.mozilla.org/MPL/ +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and +%% limitations under the License. +%% +%% The Original Code is RabbitMQ. +%% +%% The Initial Developer of the Original Code is GoPivotal, Inc. +%% Copyright (c) 2018-2019 Pivotal Software, Inc. All rights reserved. +%% + +-module(amqqueue_v1). + +-include_lib("rabbit_common/include/resource.hrl"). + +-export([new/8, + new_with_version/9, + fields/0, + fields/1, + field_vhost/0, + record_version_to_use/0, + upgrade/1, + upgrade_to/2, + % arguments + get_arguments/1, + set_arguments/2, + % decorators + get_decorators/1, + set_decorators/2, + % exclusive_owner + get_exclusive_owner/1, + % gm_pids + get_gm_pids/1, + set_gm_pids/2, + % name + get_name/1, + set_name/2, + % operator_policy + get_operator_policy/1, + set_operator_policy/2, + get_options/1, + % pid + get_pid/1, + set_pid/2, + % policy + get_policy/1, + set_policy/2, + % policy_version + get_policy_version/1, + set_policy_version/2, + % recoverable_slaves + get_recoverable_slaves/1, + set_recoverable_slaves/2, + % slave_pids + get_slave_pids/1, + set_slave_pids/2, + % slave_pids_pending_shutdown + get_slave_pids_pending_shutdown/1, + set_slave_pids_pending_shutdown/2, + % state + get_state/1, + set_state/2, + % sync_slave_pids + get_sync_slave_pids/1, + set_sync_slave_pids/2, + get_vhost/1, + is_amqqueue/1, + is_auto_delete/1, + is_durable/1, + pattern_match_all/0, + pattern_match_on_name/1, + reset_mirroring_and_decorators/1, + set_immutable/1, + macros/0]). + +-define(record_version, ?MODULE). + +-record(amqqueue, { + name :: rabbit_amqqueue:name() | '_', %% immutable + durable :: boolean() | '_', %% immutable + auto_delete :: boolean() | '_', %% immutable + exclusive_owner = none :: pid() | none | '_', %% immutable + arguments = [] :: rabbit_framing:amqp_table() | '_', %% immutable + pid :: pid() | none | '_', %% durable (just so we + %% know home node) + slave_pids = [] :: [pid()] | none | '_', %% transient + sync_slave_pids = [] :: [pid()] | none| '_',%% transient + recoverable_slaves = [] :: [atom()] | none | '_', %% durable + policy :: binary() | none | undefined | '_', %% durable, implicit + %% update as above + operator_policy :: binary() | none | undefined | '_', %% durable, + %% implicit + %% update + %% as above + gm_pids = [] :: [pid()] | none | '_', %% transient + decorators :: [atom()] | none | undefined | '_', %% transient, + %% recalculated + %% as above + state = live :: atom() | none | '_', %% durable (have we crashed?) + policy_version = 0 :: non_neg_integer() | '_', + slave_pids_pending_shutdown = [] :: [pid()] | '_', + vhost :: rabbit_types:vhost() | undefined | '_', %% secondary index + options = #{} :: map() | '_' + }). + +-type amqqueue() :: amqqueue_v1(). +-type amqqueue_v1() :: #amqqueue{ + name :: rabbit_amqqueue:name(), + durable :: boolean(), + auto_delete :: boolean(), + exclusive_owner :: pid() | none, + arguments :: rabbit_framing:amqp_table(), + pid :: pid() | none, + slave_pids :: [pid()] | none, + sync_slave_pids :: [pid()] | none, + recoverable_slaves :: [atom()] | none, + policy :: binary() | none | undefined, + operator_policy :: binary() | none | undefined, + gm_pids :: [pid()] | none, + decorators :: [atom()] | none | undefined, + state :: atom() | none, + policy_version :: non_neg_integer(), + slave_pids_pending_shutdown :: [pid()], + vhost :: rabbit_types:vhost() | undefined, + options :: map() + }. + +-type amqqueue_pattern() :: amqqueue_v1_pattern(). +-type amqqueue_v1_pattern() :: #amqqueue{ + name :: rabbit_amqqueue:name() | '_', + durable :: '_', + auto_delete :: '_', + exclusive_owner :: '_', + arguments :: '_', + pid :: '_', + slave_pids :: '_', + sync_slave_pids :: '_', + recoverable_slaves :: '_', + policy :: '_', + operator_policy :: '_', + gm_pids :: '_', + decorators :: '_', + state :: '_', + policy_version :: '_', + slave_pids_pending_shutdown :: '_', + vhost :: '_', + options :: '_' + }. + +-export_type([amqqueue/0, + amqqueue_v1/0, + amqqueue_pattern/0, + amqqueue_v1_pattern/0]). + +-spec new(rabbit_amqqueue:name(), + pid() | none, + boolean(), + boolean(), + pid() | none, + rabbit_framing:amqp_table(), + rabbit_types:vhost() | undefined, + map()) -> amqqueue(). + +new(#resource{kind = queue} = Name, + Pid, + Durable, + AutoDelete, + Owner, + Args, + VHost, + Options) + when (is_pid(Pid) orelse Pid =:= none) andalso + is_boolean(Durable) andalso + is_boolean(AutoDelete) andalso + (is_pid(Owner) orelse Owner =:= none) andalso + is_list(Args) andalso + (is_binary(VHost) orelse VHost =:= undefined) andalso + is_map(Options) -> + new_with_version( + ?record_version, + Name, + Pid, + Durable, + AutoDelete, + Owner, + Args, + VHost, + Options). + +-spec new_with_version(amqqueue_v1, + rabbit_amqqueue:name(), + pid() | none, + boolean(), + boolean(), + pid() | none, + rabbit_framing:amqp_table(), + rabbit_types:vhost() | undefined, + map()) -> amqqueue(). + +new_with_version(?record_version, + #resource{kind = queue} = Name, + Pid, + Durable, + AutoDelete, + Owner, + Args, + VHost, + Options) + when (is_pid(Pid) orelse Pid =:= none) andalso + is_boolean(Durable) andalso + is_boolean(AutoDelete) andalso + (is_pid(Owner) orelse Owner =:= none) andalso + is_list(Args) andalso + (is_binary(VHost) orelse VHost =:= undefined) andalso + is_map(Options) -> + #amqqueue{name = Name, + durable = Durable, + auto_delete = AutoDelete, + arguments = Args, + exclusive_owner = Owner, + pid = Pid, + vhost = VHost, + options = Options}. + +-spec is_amqqueue(any()) -> boolean(). + +is_amqqueue(#amqqueue{}) -> true; +is_amqqueue(_) -> false. + +-spec record_version_to_use() -> amqqueue_v1. + +record_version_to_use() -> + ?record_version. + +-spec upgrade(amqqueue()) -> amqqueue(). + +upgrade(#amqqueue{} = Queue) -> + Queue. + +-spec upgrade_to(amqqueue_v1, amqqueue()) -> amqqueue(). + +upgrade_to(?record_version, #amqqueue{} = Queue) -> + Queue. + +% arguments + +-spec get_arguments(amqqueue()) -> rabbit_framing:amqp_table(). + +get_arguments(#amqqueue{arguments = Args}) -> Args. + +-spec set_arguments(amqqueue(), rabbit_framing:amqp_table()) -> amqqueue(). + +set_arguments(#amqqueue{} = Queue, Args) -> + Queue#amqqueue{arguments = Args}. + +% decorators + +get_decorators(#amqqueue{decorators = Decorators}) -> Decorators. + +set_decorators(#amqqueue{} = Queue, Decorators) -> + Queue#amqqueue{decorators = Decorators}. + +get_exclusive_owner(#amqqueue{exclusive_owner = Owner}) -> Owner. + +% gm_pids + +get_gm_pids(#amqqueue{gm_pids = GMPids}) -> GMPids. + +set_gm_pids(#amqqueue{} = Queue, GMPids) -> + Queue#amqqueue{gm_pids = GMPids}. + +% name + +get_name(#amqqueue{name = Name}) -> Name. + +set_name(#amqqueue{} = Queue, Name) -> + Queue#amqqueue{name = Name}. + +% operator_policy + +get_operator_policy(#amqqueue{operator_policy = OpPolicy}) -> OpPolicy. + +set_operator_policy(#amqqueue{} = Queue, OpPolicy) -> + Queue#amqqueue{operator_policy = OpPolicy}. + +get_options(#amqqueue{options = Options}) -> Options. + +% pid + +get_pid(#amqqueue{pid = Pid}) -> Pid. + +set_pid(#amqqueue{} = Queue, Pid) -> + Queue#amqqueue{pid = Pid}. + +% policy + +get_policy(#amqqueue{policy = Policy}) -> Policy. + +set_policy(#amqqueue{} = Queue, Policy) -> + Queue#amqqueue{policy = Policy}. + +% policy_version + +get_policy_version(#amqqueue{policy_version = PV}) -> + PV. + +set_policy_version(#amqqueue{} = Queue, PV) -> + Queue#amqqueue{policy_version = PV}. + +% recoverable_slaves + +get_recoverable_slaves(#amqqueue{recoverable_slaves = Slaves}) -> + Slaves. + +set_recoverable_slaves(#amqqueue{} = Queue, Slaves) -> + Queue#amqqueue{recoverable_slaves = Slaves}. + +% slave_pids + +get_slave_pids(#amqqueue{slave_pids = Slaves}) -> + Slaves. + +set_slave_pids(#amqqueue{} = Queue, SlavePids) -> + Queue#amqqueue{slave_pids = SlavePids}. + +% slave_pids_pending_shutdown + +get_slave_pids_pending_shutdown(#amqqueue{slave_pids_pending_shutdown = Slaves}) -> + Slaves. + +set_slave_pids_pending_shutdown(#amqqueue{} = Queue, SlavePids) -> + Queue#amqqueue{slave_pids_pending_shutdown = SlavePids}. + +% state + +get_state(#amqqueue{state = State}) -> State. + +set_state(#amqqueue{} = Queue, State) -> Queue#amqqueue{state = State}. + +% sync_slave_pids + +get_sync_slave_pids(#amqqueue{sync_slave_pids = Pids}) -> Pids. + +set_sync_slave_pids(#amqqueue{} = Queue, Pids) -> + Queue#amqqueue{sync_slave_pids = Pids}. + +get_vhost(#amqqueue{vhost = VHost}) -> VHost. + +is_auto_delete(#amqqueue{auto_delete = AutoDelete}) -> AutoDelete. + +is_durable(#amqqueue{durable = Durable}) -> Durable. + +fields() -> fields(?record_version). + +fields(?record_version) -> record_info(fields, amqqueue). + +field_vhost() -> #amqqueue.vhost. + +-spec pattern_match_all() -> amqqueue_pattern(). + +pattern_match_all() -> #amqqueue{_ = '_'}. + +-spec pattern_match_on_name(rabbit_amqqueue:name()) -> + amqqueue_pattern(). + +pattern_match_on_name(Name) -> #amqqueue{name = Name, _ = '_'}. + +reset_mirroring_and_decorators(#amqqueue{} = Queue) -> + Queue#amqqueue{slave_pids = [], + sync_slave_pids = [], + gm_pids = [], + decorators = undefined}. + +set_immutable(#amqqueue{} = Queue) -> + Queue#amqqueue{pid = none, + slave_pids = none, + sync_slave_pids = none, + recoverable_slaves = none, + gm_pids = none, + policy = none, + decorators = none, + state = none}. + +macros() -> + io:format( + "-define(is_~s(Q), is_record(Q, amqqueue, ~b)).~n~n", + [?record_version, record_info(size, amqqueue)]), + %% The field number starts at 2 because the first element is the + %% record name. + macros(record_info(fields, amqqueue), 2). + +macros([Field | Rest], I) -> + io:format( + "-define(~s_field_~s(Q), element(~b, Q)).~n", + [?record_version, Field, I]), + macros(Rest, I + 1); +macros([], _) -> + ok. diff --git a/src/background_gc.erl b/src/background_gc.erl index 43c109ee2a..7aea28c1f4 100644 --- a/src/background_gc.erl +++ b/src/background_gc.erl @@ -32,14 +32,12 @@ %%---------------------------------------------------------------------------- -spec start_link() -> {'ok', pid()} | {'error', any()}. --spec run() -> 'ok'. --spec gc() -> 'ok'. - -%%---------------------------------------------------------------------------- start_link() -> gen_server2:start_link({local, ?MODULE}, ?MODULE, [], [{timeout, infinity}]). +-spec run() -> 'ok'. + run() -> gen_server2:cast(?MODULE, run). %%---------------------------------------------------------------------------- @@ -73,6 +71,8 @@ interval_gc(State = #state{last_interval = LastInterval}) -> erlang:send_after(Interval, self(), run), State#state{last_interval = Interval}. +-spec gc() -> 'ok'. + gc() -> Enabled = rabbit_misc:get_env(rabbit, background_gc_enabled, false), case Enabled of diff --git a/src/dtree.erl b/src/dtree.erl index 08ddd22532..fd2188de29 100644 --- a/src/dtree.erl +++ b/src/dtree.erl @@ -46,24 +46,17 @@ -type val() :: any(). -type kv() :: {pk(), val()}. --spec empty() -> ?MODULE(). --spec insert(pk(), [sk()], val(), ?MODULE()) -> ?MODULE(). --spec take([pk()], sk(), ?MODULE()) -> {[kv()], ?MODULE()}. --spec take(sk(), ?MODULE()) -> {[kv()], ?MODULE()}. --spec take_one(pk(), ?MODULE()) -> {[{pk(), val()}], ?MODULE()}. --spec take_all(sk(), ?MODULE()) -> {[kv()], ?MODULE()}. --spec drop(pk(), ?MODULE()) -> ?MODULE(). --spec is_defined(sk(), ?MODULE()) -> boolean(). --spec is_empty(?MODULE()) -> boolean(). --spec smallest(?MODULE()) -> kv(). --spec size(?MODULE()) -> non_neg_integer(). - %%---------------------------------------------------------------------------- +-spec empty() -> ?MODULE(). + empty() -> {gb_trees:empty(), gb_trees:empty()}. %% Insert an entry. Fails if there already is an entry with the given %% primary key. + +-spec insert(pk(), [sk()], val(), ?MODULE()) -> ?MODULE(). + insert(PK, [], V, {P, S}) -> %% dummy insert to force error if PK exists _ = gb_trees:insert(PK, {gb_sets:empty(), V}, P), @@ -84,6 +77,9 @@ insert(PK, SKs, V, {P, S}) -> %% that were dropped as the result (i.e. due to their secondary key %% set becoming empty). It is ok for the given primary keys and/or %% secondary key to not exist. + +-spec take([pk()], sk(), ?MODULE()) -> {[kv()], ?MODULE()}. + take(PKs, SK, {P, S}) -> case gb_trees:lookup(SK, S) of none -> {[], {P, S}}; @@ -101,6 +97,9 @@ take(PKs, SK, {P, S}) -> %% primary-key/value pairs of any entries that were dropped as the %% result (i.e. due to their secondary key set becoming empty). It is %% ok for the given secondary key to not exist. + +-spec take(sk(), ?MODULE()) -> {[kv()], ?MODULE()}. + take(SK, {P, S}) -> case gb_trees:lookup(SK, S) of none -> {[], {P, S}}; @@ -111,6 +110,9 @@ take(SK, {P, S}) -> %% Drop an entry with the primary key and clears secondary keys for this key, %% returning a list with a key-value pair as a result. %% If the primary key does not exist, returns an empty list. + +-spec take_one(pk(), ?MODULE()) -> {[{pk(), val()}], ?MODULE()}. + take_one(PK, {P, S}) -> case gb_trees:lookup(PK, P) of {value, {SKS, Value}} -> @@ -131,6 +133,9 @@ take_one(PK, {P, S}) -> %% Drop all entries which contain the given secondary key, returning %% the primary-key/value pairs of these entries. It is ok for the %% given secondary key to not exist. + +-spec take_all(sk(), ?MODULE()) -> {[kv()], ?MODULE()}. + take_all(SK, {P, S}) -> case gb_trees:lookup(SK, S) of none -> {[], {P, S}}; @@ -139,6 +144,9 @@ take_all(SK, {P, S}) -> end. %% Drop all entries for the given primary key (which does not have to exist). + +-spec drop(pk(), ?MODULE()) -> ?MODULE(). + drop(PK, {P, S}) -> case gb_trees:lookup(PK, P) of none -> {P, S}; @@ -146,13 +154,21 @@ drop(PK, {P, S}) -> prune(SKS, gb_sets:singleton(PK), S)} end. +-spec is_defined(sk(), ?MODULE()) -> boolean(). + is_defined(SK, {_P, S}) -> gb_trees:is_defined(SK, S). +-spec is_empty(?MODULE()) -> boolean(). + is_empty({P, _S}) -> gb_trees:is_empty(P). +-spec smallest(?MODULE()) -> kv(). + smallest({P, _S}) -> {K, {_SKS, V}} = gb_trees:smallest(P), {K, V}. +-spec size(?MODULE()) -> non_neg_integer(). + size({P, _S}) -> gb_trees:size(P). %%---------------------------------------------------------------------------- diff --git a/src/gatherer.erl b/src/gatherer.erl index a8b55892c1..1625468a52 100644 --- a/src/gatherer.erl +++ b/src/gatherer.erl @@ -39,16 +39,6 @@ %%---------------------------------------------------------------------------- --spec start_link() -> rabbit_types:ok_pid_or_error(). --spec stop(pid()) -> 'ok'. --spec fork(pid()) -> 'ok'. --spec finish(pid()) -> 'ok'. --spec in(pid(), any()) -> 'ok'. --spec sync_in(pid(), any()) -> 'ok'. --spec out(pid()) -> {'value', any()} | 'empty'. - -%%---------------------------------------------------------------------------- - -define(HIBERNATE_AFTER_MIN, 1000). -define(DESIRED_HIBERNATE, 10000). @@ -58,24 +48,38 @@ %%---------------------------------------------------------------------------- +-spec start_link() -> rabbit_types:ok_pid_or_error(). + start_link() -> gen_server2:start_link(?MODULE, [], [{timeout, infinity}]). +-spec stop(pid()) -> 'ok'. + stop(Pid) -> gen_server2:call(Pid, stop, infinity). +-spec fork(pid()) -> 'ok'. + fork(Pid) -> gen_server2:call(Pid, fork, infinity). +-spec finish(pid()) -> 'ok'. + finish(Pid) -> gen_server2:cast(Pid, finish). +-spec in(pid(), any()) -> 'ok'. + in(Pid, Value) -> gen_server2:cast(Pid, {in, Value}). +-spec sync_in(pid(), any()) -> 'ok'. + sync_in(Pid, Value) -> gen_server2:call(Pid, {in, Value}, infinity). +-spec out(pid()) -> {'value', any()} | 'empty'. + out(Pid) -> gen_server2:call(Pid, out, infinity). diff --git a/src/gm.erl b/src/gm.erl index 427fa78f4e..02ee76cd60 100644 --- a/src/gm.erl +++ b/src/gm.erl @@ -436,16 +436,6 @@ -type group_name() :: any(). -type txn_fun() :: fun((fun(() -> any())) -> any()). --spec create_tables() -> 'ok' | {'aborted', any()}. --spec start_link(group_name(), atom(), any(), txn_fun()) -> - rabbit_types:ok_pid_or_error(). --spec leave(pid()) -> 'ok'. --spec broadcast(pid(), any()) -> 'ok'. --spec confirmed_broadcast(pid(), any()) -> 'ok'. --spec info(pid()) -> rabbit_types:infos(). --spec validate_members(pid(), [pid()]) -> 'ok'. --spec forget_group(group_name()) -> 'ok'. - %% The joined, members_changed and handle_msg callbacks can all return %% any of the following terms: %% @@ -490,6 +480,8 @@ -callback handle_terminate(Args :: term(), Reason :: term()) -> ok | term(). +-spec create_tables() -> 'ok' | {'aborted', any()}. + create_tables() -> create_tables([?TABLE]). @@ -506,27 +498,42 @@ table_definitions() -> {Name, Attributes} = ?TABLE, [{Name, [?TABLE_MATCH | Attributes]}]. +-spec start_link(group_name(), atom(), any(), txn_fun()) -> + rabbit_types:ok_pid_or_error(). + start_link(GroupName, Module, Args, TxnFun) -> gen_server2:start_link(?MODULE, [GroupName, Module, Args, TxnFun], [{spawn_opt, [{fullsweep_after, 0}]}]). +-spec leave(pid()) -> 'ok'. + leave(Server) -> gen_server2:cast(Server, leave). +-spec broadcast(pid(), any()) -> 'ok'. + broadcast(Server, Msg) -> broadcast(Server, Msg, 0). broadcast(Server, Msg, SizeHint) -> gen_server2:cast(Server, {broadcast, Msg, SizeHint}). +-spec confirmed_broadcast(pid(), any()) -> 'ok'. + confirmed_broadcast(Server, Msg) -> gen_server2:call(Server, {confirmed_broadcast, Msg}, infinity). +-spec info(pid()) -> rabbit_types:infos(). + info(Server) -> gen_server2:call(Server, info, infinity). +-spec validate_members(pid(), [pid()]) -> 'ok'. + validate_members(Server, Members) -> gen_server2:cast(Server, {validate_members, Members}). +-spec forget_group(group_name()) -> 'ok'. + forget_group(GroupName) -> {atomic, ok} = mnesia:sync_transaction( fun () -> diff --git a/src/lqueue.erl b/src/lqueue.erl index acfdbe79ef..820f9f4a2f 100644 --- a/src/lqueue.erl +++ b/src/lqueue.erl @@ -36,64 +36,76 @@ -type result(T) :: 'empty' | {'value', T}. -spec new() -> ?MODULE(_). --spec drop(?MODULE(T)) -> ?MODULE(T). --spec is_empty(?MODULE(_)) -> boolean(). --spec len(?MODULE(_)) -> non_neg_integer(). --spec in(T, ?MODULE(T)) -> ?MODULE(T). --spec in_r(value(), ?MODULE()) -> ?MODULE(). --spec out(?MODULE(T)) -> {result(T), ?MODULE()}. --spec out_r(?MODULE(T)) -> {result(T), ?MODULE()}. --spec join(?MODULE(A), ?MODULE(B)) -> ?MODULE(A | B). --spec foldl(fun ((T, B) -> B), B, ?MODULE(T)) -> B. --spec foldr(fun ((T, B) -> B), B, ?MODULE(T)) -> B. --spec from_list([T]) -> ?MODULE(T). --spec to_list(?MODULE(T)) -> [T]. -% -spec peek(?MODULE()) -> result(). --spec peek(?MODULE(T)) -> result(T). --spec peek_r(?MODULE(T)) -> result(T). new() -> {0, ?QUEUE:new()}. +-spec drop(?MODULE(T)) -> ?MODULE(T). + drop({L, Q}) -> {L - 1, ?QUEUE:drop(Q)}. +-spec is_empty(?MODULE(_)) -> boolean(). + is_empty({0, _Q}) -> true; is_empty(_) -> false. +-spec in(T, ?MODULE(T)) -> ?MODULE(T). + in(V, {L, Q}) -> {L+1, ?QUEUE:in(V, Q)}. +-spec in_r(value(), ?MODULE()) -> ?MODULE(). + in_r(V, {L, Q}) -> {L+1, ?QUEUE:in_r(V, Q)}. +-spec out(?MODULE(T)) -> {result(T), ?MODULE()}. + out({0, _Q} = Q) -> {empty, Q}; out({L, Q}) -> {Result, Q1} = ?QUEUE:out(Q), {Result, {L-1, Q1}}. +-spec out_r(?MODULE(T)) -> {result(T), ?MODULE()}. + out_r({0, _Q} = Q) -> {empty, Q}; out_r({L, Q}) -> {Result, Q1} = ?QUEUE:out_r(Q), {Result, {L-1, Q1}}. +-spec join(?MODULE(A), ?MODULE(B)) -> ?MODULE(A | B). + join({L1, Q1}, {L2, Q2}) -> {L1 + L2, ?QUEUE:join(Q1, Q2)}. +-spec to_list(?MODULE(T)) -> [T]. + to_list({_L, Q}) -> ?QUEUE:to_list(Q). +-spec from_list([T]) -> ?MODULE(T). + from_list(L) -> {length(L), ?QUEUE:from_list(L)}. +-spec foldl(fun ((T, B) -> B), B, ?MODULE(T)) -> B. + foldl(Fun, Init, Q) -> case out(Q) of {empty, _Q} -> Init; {{value, V}, Q1} -> foldl(Fun, Fun(V, Init), Q1) end. +-spec foldr(fun ((T, B) -> B), B, ?MODULE(T)) -> B. + foldr(Fun, Init, Q) -> case out_r(Q) of {empty, _Q} -> Init; {{value, V}, Q1} -> foldr(Fun, Fun(V, Init), Q1) end. +-spec len(?MODULE(_)) -> non_neg_integer(). + len({L, _}) -> L. +-spec peek(?MODULE(T)) -> result(T). peek({ 0, _Q}) -> empty; peek({_L, Q}) -> ?QUEUE:peek(Q). +-spec peek_r(?MODULE(T)) -> result(T). + peek_r({ 0, _Q}) -> empty; peek_r({_L, Q}) -> ?QUEUE:peek_r(Q). diff --git a/src/pg_local.erl b/src/pg_local.erl index 0ed7e9d85d..3f03c97182 100644 --- a/src/pg_local.erl +++ b/src/pg_local.erl @@ -44,15 +44,6 @@ -type name() :: term(). --spec start_link() -> {'ok', pid()} | {'error', any()}. --spec start() -> {'ok', pid()} | {'error', any()}. --spec join(name(), pid()) -> 'ok'. --spec leave(name(), pid()) -> 'ok'. --spec get_members(name()) -> [pid()]. --spec in_group(name(), pid()) -> boolean(). - --spec sync() -> 'ok'. - %%---------------------------------------------------------------------------- -define(TABLE, pg_local_table). @@ -61,24 +52,36 @@ %%% Exported functions %%% +-spec start_link() -> {'ok', pid()} | {'error', any()}. + start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). +-spec start() -> {'ok', pid()} | {'error', any()}. + start() -> ensure_started(). +-spec join(name(), pid()) -> 'ok'. + join(Name, Pid) when is_pid(Pid) -> _ = ensure_started(), gen_server:cast(?MODULE, {join, Name, Pid}). +-spec leave(name(), pid()) -> 'ok'. + leave(Name, Pid) when is_pid(Pid) -> _ = ensure_started(), gen_server:cast(?MODULE, {leave, Name, Pid}). +-spec get_members(name()) -> [pid()]. + get_members(Name) -> _ = ensure_started(), group_members(Name). +-spec in_group(name(), pid()) -> boolean(). + in_group(Name, Pid) -> _ = ensure_started(), %% The join message is a cast and thus can race, but we want to @@ -89,6 +92,8 @@ in_group(Name, Pid) -> member_present(Name, Pid) end. +-spec sync() -> 'ok'. + sync() -> _ = ensure_started(), gen_server:call(?MODULE, sync, infinity). diff --git a/src/rabbit.erl b/src/rabbit.erl index d40aa5a279..2d16661768 100644 --- a/src/rabbit.erl +++ b/src/rabbit.erl @@ -25,7 +25,7 @@ -export([start/0, boot/0, stop/0, stop_and_halt/0, await_startup/0, await_startup/1, await_startup/3, status/0, is_running/0, alarms/0, - is_running/1, environment/0, rotate_logs/0, + is_running/1, environment/0, rotate_logs/0, force_event_refresh/1, start_fhc/0]). -export([start/2, stop/1, prep_stop/1]). @@ -33,6 +33,8 @@ -export([log_locations/0, config_files/0, decrypt_config/2]). %% for testing and mgmt-agent -export([is_booted/1, is_booted/0, is_booting/1, is_booting/0]). +-deprecated([{force_event_refresh, 1, eventually}]). + -ifdef(TEST). -export([start_logger/0]). @@ -63,6 +65,12 @@ {requires, pre_boot}, {enables, external_infrastructure}]}). +-rabbit_boot_step({feature_flags, + [{description, "feature flags registry and initial state"}, + {mfa, {rabbit_feature_flags, init, []}}, + {requires, pre_boot}, + {enables, external_infrastructure}]}). + -rabbit_boot_step({database, [{mfa, {rabbit_mnesia, init, []}}, {requires, file_handle_cache}, @@ -251,40 +259,6 @@ -type param() :: atom(). -type app_name() :: atom(). --spec start() -> 'ok'. --spec boot() -> 'ok'. --spec stop() -> 'ok'. --spec stop_and_halt() -> no_return(). - --spec status - () -> [{pid, integer()} | - {running_applications, [{atom(), string(), string()}]} | - {os, {atom(), atom()}} | - {erlang_version, string()} | - {memory, any()}]. --spec is_running() -> boolean(). --spec is_running(node()) -> boolean(). --spec environment() -> [{param(), term()}]. --spec rotate_logs() -> rabbit_types:ok_or_error(any()). - --spec log_locations() -> [log_location()]. - --spec start('normal',[]) -> - {'error', - {'erlang_version_too_old', - {'found',string(),string()}, - {'required',string(),string()}}} | - {'ok',pid()}. --spec stop(_) -> 'ok'. - --spec maybe_insert_default_data() -> 'ok'. --spec boot_delegate() -> 'ok'. --spec recover() -> 'ok'. --spec start_apps([app_name()]) -> 'ok'. --spec start_apps([app_name()], - #{app_name() => restart_type()}) -> 'ok'. --spec stop_apps([app_name()]) -> 'ok'. - %%---------------------------------------------------------------------------- ensure_application_loaded() -> @@ -296,6 +270,8 @@ ensure_application_loaded() -> {error, {already_loaded, rabbit}} -> ok end. +-spec start() -> 'ok'. + start() -> start_it(fun() -> %% We do not want to upgrade mnesia after just @@ -309,6 +285,8 @@ start() -> broker_start() end). +-spec boot() -> 'ok'. + boot() -> start_it(fun() -> ensure_config(), @@ -486,6 +464,8 @@ start_it(StartFun) -> Marker ! stop end. +-spec stop() -> 'ok'. + stop() -> case whereis(rabbit_boot) of undefined -> ok; @@ -500,6 +480,8 @@ stop() -> stop_apps(app_utils:app_dependency_order(Apps, true)), rabbit_log:info("Successfully stopped RabbitMQ and its dependencies~n", []). +-spec stop_and_halt() -> no_return(). + stop_and_halt() -> try stop() @@ -525,11 +507,17 @@ stop_and_halt() -> end, ok. +-spec start_apps([app_name()]) -> 'ok'. + start_apps(Apps) -> start_apps(Apps, #{}). +-spec start_apps([app_name()], + #{app_name() => restart_type()}) -> 'ok'. + start_apps(Apps, RestartTypes) -> app_utils:load_applications(Apps), + rabbit_feature_flags:initialize_registry(), ensure_sysmon_handler_app_config(), %% make Ra use a custom logger that dispatches to lager instead of the %% default OTP logger @@ -669,6 +657,8 @@ decrypt_list([{Key, Value}|Tail], Algo, Acc) when Key =/= encrypted -> decrypt_list([Value|Tail], Algo, Acc) -> decrypt_list(Tail, Algo, [decrypt(Value, Algo)|Acc]). +-spec stop_apps([app_name()]) -> 'ok'. + stop_apps([]) -> ok; stop_apps(Apps) -> @@ -703,16 +693,19 @@ is_booting(Node) -> -spec await_startup() -> 'ok' | {'error', 'timeout'}. + await_startup() -> await_startup(node(), false). -spec await_startup(node() | non_neg_integer()) -> 'ok' | {'error', 'timeout'}. + await_startup(Node) when is_atom(Node) -> await_startup(Node, false); await_startup(Timeout) when is_integer(Timeout) -> await_startup(node(), false, Timeout). -spec await_startup(node(), boolean()) -> 'ok' | {'error', 'timeout'}. + await_startup(Node, PrintProgressReports) -> case is_booting(Node) of true -> wait_for_boot_to_finish(Node, PrintProgressReports); @@ -725,6 +718,7 @@ await_startup(Node, PrintProgressReports) -> end. -spec await_startup(node(), boolean(), non_neg_integer()) -> 'ok' | {'error', 'timeout'}. + await_startup(Node, PrintProgressReports, Timeout) -> case is_booting(Node) of true -> wait_for_boot_to_finish(Node, PrintProgressReports, Timeout); @@ -796,6 +790,13 @@ maybe_print_boot_progress(true, IterationsLeft) -> _ -> ok end. +-spec status + () -> [{pid, integer()} | + {running_applications, [{atom(), string(), string()}]} | + {os, {atom(), atom()}} | + {erlang_version, string()} | + {memory, any()}]. + status() -> S1 = [{pid, list_to_integer(os:getpid())}, %% The timeout value used is twice that of gen_server:call/2. @@ -851,8 +852,13 @@ listeners() -> %% TODO this only determines if the rabbit application has started, %% not if it is running, never mind plugins. It would be nice to have %% more nuance here. + +-spec is_running() -> boolean(). + is_running() -> is_running(node()). +-spec is_running(node()) -> boolean(). + is_running(Node) -> rabbit_nodes:is_process_running(Node, rabbit). is_booted() -> is_booted(node()). @@ -864,6 +870,8 @@ is_booted(Node) -> _ -> false end. +-spec environment() -> [{param(), term()}]. + environment() -> %% The timeout value is twice that of gen_server:call/2. [{A, environment(A)} || @@ -874,6 +882,8 @@ environment(App) -> lists:keysort(1, [P || P = {K, _} <- application:get_all_env(App), not lists:member(K, Ignore)]). +-spec rotate_logs() -> rabbit_types:ok_or_error(any()). + rotate_logs() -> rabbit_lager:fold_sinks( fun @@ -897,6 +907,13 @@ rotate_logs() -> %%-------------------------------------------------------------------- +-spec start('normal',[]) -> + {'error', + {'erlang_version_too_old', + {'found',string(),string()}, + {'required',string(),string()}}} | + {'ok',pid()}. + start(normal, []) -> case erts_version_check() of ok -> @@ -929,6 +946,8 @@ prep_stop(State) -> rabbit_peer_discovery:maybe_unregister(), State. +-spec stop(_) -> 'ok'. + stop(_State) -> ok = rabbit_alarm:stop(), ok = case rabbit_mnesia:is_clustered() of @@ -983,14 +1002,20 @@ log_boot_error_and_exit(Reason, Format, Args) -> %%--------------------------------------------------------------------------- %% boot step functions +-spec boot_delegate() -> 'ok'. + boot_delegate() -> {ok, Count} = application:get_env(rabbit, delegate_count), rabbit_sup:start_supervisor_child(delegate_sup, [Count]). +-spec recover() -> 'ok'. + recover() -> rabbit_policy:recover(), rabbit_vhost:recover(). +-spec maybe_insert_default_data() -> 'ok'. + maybe_insert_default_data() -> case rabbit_table:needs_default_data() of true -> insert_default_data(); @@ -1035,9 +1060,23 @@ start_logger() -> rabbit_lager:start_logger(), ok. +-spec log_locations() -> [log_location()]. + log_locations() -> rabbit_lager:log_locations(). +%% This feature was used by the management API up-to and including +%% RabbitMQ 3.7.x. It is unused in 3.8.x and thus deprecated. We keep it +%% to support in-place upgrades to 3.8.x (i.e. mixed-version clusters). + +-spec force_event_refresh(reference()) -> 'ok'. + +force_event_refresh(Ref) -> + rabbit_direct:force_event_refresh(Ref), + rabbit_networking:force_connection_event_refresh(Ref), + rabbit_channel:force_event_refresh(Ref), + rabbit_amqqueue:force_event_refresh(Ref). + %%--------------------------------------------------------------------------- %% misc diff --git a/src/rabbit_access_control.erl b/src/rabbit_access_control.erl index 4356cb427f..44e72f7ce4 100644 --- a/src/rabbit_access_control.erl +++ b/src/rabbit_access_control.erl @@ -27,29 +27,20 @@ -type permission_atom() :: 'configure' | 'read' | 'write'. +%%---------------------------------------------------------------------------- + -spec check_user_pass_login (rabbit_types:username(), rabbit_types:password()) -> {'ok', rabbit_types:user()} | {'refused', rabbit_types:username(), string(), [any()]}. + +check_user_pass_login(Username, Password) -> + check_user_login(Username, [{password, Password}]). + -spec check_user_login (rabbit_types:username(), [{atom(), any()}]) -> {'ok', rabbit_types:user()} | {'refused', rabbit_types:username(), string(), [any()]}. --spec check_user_loopback - (rabbit_types:username(), rabbit_net:socket() | inet:ip_address()) -> - 'ok' | 'not_allowed'. --spec check_vhost_access - (rabbit_types:user(), rabbit_types:vhost(), - rabbit_net:socket() | #authz_socket_info{}) -> - 'ok' | rabbit_types:channel_exit(). --spec check_resource_access - (rabbit_types:user(), rabbit_types:r(atom()), permission_atom()) -> - 'ok' | rabbit_types:channel_exit(). - -%%---------------------------------------------------------------------------- - -check_user_pass_login(Username, Password) -> - check_user_login(Username, [{password, Password}]). check_user_login(Username, AuthProps) -> {ok, Modules} = application:get_env(rabbit, auth_backends), @@ -122,6 +113,10 @@ auth_user(#user{username = Username, tags = Tags}, Impl) -> tags = Tags, impl = Impl}. +-spec check_user_loopback + (rabbit_types:username(), rabbit_net:socket() | inet:ip_address()) -> + 'ok' | 'not_allowed'. + check_user_loopback(Username, SockOrAddr) -> {ok, Users} = application:get_env(rabbit, loopback_users), case rabbit_net:is_loopback(SockOrAddr) @@ -130,6 +125,11 @@ check_user_loopback(Username, SockOrAddr) -> false -> not_allowed end. +-spec check_vhost_access + (rabbit_types:user(), rabbit_types:vhost(), + rabbit_net:socket() | #authz_socket_info{}) -> + 'ok' | rabbit_types:channel_exit(). + check_vhost_access(User = #user{username = Username, authz_backends = Modules}, VHostPath, Sock) -> lists:foldl( @@ -146,6 +146,10 @@ check_vhost_access(User = #user{username = Username, Else end, ok, Modules). +-spec check_resource_access + (rabbit_types:user(), rabbit_types:r(atom()), permission_atom()) -> + 'ok' | rabbit_types:channel_exit(). + check_resource_access(User, R = #resource{kind = exchange, name = <<"">>}, Permission) -> check_resource_access(User, R#resource{name = <<"amq.default">>}, diff --git a/src/rabbit_alarm.erl b/src/rabbit_alarm.erl index 789992cb3d..6f33e02fe4 100644 --- a/src/rabbit_alarm.erl +++ b/src/rabbit_alarm.erl @@ -52,21 +52,15 @@ -type resource_alarm() :: {resource_limit, resource_alarm_source(), node()}. -type alarm() :: local_alarm() | resource_alarm(). --spec start_link() -> rabbit_types:ok_pid_or_error(). --spec start() -> 'ok'. --spec stop() -> 'ok'. --spec register(pid(), rabbit_types:mfargs()) -> [atom()]. --spec set_alarm({alarm(), []}) -> 'ok'. --spec clear_alarm(alarm()) -> 'ok'. --spec on_node_up(node()) -> 'ok'. --spec on_node_down(node()) -> 'ok'. --spec get_alarms() -> [{alarm(), []}]. - %%---------------------------------------------------------------------------- +-spec start_link() -> rabbit_types:ok_pid_or_error(). + start_link() -> gen_event:start_link({local, ?SERVER}). +-spec start() -> 'ok'. + start() -> ok = rabbit_sup:start_restartable_child(?MODULE), ok = gen_event:add_handler(?SERVER, ?MODULE, []), @@ -84,21 +78,38 @@ start() -> rabbit_disk_monitor, [DiskLimit]), ok. +-spec stop() -> 'ok'. + stop() -> ok. %% Registers a handler that should be called on every resource alarm change. %% Given a call rabbit_alarm:register(Pid, {M, F, A}), the handler would be %% called like this: `apply(M, F, A ++ [Pid, Source, Alert])', where `Source' %% has the type of resource_alarm_source() and `Alert' has the type of resource_alert(). + +-spec register(pid(), rabbit_types:mfargs()) -> [atom()]. + register(Pid, AlertMFA) -> gen_event:call(?SERVER, ?MODULE, {register, Pid, AlertMFA}, infinity). +-spec set_alarm({alarm(), []}) -> 'ok'. + set_alarm(Alarm) -> gen_event:notify(?SERVER, {set_alarm, Alarm}). + +-spec clear_alarm(alarm()) -> 'ok'. + clear_alarm(Alarm) -> gen_event:notify(?SERVER, {clear_alarm, Alarm}). +-spec get_alarms() -> [{alarm(), []}]. + get_alarms() -> gen_event:call(?SERVER, ?MODULE, get_alarms, infinity). +-spec on_node_up(node()) -> 'ok'. + on_node_up(Node) -> gen_event:notify(?SERVER, {node_up, Node}). + +-spec on_node_down(node()) -> 'ok'. + on_node_down(Node) -> gen_event:notify(?SERVER, {node_down, Node}). remote_conserve_resources(Pid, Source, {true, _, _}) -> diff --git a/src/rabbit_amqqueue.erl b/src/rabbit_amqqueue.erl index 63d89ff4a7..c9c120df77 100644 --- a/src/rabbit_amqqueue.erl +++ b/src/rabbit_amqqueue.erl @@ -21,17 +21,19 @@ delete_immediately/1, delete_exclusive/2, delete/4, purge/1, forget_all_durable/1, delete_crashed/1, delete_crashed/2, delete_crashed_internal/2]). --export([pseudo_queue/2, immutable/1]). +-export([pseudo_queue/2, pseudo_queue/3, immutable/1]). -export([lookup/1, not_found_or_absent/1, with/2, with/3, with_or_die/2, assert_equivalence/5, check_exclusive_access/2, with_exclusive_access_or_die/3, stat/1, deliver/2, deliver/3, requeue/4, ack/4, reject/5]). +-export([not_found/1, absent/2]). -export([list/0, list/1, info_keys/0, info/1, info/2, info_all/1, info_all/2, emit_info_all/5, list_local/1, info_local/1, - emit_info_local/4, emit_info_down/4]). --export([list_down/1, count/1, list_names/0, list_names/1, list_local_names/0]). + emit_info_local/4, emit_info_down/4]). +-export([list_down/1, count/1, list_names/0, list_names/1, list_local_names/0, + list_with_possible_retry/1]). -export([list_by_type/1]). --export([notify_policy_changed/1]). +-export([force_event_refresh/1, notify_policy_changed/1]). -export([consumers/1, consumers_all/1, emit_consumers_all/4, consumer_info_keys/0]). -export([basic_get/6, basic_consume/12, basic_cancel/6, notify_decorators/1]). -export([notify_sent/2, notify_sent_queue_down/1, resume/2]). @@ -49,6 +51,8 @@ -export([pid_of/1, pid_of/2]). -export([mark_local_durable_queues_stopped/1]). +-deprecated([{force_event_refresh, 1, eventually}]). + %% internal -export([internal_declare/2, internal_delete/2, run_backing_queue/3, set_ram_duration_target/2, set_maximum_since_use/2, @@ -56,6 +60,7 @@ -include_lib("rabbit_common/include/rabbit.hrl"). -include_lib("stdlib/include/qlc.hrl"). +-include("amqqueue.hrl"). -define(INTEGER_ARG_TYPES, [byte, short, signedint, long, unsignedbyte, unsignedshort, unsignedint]). @@ -71,154 +76,16 @@ -type name() :: rabbit_types:r('queue'). -type qpids() :: [pid()]. -type qlen() :: rabbit_types:ok(non_neg_integer()). --type qfun(A) :: fun ((rabbit_types:amqqueue()) -> A | no_return()). +-type qfun(A) :: fun ((amqqueue:amqqueue()) -> A | no_return()). -type qmsg() :: {name(), pid(), msg_id(), boolean(), rabbit_types:message()}. -type msg_id() :: non_neg_integer(). -type ok_or_errors() :: 'ok' | {'error', [{'error' | 'exit' | 'throw', any()}]}. --type absent_reason() :: 'nodedown' | 'crashed'. --type queue_or_absent() :: rabbit_types:amqqueue() | - {'absent', rabbit_types:amqqueue(),absent_reason()}. --type not_found_or_absent() :: - 'not_found' | {'absent', rabbit_types:amqqueue(), absent_reason()}. --spec recover(rabbit_types:vhost()) -> [rabbit_types:amqqueue()]. --spec stop(rabbit_types:vhost()) -> 'ok'. --spec start([rabbit_types:amqqueue()]) -> 'ok'. --spec declare - (name(), boolean(), boolean(), rabbit_framing:amqp_table(), - rabbit_types:maybe(pid()), rabbit_types:username()) -> - {'new' | 'existing' | 'absent' | 'owner_died', - rabbit_types:amqqueue()} | - {'new', rabbit_types:amqqueue(), rabbit_fifo_client:state()} | - rabbit_types:channel_exit(). --spec declare - (name(), boolean(), boolean(), rabbit_framing:amqp_table(), - rabbit_types:maybe(pid()), rabbit_types:username(), node()) -> - {'new' | 'existing' | 'owner_died', rabbit_types:amqqueue()} | - {'new', rabbit_types:amqqueue(), rabbit_fifo_client:state()} | - {'absent', rabbit_types:amqqueue(), absent_reason()} | - rabbit_types:channel_exit(). --spec internal_declare(rabbit_types:amqqueue(), boolean()) -> - queue_or_absent() | rabbit_misc:thunk(queue_or_absent()). --spec update - (name(), fun((rabbit_types:amqqueue()) -> rabbit_types:amqqueue())) -> - 'not_found' | rabbit_types:amqqueue(). --spec lookup - (name()) -> - rabbit_types:ok(rabbit_types:amqqueue()) | - rabbit_types:error('not_found'); - ([name()]) -> - [rabbit_types:amqqueue()]. --spec not_found_or_absent(name()) -> not_found_or_absent(). --spec with(name(), qfun(A)) -> - A | rabbit_types:error(not_found_or_absent()). --spec with(name(), qfun(A), fun((not_found_or_absent()) -> B)) -> A | B. --spec with_or_die(name(), qfun(A)) -> A | rabbit_types:channel_exit(). --spec assert_equivalence - (rabbit_types:amqqueue(), boolean(), boolean(), - rabbit_framing:amqp_table(), rabbit_types:maybe(pid())) -> - 'ok' | rabbit_types:channel_exit() | rabbit_types:connection_exit(). --spec check_exclusive_access(rabbit_types:amqqueue(), pid()) -> - 'ok' | rabbit_types:channel_exit(). --spec with_exclusive_access_or_die(name(), pid(), qfun(A)) -> - A | rabbit_types:channel_exit(). --spec list() -> [rabbit_types:amqqueue()]. --spec list(rabbit_types:vhost()) -> [rabbit_types:amqqueue()]. --spec list_names() -> [rabbit_amqqueue:name()]. --spec list_down(rabbit_types:vhost()) -> [rabbit_types:amqqueue()]. --spec list_by_type(atom()) -> [rabbit_types:amqqueue()]. --spec info_keys() -> rabbit_types:info_keys(). --spec info(rabbit_types:amqqueue()) -> rabbit_types:infos(). --spec info(rabbit_types:amqqueue(), rabbit_types:info_keys()) -> - rabbit_types:infos(). --spec info_all(rabbit_types:vhost()) -> [rabbit_types:infos()]. --spec info_all(rabbit_types:vhost(), rabbit_types:info_keys()) -> - [rabbit_types:infos()]. --spec notify_policy_changed(rabbit_types:amqqueue()) -> 'ok'. --spec consumers(rabbit_types:amqqueue()) -> - [{pid(), rabbit_types:ctag(), boolean(), non_neg_integer(), - rabbit_framing:amqp_table()}]. --spec consumer_info_keys() -> rabbit_types:info_keys(). --spec consumers_all(rabbit_types:vhost()) -> - [{name(), pid(), rabbit_types:ctag(), boolean(), - non_neg_integer(), rabbit_framing:amqp_table()}]. --spec stat(rabbit_types:amqqueue()) -> - {'ok', non_neg_integer(), non_neg_integer()}. --spec delete_immediately(qpids()) -> 'ok'. --spec delete_exclusive(qpids(), pid()) -> 'ok'. --spec delete - (rabbit_types:amqqueue(), 'false', 'false', rabbit_types:username()) -> - qlen(); - (rabbit_types:amqqueue(), 'true' , 'false', rabbit_types:username()) -> - qlen() | rabbit_types:error('in_use'); - (rabbit_types:amqqueue(), 'false', 'true', rabbit_types:username()) -> - qlen() | rabbit_types:error('not_empty'); - (rabbit_types:amqqueue(), 'true' , 'true', rabbit_types:username()) -> - qlen() | - rabbit_types:error('in_use') | - rabbit_types:error('not_empty'). --spec delete_crashed(rabbit_types:amqqueue()) -> 'ok'. --spec delete_crashed_internal(rabbit_types:amqqueue(), rabbit_types:username()) -> 'ok'. --spec purge(rabbit_types:amqqueue()) -> {ok, qlen()}. --spec forget_all_durable(node()) -> 'ok'. --spec deliver([rabbit_types:amqqueue()], rabbit_types:delivery(), #{Name :: atom() => rabbit_fifo_client:state()} | 'untracked') -> - {qpids(), #{Name :: atom() => rabbit_fifo_client:state()}}. --spec deliver([rabbit_types:amqqueue()], rabbit_types:delivery()) -> 'ok'. --spec requeue(pid(), [msg_id()], pid(), #{Name :: atom() => rabbit_fifo_client:state()}) -> 'ok'. --spec ack(pid(), [msg_id()], pid(), #{Name :: atom() => rabbit_fifo_client:state()}) -> 'ok'. --spec reject(pid() | {atom(), node()}, [msg_id()], boolean(), pid(), - #{Name :: atom() => rabbit_fifo_client:state()}) -> 'ok'. --spec notify_down_all(qpids(), pid()) -> ok_or_errors(). --spec notify_down_all(qpids(), pid(), non_neg_integer()) -> - ok_or_errors(). --spec activate_limit_all(qpids(), pid()) -> ok_or_errors(). --spec basic_get(rabbit_types:amqqueue(), pid(), boolean(), pid(), rabbit_types:ctag(), - #{Name :: atom() => rabbit_fifo_client:state()}) -> - {'ok', non_neg_integer(), qmsg()} | 'empty'. --spec credit - (rabbit_types:amqqueue(), pid(), rabbit_types:ctag(), non_neg_integer(), - boolean(), #{Name :: atom() => rabbit_fifo_client:state()}) -> - 'ok'. --spec basic_consume - (rabbit_types:amqqueue(), boolean(), pid(), pid(), boolean(), - non_neg_integer(), rabbit_types:ctag(), boolean(), - rabbit_framing:amqp_table(), any(), rabbit_types:username(), - #{Name :: atom() => rabbit_fifo_client:state()}) -> - rabbit_types:ok_or_error('exclusive_consume_unavailable'). --spec basic_cancel - (rabbit_types:amqqueue(), pid(), rabbit_types:ctag(), any(), - rabbit_types:username(), #{Name :: atom() => rabbit_fifo_client:state()}) -> - 'ok' | {'ok', #{Name :: atom() => rabbit_fifo_client:state()}}. --spec notify_decorators(rabbit_types:amqqueue()) -> 'ok'. --spec resume(pid(), pid()) -> 'ok'. --spec internal_delete(name(), rabbit_types:username()) -> - 'ok' | rabbit_types:connection_exit() | - fun (() -> - 'ok' | rabbit_types:connection_exit()). --spec run_backing_queue - (pid(), atom(), (fun ((atom(), A) -> {[rabbit_types:msg_id()], A}))) -> - 'ok'. --spec set_ram_duration_target(pid(), number() | 'infinity') -> 'ok'. --spec set_maximum_since_use(pid(), non_neg_integer()) -> 'ok'. --spec on_node_up(node()) -> 'ok'. --spec on_node_down(node()) -> 'ok'. --spec pseudo_queue(name(), pid()) -> rabbit_types:amqqueue(). --spec immutable(rabbit_types:amqqueue()) -> rabbit_types:amqqueue(). --spec store_queue(rabbit_types:amqqueue()) -> 'ok'. --spec update_decorators(name()) -> 'ok'. --spec policy_changed(rabbit_types:amqqueue(), rabbit_types:amqqueue()) -> - 'ok'. --spec update_mirroring(pid()) -> 'ok'. --spec sync_mirrors(rabbit_types:amqqueue() | pid()) -> - 'ok' | rabbit_types:error('not_mirrored'). --spec cancel_sync_mirrors(rabbit_types:amqqueue() | pid()) -> - 'ok' | {'ok', 'not_syncing'}. --spec is_replicated(rabbit_types:amqqueue()) -> boolean(). - --spec pid_of(rabbit_types:amqqueue()) -> - {'ok', pid()} | rabbit_types:error('not_found'). --spec pid_of(rabbit_types:vhost(), rabbit_misc:resource_name()) -> - {'ok', pid()} | rabbit_types:error('not_found'). +-type absent_reason() :: 'nodedown' | 'crashed' | stopped | timeout. +-type queue_not_found() :: not_found. +-type queue_absent() :: {'absent', amqqueue:amqqueue(), absent_reason()}. +-type not_found_or_absent() :: queue_not_found() | queue_absent(). +-type quorum_states() :: #{Name :: atom() => rabbit_fifo_client:state()}. %%---------------------------------------------------------------------------- @@ -241,6 +108,8 @@ warn_file_limit() -> ok end. +-spec recover(rabbit_types:vhost()) -> [amqqueue:amqqueue()]. + recover(VHost) -> Classic = find_local_durable_classic_queues(VHost), Quorum = find_local_quorum_queues(VHost), @@ -252,7 +121,7 @@ recover_classic_queues(VHost, Queues) -> %% order as the supplied queue names, so that we can zip them together %% for further processing in recover_durable_queues. {ok, OrderedRecoveryTerms} = - BQ:start(VHost, [QName || #amqqueue{name = QName} <- Queues]), + BQ:start(VHost, [amqqueue:get_name(Q) || Q <- Queues]), case rabbit_amqqueue_sup_sup:start_for_vhost(VHost) of {ok, _} -> recover_durable_queues(lists:zip(Queues, OrderedRecoveryTerms)); @@ -262,18 +131,21 @@ recover_classic_queues(VHost, Queues) -> end. filter_per_type(Queues) -> - lists:partition(fun(#amqqueue{type = Type}) -> Type == classic end, Queues). + lists:partition(fun(Q) -> amqqueue:is_classic(Q) end, Queues). filter_pid_per_type(QPids) -> lists:partition(fun(QPid) -> ?IS_CLASSIC(QPid) end, QPids). filter_resource_per_type(Resources) -> Queues = [begin - {ok, #amqqueue{pid = QPid}} = lookup(Resource), + {ok, Q} = lookup(Resource), + QPid = amqqueue:get_pid(Q), {Resource, QPid} end || Resource <- Resources], lists:partition(fun({_Resource, QPid}) -> ?IS_CLASSIC(QPid) end, Queues). +-spec stop(rabbit_types:vhost()) -> 'ok'. + stop(VHost) -> %% Classic queues ok = rabbit_amqqueue_sup_sup:stop_for_vhost(VHost), @@ -281,53 +153,55 @@ stop(VHost) -> ok = BQ:stop(VHost), rabbit_quorum_queue:stop(VHost). +-spec start([amqqueue:amqqueue()]) -> 'ok'. + start(Qs) -> {Classic, _Quorum} = filter_per_type(Qs), %% At this point all recovered queues and their bindings are %% visible to routing, so now it is safe for them to complete %% their initialisation (which may involve interacting with other %% queues). - _ = [Pid ! {self(), go} || #amqqueue{pid = Pid} <- Classic], + _ = [amqqueue:get_pid(Q) ! {self(), go} || Q <- Classic], ok. mark_local_durable_queues_stopped(VHost) -> + ?try_mnesia_tx_or_upgrade_amqqueue_and_retry( + do_mark_local_durable_queues_stopped(VHost), + do_mark_local_durable_queues_stopped(VHost)). + +do_mark_local_durable_queues_stopped(VHost) -> Qs = find_local_durable_classic_queues(VHost), rabbit_misc:execute_mnesia_transaction( fun() -> - [ store_queue(Q#amqqueue{ state = stopped }) - || Q = #amqqueue{ state = State } <- Qs, - State =/= stopped ] + [ store_queue(amqqueue:set_state(Q, stopped)) + || Q <- Qs, + amqqueue:get_state(Q) =/= stopped ] end). find_local_quorum_queues(VHost) -> Node = node(), mnesia:async_dirty( fun () -> - qlc:e(qlc:q([Q || Q = #amqqueue{vhost = VH, - type = quorum, - quorum_nodes = QuorumNodes} - <- mnesia:table(rabbit_durable_queue), - VH =:= VHost, - (lists:member(Node, QuorumNodes))])) + qlc:e(qlc:q([Q || Q <- mnesia:table(rabbit_durable_queue), + amqqueue:get_vhost(Q) =:= VHost, + amqqueue:is_quorum(Q) andalso + (lists:member(Node, amqqueue:get_quorum_nodes(Q)))])) end). find_local_durable_classic_queues(VHost) -> Node = node(), mnesia:async_dirty( fun () -> - qlc:e(qlc:q([Q || Q = #amqqueue{name = Name, - vhost = VH, - pid = Pid, - type = classic} - <- mnesia:table(rabbit_durable_queue), - VH =:= VHost, - (is_local_to_node(Pid, Node) andalso + qlc:e(qlc:q([Q || Q <- mnesia:table(rabbit_durable_queue), + amqqueue:get_vhost(Q) =:= VHost, + amqqueue:is_classic(Q) andalso + (is_local_to_node(amqqueue:get_pid(Q), Node) andalso %% Terminations on node down will not remove the rabbit_queue %% record if it is a mirrored queue (such info is now obtained from %% the policy). Thus, we must check if the local pid is alive %% - if the record is present - in order to restart. - (mnesia:read(rabbit_queue, Name, read) =:= [] - orelse not rabbit_mnesia:is_process_alive(Pid))) + (mnesia:read(rabbit_queue, amqqueue:get_name(Q), read) =:= [] + orelse not rabbit_mnesia:is_process_alive(amqqueue:get_pid(Q)))) ])) end). @@ -335,20 +209,16 @@ find_recoverable_queues() -> Node = node(), mnesia:async_dirty( fun () -> - qlc:e(qlc:q([Q || Q = #amqqueue{name = Name, - pid = Pid, - type = Type, - quorum_nodes = QuorumNodes} - <- mnesia:table(rabbit_durable_queue), - (Type == classic andalso - (is_local_to_node(Pid, Node) andalso + qlc:e(qlc:q([Q || Q <- mnesia:table(rabbit_durable_queue), + (amqqueue:is_classic(Q) andalso + (is_local_to_node(amqqueue:get_pid(Q), Node) andalso %% Terminations on node down will not remove the rabbit_queue %% record if it is a mirrored queue (such info is now obtained from %% the policy). Thus, we must check if the local pid is alive %% - if the record is present - in order to restart. - (mnesia:read(rabbit_queue, Name, read) =:= [] - orelse not rabbit_mnesia:is_process_alive(Pid)))) - orelse (Type == quorum andalso lists:member(Node, QuorumNodes)) + (mnesia:read(rabbit_queue, amqqueue:get_name(Q), read) =:= [] + orelse not rabbit_mnesia:is_process_alive(amqqueue:get_pid(Q))))) + orelse (amqqueue:is_quorum(Q) andalso lists:member(Node, amqqueue:get_quorum_nodes(Q))) ])) end). @@ -361,6 +231,15 @@ recover_durable_queues(QueuesAndRecoveryTerms) -> [Pid, Error]) || {Pid, Error} <- Failures], [Q || {_, {new, Q}} <- Results]. +-spec declare(name(), + boolean(), + boolean(), + rabbit_framing:amqp_table(), + rabbit_types:maybe(pid()), + rabbit_types:username()) -> + {'new' | 'existing' | 'absent' | 'owner_died', amqqueue:amqqueue()} | + rabbit_types:channel_exit(). + declare(QueueName, Durable, AutoDelete, Args, Owner, ActingUser) -> declare(QueueName, Durable, AutoDelete, Args, Owner, ActingUser, node()). @@ -368,36 +247,56 @@ declare(QueueName, Durable, AutoDelete, Args, Owner, ActingUser) -> %% The Node argument suggests where the queue (master if mirrored) %% should be. Note that in some cases (e.g. with "nodes" policy in %% effect) this might not be possible to satisfy. + +-spec declare(name(), + boolean(), + boolean(), + rabbit_framing:amqp_table(), + rabbit_types:maybe(pid()), + rabbit_types:username(), + node()) -> + {'new' | 'existing' | 'owner_died', amqqueue:amqqueue()} | + {'new', amqqueue:amqqueue(), rabbit_fifo_client:state()} | + {'absent', amqqueue:amqqueue(), absent_reason()} | + rabbit_types:channel_exit(). + declare(QueueName = #resource{virtual_host = VHost}, Durable, AutoDelete, Args, Owner, ActingUser, Node) -> ok = check_declare_arguments(QueueName, Args), Type = get_queue_type(Args), - Q = rabbit_queue_decorator:set( - rabbit_policy:set(#amqqueue{name = QueueName, - durable = Durable, - auto_delete = AutoDelete, - arguments = Args, - exclusive_owner = Owner, - pid = none, - slave_pids = [], - sync_slave_pids = [], - recoverable_slaves = [], - gm_pids = [], - state = live, - policy_version = 0, - slave_pids_pending_shutdown = [], - vhost = VHost, - options = #{user => ActingUser}, - type = Type})), - - case Type of - classic -> - declare_classic_queue(Q, Node); - quorum -> - rabbit_quorum_queue:declare(Q) + TypeIsAllowed = + Type =:= classic orelse + rabbit_feature_flags:is_enabled(quorum_queue), + case TypeIsAllowed of + true -> + Q0 = amqqueue:new(QueueName, + none, + Durable, + AutoDelete, + Owner, + Args, + VHost, + #{user => ActingUser}, + Type), + Q = rabbit_queue_decorator:set( + rabbit_policy:set(Q0)), + do_declare(Q, Node); + false -> + rabbit_misc:protocol_error( + internal_error, + "Cannot declare a queue '~s' of type '~s' on node '~s': " + "the 'quorum_queue' feature flag is disabled", + [rabbit_misc:rs(QueueName), Type, Node]) end. -declare_classic_queue(#amqqueue{name = QName, vhost = VHost} = Q, Node) -> +do_declare(Q, Node) when ?amqqueue_is_classic(Q) -> + declare_classic_queue(Q, Node); +do_declare(Q, _Node) when ?amqqueue_is_quorum(Q) -> + rabbit_quorum_queue:declare(Q). + +declare_classic_queue(Q, Node) -> + QName = amqqueue:get_name(Q), + VHost = amqqueue:get_vhost(Q), Node1 = case rabbit_queue_master_location_misc:get_location(Q) of {ok, Node0} -> Node0; {error, _} -> Node @@ -422,20 +321,32 @@ get_queue_type(Args) -> erlang:binary_to_existing_atom(V, utf8) end. -internal_declare(Q, true) -> +-spec internal_declare(amqqueue:amqqueue(), boolean()) -> + {created | existing, amqqueue:amqqueue()} | queue_absent(). + +internal_declare(Q, Recover) -> + ?try_mnesia_tx_or_upgrade_amqqueue_and_retry( + do_internal_declare(Q, Recover), + begin + Q1 = amqqueue:upgrade(Q), + do_internal_declare(Q1, Recover) + end). + +do_internal_declare(Q, true) -> rabbit_misc:execute_mnesia_tx_with_tail( fun () -> - ok = store_queue(Q#amqqueue{state = live}), + ok = store_queue(amqqueue:set_state(Q, live)), rabbit_misc:const({created, Q}) end); -internal_declare(Q = #amqqueue{name = QueueName}, false) -> +do_internal_declare(Q, false) -> + QueueName = amqqueue:get_name(Q), rabbit_misc:execute_mnesia_tx_with_tail( fun () -> case mnesia:wread({rabbit_queue, QueueName}) of [] -> case not_found_or_absent(QueueName) of not_found -> Q1 = rabbit_policy:set(Q), - Q2 = Q1#amqqueue{state = live}, + Q2 = amqqueue:set_state(Q1, live), ok = store_queue(Q2), fun () -> {created, Q2} end; {absent, _Q, _} = R -> rabbit_misc:const(R) @@ -445,9 +356,14 @@ internal_declare(Q = #amqqueue{name = QueueName}, false) -> end end). +-spec update + (name(), fun((amqqueue:amqqueue()) -> amqqueue:amqqueue())) -> + 'not_found' | amqqueue:amqqueue(). + update(Name, Fun) -> case mnesia:wread({rabbit_queue, Name}) of - [Q = #amqqueue{durable = Durable}] -> + [Q] -> + Durable = amqqueue:is_durable(Q), Q1 = Fun(Q), ok = mnesia:write(rabbit_queue, Q1, write), case Durable of @@ -462,25 +378,34 @@ update(Name, Fun) -> %% only really used for quorum queues to ensure the rabbit_queue record %% is initialised ensure_rabbit_queue_record_is_initialized(Q) -> + ?try_mnesia_tx_or_upgrade_amqqueue_and_retry( + do_ensure_rabbit_queue_record_is_initialized(Q), + begin + Q1 = amqqueue:upgrade(Q), + do_ensure_rabbit_queue_record_is_initialized(Q1) + end). + +do_ensure_rabbit_queue_record_is_initialized(Q) -> rabbit_misc:execute_mnesia_tx_with_tail( fun () -> ok = store_queue(Q), rabbit_misc:const({ok, Q}) end). -store_queue(Q = #amqqueue{durable = true}) -> - ok = mnesia:write(rabbit_durable_queue, - Q#amqqueue{slave_pids = [], - sync_slave_pids = [], - gm_pids = [], - decorators = undefined}, write), +-spec store_queue(amqqueue:amqqueue()) -> 'ok'. + +store_queue(Q) when ?amqqueue_is_durable(Q) -> + Q1 = amqqueue:reset_mirroring_and_decorators(Q), + ok = mnesia:write(rabbit_durable_queue, Q1, write), store_queue_ram(Q); -store_queue(Q = #amqqueue{durable = false}) -> +store_queue(Q) when not ?amqqueue_is_durable(Q) -> store_queue_ram(Q). store_queue_ram(Q) -> ok = mnesia:write(rabbit_queue, rabbit_queue_decorator:set(Q), write). +-spec update_decorators(name()) -> 'ok'. + update_decorators(Name) -> rabbit_misc:execute_mnesia_transaction( fun() -> @@ -491,8 +416,12 @@ update_decorators(Name) -> end end). -policy_changed(Q1 = #amqqueue{decorators = Decorators1}, - Q2 = #amqqueue{decorators = Decorators2}) -> +-spec policy_changed(amqqueue:amqqueue(), amqqueue:amqqueue()) -> + 'ok'. + +policy_changed(Q1, Q2) -> + Decorators1 = amqqueue:get_decorators(Q1), + Decorators2 = amqqueue:get_decorators(Q2), rabbit_mirror_queue_misc:update_mirrors(Q1, Q2), D1 = rabbit_queue_decorator:select(Decorators1), D2 = rabbit_queue_decorator:select(Decorators2), @@ -501,6 +430,13 @@ policy_changed(Q1 = #amqqueue{decorators = Decorators1}, %% mirroring-related has changed - the policy may have changed anyway. notify_policy_changed(Q1). +-spec lookup + (name()) -> + rabbit_types:ok(amqqueue:amqqueue()) | + rabbit_types:error('not_found'); + ([name()]) -> + [amqqueue:amqqueue()]. + lookup([]) -> []; %% optimisation lookup([Name]) -> ets:lookup(rabbit_queue, Name); %% optimisation lookup(Names) when is_list(Names) -> @@ -510,6 +446,8 @@ lookup(Names) when is_list(Names) -> lookup(Name) -> rabbit_misc:dirty_read({rabbit_queue, Name}). +-spec not_found_or_absent(name()) -> not_found_or_absent(). + not_found_or_absent(Name) -> %% NB: we assume that the caller has already performed a lookup on %% rabbit_queue and not found anything @@ -518,6 +456,8 @@ not_found_or_absent(Name) -> [Q] -> {absent, Q, nodedown} %% Q exists on stopped node end. +-spec not_found_or_absent_dirty(name()) -> not_found_or_absent(). + not_found_or_absent_dirty(Name) -> %% We should read from both tables inside a tx, to get a %% consistent view. But the chances of an inconsistency are small, @@ -527,25 +467,30 @@ not_found_or_absent_dirty(Name) -> {ok, Q} -> {absent, Q, nodedown} end. +-spec with(name(), + qfun(A), + fun((not_found_or_absent()) -> rabbit_types:channel_exit())) -> + A | rabbit_types:channel_exit(). + with(Name, F, E) -> with(Name, F, E, 2000). -with(Name, F, E, RetriesLeft) -> +with(#resource{} = Name, F, E, RetriesLeft) -> case lookup(Name) of - {ok, Q = #amqqueue{state = live}} when RetriesLeft =:= 0 -> + {ok, Q} when ?amqqueue_state_is(Q, live) andalso RetriesLeft =:= 0 -> %% Something bad happened to that queue, we are bailing out %% on processing current request. E({absent, Q, timeout}); - {ok, Q = #amqqueue{state = stopped}} when RetriesLeft =:= 0 -> + {ok, Q} when ?amqqueue_state_is(Q, stopped) andalso RetriesLeft =:= 0 -> %% The queue was stopped and not migrated E({absent, Q, stopped}); %% The queue process has crashed with unknown error - {ok, Q = #amqqueue{state = crashed}} -> + {ok, Q} when ?amqqueue_state_is(Q, crashed) -> E({absent, Q, crashed}); %% The queue process has been stopped by a supervisor. %% In that case a synchronised slave can take over %% so we should retry. - {ok, Q = #amqqueue{state = stopped}} -> + {ok, Q} when ?amqqueue_state_is(Q, stopped) -> %% The queue process was stopped by the supervisor rabbit_misc:with_exit_handler( fun () -> retry_wait(Q, F, E, RetriesLeft) end, @@ -553,7 +498,7 @@ with(Name, F, E, RetriesLeft) -> %% The queue is supposed to be active. %% The master node can go away or queue can be killed %% so we retry, waiting for a slave to take over. - {ok, Q = #amqqueue{state = live}} -> + {ok, Q} when ?amqqueue_state_is(Q, live) -> %% We check is_process_alive(QPid) in case we receive a %% nodedown (for example) in F() that has nothing to do %% with the QPid. F() should be written s.t. that this @@ -567,7 +512,16 @@ with(Name, F, E, RetriesLeft) -> E(not_found_or_absent_dirty(Name)) end. -retry_wait(Q = #amqqueue{pid = QPid, name = Name, state = QState}, F, E, RetriesLeft) -> +-spec retry_wait(amqqueue:amqqueue(), + qfun(A), + fun((not_found_or_absent()) -> rabbit_types:channel_exit()), + non_neg_integer()) -> + A | rabbit_types:channel_exit(). + +retry_wait(Q, F, E, RetriesLeft) -> + Name = amqqueue:get_name(Q), + QPid = amqqueue:get_pid(Q), + QState = amqqueue:get_state(Q), case {QState, is_replicated(Q)} of %% We don't want to repeat an operation if %% there are no slaves to migrate to @@ -589,40 +543,107 @@ retry_wait(Q = #amqqueue{pid = QPid, name = Name, state = QState}, F, E, Retries with(Name, F, E, RetriesLeft - 1) end. +-spec with(name(), qfun(A)) -> + A | rabbit_types:error(not_found_or_absent()). + with(Name, F) -> with(Name, F, fun (E) -> {error, E} end). +-spec with_or_die(name(), qfun(A)) -> A | rabbit_types:channel_exit(). + with_or_die(Name, F) -> - with(Name, F, fun (not_found) -> rabbit_misc:not_found(Name); - ({absent, Q, Reason}) -> rabbit_misc:absent(Q, Reason) - end). - -assert_equivalence(#amqqueue{name = QName, - durable = Durable, - auto_delete = AD} = Q, - Durable1, AD1, Args1, Owner) -> + with(Name, F, die_fun(Name)). + +-spec die_fun(name()) -> + fun((not_found_or_absent()) -> rabbit_types:channel_exit()). + +die_fun(Name) -> + fun (not_found) -> not_found(Name); + ({absent, Q, Reason}) -> absent(Q, Reason) + end. + +-spec not_found(name()) -> rabbit_types:channel_exit(). + +not_found(R) -> rabbit_misc:protocol_error(not_found, "no ~s", [rabbit_misc:rs(R)]). + +-spec absent(amqqueue:amqqueue(), absent_reason()) -> + rabbit_types:channel_exit(). + +absent(Q, AbsentReason) -> + QueueName = amqqueue:get_name(Q), + QPid = amqqueue:get_pid(Q), + IsDurable = amqqueue:is_durable(Q), + priv_absent(QueueName, QPid, IsDurable, AbsentReason). + +-spec priv_absent(name(), pid(), boolean(), absent_reason()) -> + rabbit_types:channel_exit(). + +priv_absent(QueueName, QPid, true, nodedown) -> + %% The assertion of durability is mainly there because we mention + %% durability in the error message. That way we will hopefully + %% notice if at some future point our logic changes s.t. we get + %% here with non-durable queues. + rabbit_misc:protocol_error( + not_found, + "home node '~s' of durable ~s is down or inaccessible", + [node(QPid), rabbit_misc:rs(QueueName)]); + +priv_absent(QueueName, _QPid, _IsDurable, stopped) -> + rabbit_misc:protocol_error( + not_found, + "~s process is stopped by supervisor", [rabbit_misc:rs(QueueName)]); + +priv_absent(QueueName, _QPid, _IsDurable, crashed) -> + rabbit_misc:protocol_error( + not_found, + "~s has crashed and failed to restart", [rabbit_misc:rs(QueueName)]); + +priv_absent(QueueName, _QPid, _IsDurable, timeout) -> + rabbit_misc:protocol_error( + not_found, + "failed to perform operation on ~s due to timeout", [rabbit_misc:rs(QueueName)]). + +-spec assert_equivalence + (amqqueue:amqqueue(), boolean(), boolean(), + rabbit_framing:amqp_table(), rabbit_types:maybe(pid())) -> + 'ok' | rabbit_types:channel_exit() | rabbit_types:connection_exit(). + +assert_equivalence(Q, Durable1, AD1, Args1, Owner) -> + QName = amqqueue:get_name(Q), + Durable = amqqueue:is_durable(Q), + AD = amqqueue:is_auto_delete(Q), rabbit_misc:assert_field_equivalence(Durable, Durable1, QName, durable), rabbit_misc:assert_field_equivalence(AD, AD1, QName, auto_delete), assert_args_equivalence(Q, Args1), check_exclusive_access(Q, Owner, strict). +-spec check_exclusive_access(amqqueue:amqqueue(), pid()) -> + 'ok' | rabbit_types:channel_exit(). + check_exclusive_access(Q, Owner) -> check_exclusive_access(Q, Owner, lax). -check_exclusive_access(#amqqueue{exclusive_owner = Owner}, Owner, _MatchType) -> +check_exclusive_access(Q, Owner, _MatchType) + when ?amqqueue_exclusive_owner_is(Q, Owner) -> ok; -check_exclusive_access(#amqqueue{exclusive_owner = none}, _ReaderPid, lax) -> +check_exclusive_access(Q, _ReaderPid, lax) + when ?amqqueue_exclusive_owner_is(Q, none) -> ok; -check_exclusive_access(#amqqueue{name = QueueName}, _ReaderPid, _MatchType) -> +check_exclusive_access(Q, _ReaderPid, _MatchType) -> + QueueName = amqqueue:get_name(Q), rabbit_misc:protocol_error( resource_locked, "cannot obtain exclusive access to locked ~s", [rabbit_misc:rs(QueueName)]). +-spec with_exclusive_access_or_die(name(), pid(), qfun(A)) -> + A | rabbit_types:channel_exit(). + with_exclusive_access_or_die(Name, ReaderPid, F) -> with_or_die(Name, fun (Q) -> check_exclusive_access(Q, ReaderPid), F(Q) end). -assert_args_equivalence(#amqqueue{name = QueueName, arguments = Args}, - RequiredArgs) -> +assert_args_equivalence(Q, RequiredArgs) -> + QueueName = amqqueue:get_name(Q), + Args = amqqueue:get_arguments(Q), rabbit_misc:assert_args_equivalence(Args, RequiredArgs, QueueName, [Key || {Key, _Fun} <- declare_args()]). @@ -749,64 +770,109 @@ check_queue_type({longstr, Val}, _Args) -> check_queue_type({Type, _}, _Args) -> {error, {unacceptable_type, Type}}. +-spec list() -> [amqqueue:amqqueue()]. + +list() -> + list_with_possible_retry(fun do_list/0). -list() -> mnesia:dirty_match_object(rabbit_queue, #amqqueue{_ = '_'}). +do_list() -> + mnesia:dirty_match_object(rabbit_queue, amqqueue:pattern_match_all()). + +-spec list_names() -> [rabbit_amqqueue:name()]. list_names() -> mnesia:dirty_all_keys(rabbit_queue). -list_names(VHost) -> [Q#amqqueue.name || Q <- list(VHost)]. +list_names(VHost) -> [amqqueue:get_name(Q) || Q <- list(VHost)]. list_local_names() -> - [ Q#amqqueue.name || #amqqueue{state = State, pid = QPid} = Q <- list(), - State =/= crashed, is_local_to_node(QPid, node())]. + [ amqqueue:get_name(Q) || Q <- list(), + amqqueue:get_state(Q) =/= crashed, is_local_to_node(amqqueue:get_pid(Q), node())]. + +-spec list_by_type(atom()) -> [amqqueue:amqqueue()]. list_by_type(Type) -> {atomic, Qs} = mnesia:sync_transaction( fun () -> mnesia:match_object(rabbit_durable_queue, - #amqqueue{_ = '_', type = Type}, read) + amqqueue:pattern_match_on_type(Type), + read) end), Qs. list_local_followers() -> - [ Q#amqqueue.name - || #amqqueue{state = State, type = quorum, pid = {_, Leader}, - quorum_nodes = Nodes} = Q <- list(), - State =/= crashed, Leader =/= node(), lists:member(node(), Nodes)]. + [ amqqueue:get_name(Q) + || Q <- list(), + amqqueue:is_quorum(Q), + amqqueue:get_state(Q) =/= crashed, amqqueue:get_leader(Q) =/= node(), lists:member(node(), amqqueue:get_quorum_nodes(Q))]. is_local_to_node(QPid, Node) when ?IS_CLASSIC(QPid) -> Node =:= node(QPid); is_local_to_node({_, Leader} = QPid, Node) when ?IS_QUORUM(QPid) -> Node =:= Leader. -qnode(QPid) when ?IS_CLASSIC(QPid) -> - node(QPid); -qnode({_, Node} = QPid) when ?IS_QUORUM(QPid) -> - Node. +-spec list(rabbit_types:vhost()) -> [amqqueue:amqqueue()]. list(VHostPath) -> list(VHostPath, rabbit_queue). +list(VHostPath, TableName) -> + list_with_possible_retry(fun() -> do_list(VHostPath, TableName) end). + %% Not dirty_match_object since that would not be transactional when used in a %% tx context -list(VHostPath, TableName) -> +do_list(VHostPath, TableName) -> mnesia:async_dirty( fun () -> mnesia:match_object( TableName, - #amqqueue{name = rabbit_misc:r(VHostPath, queue), _ = '_'}, + amqqueue:pattern_match_on_name(rabbit_misc:r(VHostPath, queue)), read) end). +list_with_possible_retry(Fun) -> + %% amqqueue migration: + %% The `rabbit_queue` or `rabbit_durable_queue` tables + %% might be migrated between the time we query the pattern + %% (with the `amqqueue` module) and the time we call + %% `mnesia:dirty_match_object()`. This would lead to an empty list + %% (no object matching the now incorrect pattern), not a Mnesia + %% error. + %% + %% So if the result is an empty list and the version of the + %% `amqqueue` record changed in between, we retry the operation. + %% + %% However, we don't do this if inside a Mnesia transaction: we + %% could end up with a live lock between this started transaction + %% and the Mnesia table migration which is blocked (but the + %% rabbit_feature_flags lock is held). + AmqqueueRecordVersion = amqqueue:record_version_to_use(), + case Fun() of + [] -> + case mnesia:is_transaction() of + true -> + []; + false -> + case amqqueue:record_version_to_use() of + AmqqueueRecordVersion -> []; + _ -> Fun() + end + end; + Ret -> + Ret + end. + +-spec list_down(rabbit_types:vhost()) -> [amqqueue:amqqueue()]. + list_down(VHostPath) -> case rabbit_vhost:exists(VHostPath) of false -> []; true -> Present = list(VHostPath), Durable = list(VHostPath, rabbit_durable_queue), - PresentS = sets:from_list([N || #amqqueue{name = N} <- Present]), - sets:to_list(sets:filter(fun (#amqqueue{name = N}) -> + PresentS = sets:from_list([amqqueue:get_name(Q) || Q <- Present]), + sets:to_list(sets:filter(fun (Q) -> + N = amqqueue:get_name(Q), not sets:is_element(N, PresentS) end, sets:from_list(Durable))) end. @@ -818,20 +884,31 @@ count(VHost) -> %% won't work here because with master migration of mirrored queues %% the "ownership" of queues by nodes becomes a non-trivial problem %% that requires a proper consensus algorithm. - length(mnesia:dirty_index_read(rabbit_queue, VHost, #amqqueue.vhost)) + length(list_for_count(VHost)) catch _:Err -> rabbit_log:error("Failed to fetch number of queues in vhost ~p:~n~p~n", [VHost, Err]), 0 end. +list_for_count(VHost) -> + list_with_possible_retry( + fun() -> + mnesia:dirty_index_read(rabbit_queue, + VHost, + amqqueue:field_vhost()) + end). + +-spec info_keys() -> rabbit_types:info_keys(). + info_keys() -> rabbit_amqqueue_process:info_keys(). map(Qs, F) -> rabbit_misc:filter_exit_map(F, Qs). -is_unresponsive(#amqqueue{ state = crashed }, _Timeout) -> +is_unresponsive(Q, _Timeout) when ?amqqueue_state_is(Q, crashed) -> false; -is_unresponsive(#amqqueue{ pid = QPid }, Timeout) -> +is_unresponsive(Q, Timeout) -> + QPid = amqqueue:get_pid(Q), try delegate:invoke(QPid, {gen_server2, call, [{info, [name]}, Timeout]}), false @@ -841,21 +918,29 @@ is_unresponsive(#amqqueue{ pid = QPid }, Timeout) -> true end. -format(Q = #amqqueue{ type = quorum }) -> rabbit_quorum_queue:format(Q); +format(Q) when ?amqqueue_is_quorum(Q) -> rabbit_quorum_queue:format(Q); format(_) -> []. -info(Q = #amqqueue{ type = quorum }) -> rabbit_quorum_queue:info(Q); -info(Q = #amqqueue{ state = crashed }) -> info_down(Q, crashed); -info(Q = #amqqueue{ state = stopped }) -> info_down(Q, stopped); -info(#amqqueue{ pid = QPid }) -> delegate:invoke(QPid, {gen_server2, call, [info, infinity]}). +-spec info(amqqueue:amqqueue()) -> rabbit_types:infos(). + +info(Q) when ?amqqueue_is_quorum(Q) -> rabbit_quorum_queue:info(Q); +info(Q) when ?amqqueue_state_is(Q, crashed) -> info_down(Q, crashed); +info(Q) when ?amqqueue_state_is(Q, stopped) -> info_down(Q, stopped); +info(Q) -> + QPid = amqqueue:get_pid(Q), + delegate:invoke(QPid, {gen_server2, call, [info, infinity]}). -info(Q = #amqqueue{ type = quorum }, Items) -> +-spec info(amqqueue:amqqueue(), rabbit_types:info_keys()) -> + rabbit_types:infos(). + +info(Q, Items) when ?amqqueue_is_quorum(Q) -> rabbit_quorum_queue:info(Q, Items); -info(Q = #amqqueue{ state = crashed }, Items) -> +info(Q, Items) when ?amqqueue_state_is(Q, crashed) -> info_down(Q, Items, crashed); -info(Q = #amqqueue{ state = stopped }, Items) -> +info(Q, Items) when ?amqqueue_state_is(Q, stopped) -> info_down(Q, Items, stopped); -info(#amqqueue{ pid = QPid }, Items) -> +info(Q, Items) -> + QPid = amqqueue:get_pid(Q), case delegate:invoke(QPid, {gen_server2, call, [{info, Items}, infinity]}) of {ok, Res} -> Res; {error, Error} -> throw(Error) @@ -867,23 +952,28 @@ info_down(Q, DownReason) -> info_down(Q, Items, DownReason) -> [{Item, i_down(Item, Q, DownReason)} || Item <- Items]. -i_down(name, #amqqueue{name = Name}, _) -> Name; -i_down(durable, #amqqueue{durable = Dur}, _) -> Dur; -i_down(auto_delete, #amqqueue{auto_delete = AD}, _) -> AD; -i_down(arguments, #amqqueue{arguments = Args}, _) -> Args; -i_down(pid, #amqqueue{pid = QPid}, _) -> QPid; -i_down(recoverable_slaves, #amqqueue{recoverable_slaves = RS}, _) -> RS; -i_down(state, _Q, DownReason) -> DownReason; +i_down(name, Q, _) -> amqqueue:get_name(Q); +i_down(durable, Q, _) -> amqqueue:is_durable(Q); +i_down(auto_delete, Q, _) -> amqqueue:is_auto_delete(Q); +i_down(arguments, Q, _) -> amqqueue:get_arguments(Q); +i_down(pid, Q, _) -> amqqueue:get_pid(Q); +i_down(recoverable_slaves, Q, _) -> amqqueue:get_recoverable_slaves(Q); +i_down(state, _Q, DownReason) -> DownReason; i_down(K, _Q, _DownReason) -> case lists:member(K, rabbit_amqqueue_process:info_keys()) of true -> ''; false -> throw({bad_argument, K}) end. +-spec info_all(rabbit_types:vhost()) -> [rabbit_types:infos()]. + info_all(VHostPath) -> map(list(VHostPath), fun (Q) -> info(Q) end) ++ map(list_down(VHostPath), fun (Q) -> info_down(Q, down) end). +-spec info_all(rabbit_types:vhost(), rabbit_types:info_keys()) -> + [rabbit_types:infos()]. + info_all(VHostPath, Items) -> map(list(VHostPath), fun (Q) -> info(Q, Items) end) ++ map(list_down(VHostPath), fun (Q) -> info_down(Q, Items, down) end). @@ -919,24 +1009,47 @@ info_local(VHostPath) -> map(list_local(VHostPath), fun (Q) -> info(Q, [name]) end). list_local(VHostPath) -> - [ Q || #amqqueue{state = State, pid = QPid} = Q <- list(VHostPath), - State =/= crashed, is_local_to_node(QPid, node()) ]. + [Q || Q <- list(VHostPath), + amqqueue:get_state(Q) =/= crashed, is_local_to_node(amqqueue:get_pid(Q), node())]. + +-spec force_event_refresh(reference()) -> 'ok'. + +force_event_refresh(Ref) -> + [gen_server2:cast(amqqueue:get_pid(Q), + {force_event_refresh, Ref}) || Q <- list()], + ok. + +-spec notify_policy_changed(amqqueue:amqqueue()) -> 'ok'. -notify_policy_changed(#amqqueue{pid = QPid}) when ?IS_CLASSIC(QPid) -> +notify_policy_changed(Q) when ?amqqueue_is_classic(Q) -> + QPid = amqqueue:get_pid(Q), gen_server2:cast(QPid, policy_changed); -notify_policy_changed(#amqqueue{pid = QPid, - name = QName}) when ?IS_QUORUM(QPid) -> +notify_policy_changed(Q) when ?amqqueue_is_quorum(Q) -> + QPid = amqqueue:get_pid(Q), + QName = amqqueue:get_name(Q), rabbit_quorum_queue:policy_changed(QName, QPid). -consumers(#amqqueue{pid = QPid}) when ?IS_CLASSIC(QPid) -> +-spec consumers(amqqueue:amqqueue()) -> + [{pid(), rabbit_types:ctag(), boolean(), non_neg_integer(), + rabbit_framing:amqp_table(), rabbit_types:username()}]. + +consumers(Q) when ?amqqueue_is_classic(Q) -> + QPid = amqqueue:get_pid(Q), delegate:invoke(QPid, {gen_server2, call, [consumers, infinity]}); -consumers(#amqqueue{pid = QPid}) when ?IS_QUORUM(QPid) -> +consumers(Q) when ?amqqueue_is_quorum(Q) -> + QPid = amqqueue:get_pid(Q), {ok, {_, Result}, _} = ra:local_query(QPid, fun rabbit_fifo:query_consumers/1), maps:values(Result). +-spec consumer_info_keys() -> rabbit_types:info_keys(). + consumer_info_keys() -> ?CONSUMER_INFO_KEYS. +-spec consumers_all(rabbit_types:vhost()) -> + [{name(), pid(), rabbit_types:ctag(), boolean(), + non_neg_integer(), rabbit_framing:amqp_table()}]. + consumers_all(VHostPath) -> ConsumerInfoKeys = consumer_info_keys(), lists:append( @@ -957,24 +1070,38 @@ emit_consumers_local(VHostPath, Ref, AggregatorPid) -> get_queue_consumer_info(Q, ConsumerInfoKeys) -> [lists:zip(ConsumerInfoKeys, - [Q#amqqueue.name, ChPid, CTag, + [amqqueue:get_name(Q), ChPid, CTag, AckRequired, Prefetch, Active, ActivityStatus, Args]) || {ChPid, CTag, AckRequired, Prefetch, Active, ActivityStatus, Args, _} <- consumers(Q)]. -stat(#amqqueue{type = quorum} = Q) -> rabbit_quorum_queue:stat(Q); -stat(#amqqueue{pid = QPid}) -> delegate:invoke(QPid, {gen_server2, call, [stat, infinity]}). +-spec stat(amqqueue:amqqueue()) -> + {'ok', non_neg_integer(), non_neg_integer()}. + +stat(Q) when ?amqqueue_is_quorum(Q) -> rabbit_quorum_queue:stat(Q); +stat(Q) -> delegate:invoke(amqqueue:get_pid(Q), {gen_server2, call, [stat, infinity]}). + +-spec pid_of(amqqueue:amqqueue()) -> + {'ok', pid()} | rabbit_types:error('not_found'). + +pid_of(Q) -> amqqueue:get_pid(Q). + +-spec pid_of(rabbit_types:vhost(), rabbit_misc:resource_name()) -> + {'ok', pid()} | rabbit_types:error('not_found'). -pid_of(#amqqueue{pid = Pid}) -> Pid. pid_of(VHost, QueueName) -> case lookup(rabbit_misc:r(VHost, queue, QueueName)) of {ok, Q} -> pid_of(Q); {error, not_found} = E -> E end. +-spec delete_exclusive(qpids(), pid()) -> 'ok'. + delete_exclusive(QPids, ConnId) -> [gen_server2:cast(QPid, {delete_exclusive, ConnId}) || QPid <- QPids], ok. +-spec delete_immediately(qpids()) -> 'ok'. + delete_immediately(QPids) -> {Classic, Quorum} = filter_pid_per_type(QPids), [gen_server2:cast(QPid, delete_immediately) || QPid <- Classic], @@ -990,15 +1117,28 @@ delete_immediately_by_resource(Resources) -> || {Resource, QPid} <- Quorum], ok. -delete(#amqqueue{ type = quorum} = Q, - IfUnused, IfEmpty, ActingUser) -> +-spec delete + (amqqueue:amqqueue(), 'false', 'false', rabbit_types:username()) -> + qlen(); + (amqqueue:amqqueue(), 'true' , 'false', rabbit_types:username()) -> + qlen() | rabbit_types:error('in_use'); + (amqqueue:amqqueue(), 'false', 'true', rabbit_types:username()) -> + qlen() | rabbit_types:error('not_empty'); + (amqqueue:amqqueue(), 'true' , 'true', rabbit_types:username()) -> + qlen() | + rabbit_types:error('in_use') | + rabbit_types:error('not_empty'). + +delete(Q, + IfUnused, IfEmpty, ActingUser) when ?amqqueue_is_quorum(Q) -> rabbit_quorum_queue:delete(Q, IfUnused, IfEmpty, ActingUser); delete(Q, IfUnused, IfEmpty, ActingUser) -> case wait_for_promoted_or_stopped(Q) of - {promoted, #amqqueue{pid = QPid}} -> + {promoted, Q1} -> + QPid = amqqueue:get_pid(Q1), delegate:invoke(QPid, {gen_server2, call, [{delete, IfUnused, IfEmpty, ActingUser}, infinity]}); {stopped, Q1} -> - #resource{name = Name, virtual_host = Vhost} = Q1#amqqueue.name, + #resource{name = Name, virtual_host = Vhost} = amqqueue:get_name(Q1), case IfEmpty of true -> rabbit_log:error("Queue ~s in vhost ~s has its master node down and " @@ -1020,10 +1160,16 @@ delete(Q, IfUnused, IfEmpty, ActingUser) -> {ok, 0} end. --spec wait_for_promoted_or_stopped(#amqqueue{}) -> {promoted, #amqqueue{}} | {stopped, #amqqueue{}} | {error, not_found}. -wait_for_promoted_or_stopped(#amqqueue{name = QName}) -> +-spec wait_for_promoted_or_stopped(amqqueue:amqqueue()) -> + {promoted, amqqueue:amqqueue()} | + {stopped, amqqueue:amqqueue()} | + {error, not_found}. +wait_for_promoted_or_stopped(Q0) -> + QName = amqqueue:get_name(Q0), case lookup(QName) of - {ok, Q = #amqqueue{pid = QPid, slave_pids = SPids}} -> + {ok, Q} -> + QPid = amqqueue:get_pid(Q), + SPids = amqqueue:get_slave_pids(Q), case rabbit_mnesia:is_process_alive(QPid) of true -> {promoted, Q}; false -> @@ -1043,22 +1189,36 @@ wait_for_promoted_or_stopped(#amqqueue{name = QName}) -> {error, not_found} end. +-spec delete_crashed(amqqueue:amqqueue()) -> 'ok'. + delete_crashed(Q) -> delete_crashed(Q, ?INTERNAL_USER). -delete_crashed(#amqqueue{ pid = QPid } = Q, ActingUser) -> - ok = rpc:call(qnode(QPid), ?MODULE, delete_crashed_internal, [Q, ActingUser]). +delete_crashed(Q, ActingUser) -> + ok = rpc:call(amqqueue:qnode(Q), ?MODULE, delete_crashed_internal, [Q, ActingUser]). -delete_crashed_internal(Q = #amqqueue{ name = QName }, ActingUser) -> +-spec delete_crashed_internal(amqqueue:amqqueue(), rabbit_types:username()) -> 'ok'. + +delete_crashed_internal(Q, ActingUser) -> + QName = amqqueue:get_name(Q), {ok, BQ} = application:get_env(rabbit, backing_queue_module), BQ:delete_crashed(Q), ok = internal_delete(QName, ActingUser). -purge(#amqqueue{ pid = QPid, type = classic}) -> +-spec purge(amqqueue:amqqueue()) -> {ok, qlen()}. + +purge(Q) when ?amqqueue_is_classic(Q) -> + QPid = amqqueue:get_pid(Q), delegate:invoke(QPid, {gen_server2, call, [purge, infinity]}); -purge(#amqqueue{ pid = NodeId, type = quorum}) -> +purge(Q) when ?amqqueue_is_quorum(Q) -> + NodeId = amqqueue:get_pid(Q), rabbit_quorum_queue:purge(NodeId). +-spec requeue(pid(), + {rabbit_fifo:consumer_tag(), [msg_id()]}, + pid(), + quorum_states()) -> + 'ok'. requeue(QPid, {_, MsgIds}, ChPid, QuorumStates) when ?IS_CLASSIC(QPid) -> ok = delegate:invoke(QPid, {gen_server2, call, [{requeue, MsgIds, ChPid}, infinity]}), @@ -1074,6 +1234,12 @@ requeue({Name, _} = QPid, {CTag, MsgIds}, _ChPid, QuorumStates) QuorumStates end. +-spec ack(pid(), + {rabbit_fifo:consumer_tag(), [msg_id()]}, + pid(), + quorum_states()) -> + quorum_states(). + ack(QPid, {_, MsgIds}, ChPid, QueueStates) when ?IS_CLASSIC(QPid) -> delegate:invoke_no_result(QPid, {gen_server2, cast, [{ack, MsgIds, ChPid}]}), QueueStates; @@ -1088,6 +1254,13 @@ ack({Name, _} = QPid, {CTag, MsgIds}, _ChPid, QuorumStates) QuorumStates end. +-spec reject(pid() | amqqueue:ra_server_id(), + boolean(), + {rabbit_fifo:consumer_tag(), [msg_id()]}, + pid(), + quorum_states()) -> + quorum_states(). + reject(QPid, Requeue, {_, MsgIds}, ChPid, QStates) when ?IS_CLASSIC(QPid) -> ok = delegate:invoke_no_result(QPid, {gen_server2, cast, [{reject, Requeue, MsgIds, ChPid}]}), @@ -1104,9 +1277,14 @@ reject({Name, _} = QPid, Requeue, {CTag, MsgIds}, _ChPid, QuorumStates) QuorumStates end. +-spec notify_down_all(qpids(), pid()) -> ok_or_errors(). + notify_down_all(QPids, ChPid) -> notify_down_all(QPids, ChPid, ?CHANNEL_OPERATION_TIMEOUT). +-spec notify_down_all(qpids(), pid(), non_neg_integer()) -> + ok_or_errors(). + notify_down_all(QPids, ChPid, Timeout) -> case rpc:call(node(), delegate, invoke, [QPids, {gen_server2, call, [{notify_down, ChPid}, infinity]}], Timeout) of @@ -1124,30 +1302,51 @@ notify_down_all(QPids, ChPid, Timeout) -> Error -> {error, Error} end. +-spec activate_limit_all(qpids(), pid()) -> ok. + activate_limit_all(QRefs, ChPid) -> QPids = [P || P <- QRefs, ?IS_CLASSIC(P)], delegate:invoke_no_result(QPids, {gen_server2, cast, [{activate_limit, ChPid}]}). -credit(#amqqueue{pid = QPid, type = classic}, ChPid, CTag, Credit, - Drain, QStates) -> +-spec credit(amqqueue:amqqueue(), + pid(), + rabbit_types:ctag(), + non_neg_integer(), + boolean(), + quorum_states()) -> + {'ok', quorum_states()}. + +credit(Q, ChPid, CTag, Credit, + Drain, QStates) when ?amqqueue_is_classic(Q) -> + QPid = amqqueue:get_pid(Q), delegate:invoke_no_result(QPid, {gen_server2, cast, [{credit, ChPid, CTag, Credit, Drain}]}), {ok, QStates}; -credit(#amqqueue{pid = {Name, _} = Id, name = QName, type = quorum}, +credit(Q, _ChPid, CTag, Credit, - Drain, QStates) -> + Drain, QStates) when ?amqqueue_is_quorum(Q) -> + {Name, _} = Id = amqqueue:get_pid(Q), + QName = amqqueue:get_name(Q), QState0 = get_quorum_state(Id, QName, QStates), {ok, QState} = rabbit_quorum_queue:credit(CTag, Credit, Drain, QState0), {ok, maps:put(Name, QState, QStates)}. +-spec basic_get(amqqueue:amqqueue(), pid(), boolean(), pid(), rabbit_types:ctag(), + #{Name :: atom() => rabbit_fifo_client:state()}) -> + {'ok', non_neg_integer(), qmsg(), quorum_states()} | + {'empty', quorum_states()} | + rabbit_types:channel_exit(). -basic_get(#amqqueue{pid = QPid, type = classic}, ChPid, NoAck, LimiterPid, - _CTag, _) -> +basic_get(Q, ChPid, NoAck, LimiterPid, _CTag, _) + when ?amqqueue_is_classic(Q) -> + QPid = amqqueue:get_pid(Q), delegate:invoke(QPid, {gen_server2, call, [{basic_get, ChPid, NoAck, LimiterPid}, infinity]}); -basic_get(#amqqueue{pid = {Name, _} = Id, type = quorum, name = QName} = Q, _ChPid, NoAck, - _LimiterPid, CTag, QStates) -> +basic_get(Q, _ChPid, NoAck, _LimiterPid, CTag, QStates) + when ?amqqueue_is_quorum(Q) -> + {Name, _} = Id = amqqueue:get_pid(Q), + QName = amqqueue:get_name(Q), QState0 = get_quorum_state(Id, QName, QStates), case rabbit_quorum_queue:basic_get(Q, NoAck, CTag, QState0) of {ok, empty, QState} -> @@ -1160,9 +1359,19 @@ basic_get(#amqqueue{pid = {Name, _} = Id, type = quorum, name = QName} = Q, _ChP [rabbit_misc:rs(QName), Reason]) end. -basic_consume(#amqqueue{pid = QPid, name = QName, type = classic}, NoAck, ChPid, - LimiterPid, LimiterActive, ConsumerPrefetchCount, ConsumerTag, - ExclusiveConsume, Args, OkMsg, ActingUser, QState) -> +-spec basic_consume + (amqqueue:amqqueue(), boolean(), pid(), pid(), boolean(), + non_neg_integer(), rabbit_types:ctag(), boolean(), + rabbit_framing:amqp_table(), any(), rabbit_types:username(), + #{Name :: atom() => rabbit_fifo_client:state()}) -> + rabbit_types:ok_or_error('exclusive_consume_unavailable'). + +basic_consume(Q, NoAck, ChPid, LimiterPid, + LimiterActive, ConsumerPrefetchCount, ConsumerTag, + ExclusiveConsume, Args, OkMsg, ActingUser, QState) + when ?amqqueue_is_classic(Q) -> + QPid = amqqueue:get_pid(Q), + QName = amqqueue:get_name(Q), ok = check_consume_arguments(QName, Args), case delegate:invoke(QPid, {gen_server2, call, @@ -1174,14 +1383,18 @@ basic_consume(#amqqueue{pid = QPid, name = QName, type = classic}, NoAck, ChPid, Err -> Err end; -basic_consume(#amqqueue{type = quorum}, _NoAck, _ChPid, +basic_consume(Q, _NoAck, _ChPid, _LimiterPid, true, _ConsumerPrefetchCount, _ConsumerTag, - _ExclusiveConsume, _Args, _OkMsg, _ActingUser, _QStates) -> + _ExclusiveConsume, _Args, _OkMsg, _ActingUser, _QStates) + when ?amqqueue_is_quorum(Q) -> {error, global_qos_not_supported_for_queue_type}; -basic_consume(#amqqueue{pid = {Name, _} = Id, name = QName, type = quorum} = Q, +basic_consume(Q, NoAck, ChPid, _LimiterPid, _LimiterActive, ConsumerPrefetchCount, ConsumerTag, ExclusiveConsume, Args, OkMsg, - ActingUser, QStates) -> + ActingUser, QStates) + when ?amqqueue_is_quorum(Q) -> + {Name, _} = Id = amqqueue:get_pid(Q), + QName = amqqueue:get_name(Q), ok = check_consume_arguments(QName, Args), QState0 = get_quorum_state(Id, QName, QStates), {ok, QState} = rabbit_quorum_queue:basic_consume(Q, NoAck, ChPid, @@ -1192,8 +1405,15 @@ basic_consume(#amqqueue{pid = {Name, _} = Id, name = QName, type = quorum} = Q, OkMsg, QState0), {ok, maps:put(Name, QState, QStates)}. -basic_cancel(#amqqueue{pid = QPid, type = classic}, ChPid, ConsumerTag, OkMsg, ActingUser, - QState) -> +-spec basic_cancel + (amqqueue:amqqueue(), pid(), rabbit_types:ctag(), any(), + rabbit_types:username(), #{Name :: atom() => rabbit_fifo_client:state()}) -> + 'ok' | {'ok', #{Name :: atom() => rabbit_fifo_client:state()}}. + +basic_cancel(Q, ChPid, ConsumerTag, OkMsg, ActingUser, + QState) + when ?amqqueue_is_classic(Q) -> + QPid = amqqueue:get_pid(Q), case delegate:invoke(QPid, {gen_server2, call, [{basic_cancel, ChPid, ConsumerTag, OkMsg, ActingUser}, infinity]}) of @@ -1201,13 +1421,18 @@ basic_cancel(#amqqueue{pid = QPid, type = classic}, ChPid, ConsumerTag, OkMsg, A {ok, QState}; Err -> Err end; -basic_cancel(#amqqueue{pid = {Name, _} = Id, type = quorum}, ChPid, - ConsumerTag, OkMsg, _ActingUser, QStates) -> +basic_cancel(Q, ChPid, + ConsumerTag, OkMsg, _ActingUser, QStates) + when ?amqqueue_is_quorum(Q) -> + {Name, _} = Id = amqqueue:get_pid(Q), QState0 = get_quorum_state(Id, QStates), {ok, QState} = rabbit_quorum_queue:basic_cancel(ConsumerTag, ChPid, OkMsg, QState0), {ok, maps:put(Name, QState, QStates)}. -notify_decorators(#amqqueue{pid = QPid}) -> +-spec notify_decorators(amqqueue:amqqueue()) -> 'ok'. + +notify_decorators(Q) -> + QPid = amqqueue:get_pid(Q), delegate:invoke_no_result(QPid, {gen_server2, cast, [notify_decorators]}). notify_sent(QPid, ChPid) -> @@ -1216,6 +1441,8 @@ notify_sent(QPid, ChPid) -> notify_sent_queue_down(QPid) -> rabbit_amqqueue_common:notify_sent_queue_down(QPid). +-spec resume(pid(), pid()) -> 'ok'. + resume(QPid, ChPid) -> delegate:invoke_no_result(QPid, {gen_server2, cast, [{resume, ChPid}]}). internal_delete1(QueueName, OnlyDurable) -> @@ -1236,6 +1463,8 @@ internal_delete1(QueueName, OnlyDurable, Reason) -> %% after the transaction. rabbit_binding:remove_for_destination(QueueName, OnlyDurable). +-spec internal_delete(name(), rabbit_types:username()) -> 'ok'. + internal_delete(QueueName, ActingUser) -> internal_delete(QueueName, ActingUser, normal). @@ -1260,6 +1489,8 @@ internal_delete(QueueName, ActingUser, Reason) -> end end). +-spec forget_all_durable(node()) -> 'ok'. + forget_all_durable(Node) -> %% Note rabbit is not running so we avoid e.g. the worker pool. Also why %% we don't invoke the return from rabbit_binding:process_deletions/1. @@ -1267,10 +1498,10 @@ forget_all_durable(Node) -> mnesia:sync_transaction( fun () -> Qs = mnesia:match_object(rabbit_durable_queue, - #amqqueue{_ = '_'}, write), + amqqueue:pattern_match_all(), write), [forget_node_for_queue(Node, Q) || - #amqqueue{pid = Pid} = Q <- Qs, - is_local_to_node(Pid, Node)], + Q <- Qs, + is_local_to_node(amqqueue:get_pid(Q), Node)], ok end), ok. @@ -1278,26 +1509,30 @@ forget_all_durable(Node) -> %% Try to promote a slave while down - it should recover as a %% master. We try to take the oldest slave here for best chance of %% recovery. -forget_node_for_queue(DeadNode, Q = #amqqueue{type = quorum, - quorum_nodes = QN}) -> +forget_node_for_queue(DeadNode, Q) + when ?amqqueue_is_quorum(Q) -> + QN = amqqueue:get_quorum_nodes(Q), forget_node_for_queue(DeadNode, QN, Q); -forget_node_for_queue(DeadNode, Q = #amqqueue{recoverable_slaves = RS}) -> +forget_node_for_queue(DeadNode, Q) -> + RS = amqqueue:get_recoverable_slaves(Q), forget_node_for_queue(DeadNode, RS, Q). -forget_node_for_queue(_DeadNode, [], #amqqueue{name = Name}) -> +forget_node_for_queue(_DeadNode, [], Q) -> %% No slaves to recover from, queue is gone. %% Don't process_deletions since that just calls callbacks and we %% are not really up. + Name = amqqueue:get_name(Q), internal_delete1(Name, true); %% Should not happen, but let's be conservative. forget_node_for_queue(DeadNode, [DeadNode | T], Q) -> forget_node_for_queue(DeadNode, T, Q); -forget_node_for_queue(DeadNode, [H|T], #amqqueue{type = Type} = Q) -> +forget_node_for_queue(DeadNode, [H|T], Q) when ?is_amqqueue(Q) -> + Type = amqqueue:get_type(Q), case {node_permits_offline_promotion(H), Type} of {false, _} -> forget_node_for_queue(DeadNode, T, Q); - {true, classic} -> Q1 = Q#amqqueue{pid = rabbit_misc:node_to_fake_pid(H)}, + {true, classic} -> Q1 = amqqueue:set_pid(Q, rabbit_misc:node_to_fake_pid(H)), ok = mnesia:write(rabbit_durable_queue, Q1, write); {true, quorum} -> ok end. @@ -1317,49 +1552,73 @@ node_permits_offline_promotion(Node) -> %% %% [2] This is simpler; as long as it's down that's OK +-spec run_backing_queue + (pid(), atom(), (fun ((atom(), A) -> {[rabbit_types:msg_id()], A}))) -> + 'ok'. + run_backing_queue(QPid, Mod, Fun) -> gen_server2:cast(QPid, {run_backing_queue, Mod, Fun}). +-spec set_ram_duration_target(pid(), number() | 'infinity') -> 'ok'. + set_ram_duration_target(QPid, Duration) -> gen_server2:cast(QPid, {set_ram_duration_target, Duration}). +-spec set_maximum_since_use(pid(), non_neg_integer()) -> 'ok'. + set_maximum_since_use(QPid, Age) -> gen_server2:cast(QPid, {set_maximum_since_use, Age}). +-spec update_mirroring(pid()) -> 'ok'. + update_mirroring(QPid) -> ok = delegate:invoke_no_result(QPid, {gen_server2, cast, [update_mirroring]}). -sync_mirrors(#amqqueue{pid = QPid}) -> +-spec sync_mirrors(amqqueue:amqqueue() | pid()) -> + 'ok' | rabbit_types:error('not_mirrored'). + +sync_mirrors(Q) when ?is_amqqueue(Q) -> + QPid = amqqueue:get_pid(Q), delegate:invoke(QPid, {gen_server2, call, [sync_mirrors, infinity]}); sync_mirrors(QPid) -> delegate:invoke(QPid, {gen_server2, call, [sync_mirrors, infinity]}). -cancel_sync_mirrors(#amqqueue{pid = QPid}) -> + +-spec cancel_sync_mirrors(amqqueue:amqqueue() | pid()) -> + 'ok' | {'ok', 'not_syncing'}. + +cancel_sync_mirrors(Q) when ?is_amqqueue(Q) -> + QPid = amqqueue:get_pid(Q), delegate:invoke(QPid, {gen_server2, call, [cancel_sync_mirrors, infinity]}); cancel_sync_mirrors(QPid) -> delegate:invoke(QPid, {gen_server2, call, [cancel_sync_mirrors, infinity]}). -is_replicated(#amqqueue{type = quorum}) -> +-spec is_replicated(amqqueue:amqqueue()) -> boolean(). + +is_replicated(Q) when ?amqqueue_is_quorum(Q) -> true; is_replicated(Q) -> rabbit_mirror_queue_misc:is_mirrored(Q). -is_dead_exclusive(#amqqueue{exclusive_owner = none}) -> +is_dead_exclusive(Q) when ?amqqueue_exclusive_owner_is(Q, none) -> false; -is_dead_exclusive(#amqqueue{exclusive_owner = Pid}) when is_pid(Pid) -> +is_dead_exclusive(Q) when ?amqqueue_exclusive_owner_is_pid(Q) -> + Pid = amqqueue:get_pid(Q), not rabbit_mnesia:is_process_alive(Pid). +-spec on_node_up(node()) -> 'ok'. + on_node_up(Node) -> ok = rabbit_misc:execute_mnesia_transaction( fun () -> Qs = mnesia:match_object(rabbit_queue, - #amqqueue{_ = '_'}, write), + amqqueue:pattern_match_all(), write), [maybe_clear_recoverable_node(Node, Q) || Q <- Qs], ok end). -maybe_clear_recoverable_node(Node, - #amqqueue{sync_slave_pids = SPids, - recoverable_slaves = RSs} = Q) -> +maybe_clear_recoverable_node(Node, Q) -> + SPids = amqqueue:get_sync_slave_pids(Q), + RSs = amqqueue:get_recoverable_slaves(Q), case lists:member(Node, RSs) of true -> %% There is a race with @@ -1381,13 +1640,15 @@ maybe_clear_recoverable_node(Node, if DoClearNode -> RSs1 = RSs -- [Node], store_queue( - Q#amqqueue{recoverable_slaves = RSs1}); + amqqueue:set_recoverable_slaves(Q, RSs1)); true -> ok end; false -> ok end. +-spec on_node_down(node()) -> 'ok'. + on_node_down(Node) -> {QueueNames, QueueDeletions} = delete_queues_on_node_down(Node), notify_queue_binding_deletions(QueueDeletions), @@ -1421,10 +1682,10 @@ partition_queues(T) -> queues_to_delete_when_node_down(NodeDown) -> rabbit_misc:execute_mnesia_transaction(fun () -> - qlc:e(qlc:q([QName || - #amqqueue{name = QName, pid = Pid} = Q <- mnesia:table(rabbit_queue), - qnode(Pid) == NodeDown andalso - not rabbit_mnesia:is_process_alive(Pid) andalso + qlc:e(qlc:q([amqqueue:get_name(Q) || + Q <- mnesia:table(rabbit_queue), + amqqueue:qnode(Q) == NodeDown andalso + not rabbit_mnesia:is_process_alive(amqqueue:get_pid(Q)) andalso (not rabbit_amqqueue:is_replicated(Q) orelse rabbit_amqqueue:is_dead_exclusive(Q))] )) @@ -1453,27 +1714,44 @@ notify_queues_deleted(QueueDeletions) -> end, QueueDeletions). +-spec pseudo_queue(name(), pid()) -> amqqueue:amqqueue(). + pseudo_queue(QueueName, Pid) -> - #amqqueue{name = QueueName, - durable = false, - auto_delete = false, - arguments = [], - pid = Pid, - slave_pids = []}. - -immutable(Q) -> Q#amqqueue{pid = none, - slave_pids = none, - sync_slave_pids = none, - recoverable_slaves = none, - gm_pids = none, - policy = none, - decorators = none, - state = none}. + pseudo_queue(QueueName, Pid, false). + +-spec pseudo_queue(name(), pid(), boolean()) -> amqqueue:amqqueue(). + +pseudo_queue(#resource{kind = queue} = QueueName, Pid, Durable) + when is_pid(Pid) andalso + is_boolean(Durable) -> + amqqueue:new(QueueName, + Pid, + Durable, + false, + none, % Owner, + [], + undefined, % VHost, + #{user => undefined}, % ActingUser + classic % Type + ). + +-spec immutable(amqqueue:amqqueue()) -> amqqueue:amqqueue(). + +immutable(Q) -> amqqueue:set_immutable(Q). + +-spec deliver([amqqueue:amqqueue()], rabbit_types:delivery()) -> 'ok'. deliver(Qs, Delivery) -> deliver(Qs, Delivery, untracked), ok. +-spec deliver([amqqueue:amqqueue()], + rabbit_types:delivery(), + quorum_states() | 'untracked') -> + {qpids(), + [{amqqueue:ra_server_id(), name()}], + quorum_states()}. + deliver([], _Delivery, QueueState) -> %% /dev/null optimisation {[], [], QueueState}; @@ -1530,17 +1808,26 @@ deliver(Qs, Delivery = #delivery{flow = Flow, {QPids, QuorumPids, QueueState}. qpids([]) -> {[], [], []}; %% optimisation -qpids([#amqqueue{pid = {LocalName, LeaderNode}, type = quorum, name = QName}]) -> +qpids([Q]) when ?amqqueue_is_quorum(Q) -> + QName = amqqueue:get_name(Q), + {LocalName, LeaderNode} = amqqueue:get_pid(Q), {[{{LocalName, LeaderNode}, QName}], [], []}; %% opt -qpids([#amqqueue{pid = QPid, slave_pids = SPids}]) -> +qpids([Q]) -> + QPid = amqqueue:get_pid(Q), + SPids = amqqueue:get_slave_pids(Q), {[], [QPid], SPids}; %% opt qpids(Qs) -> {QuoPids, MPids, SPids} = - lists:foldl(fun (#amqqueue{pid = QPid, type = quorum, name = QName}, - {QuoPidAcc, MPidAcc, SPidAcc}) -> + lists:foldl(fun (Q, + {QuoPidAcc, MPidAcc, SPidAcc}) + when ?amqqueue_is_quorum(Q) -> + QPid = amqqueue:get_pid(Q), + QName = amqqueue:get_name(Q), {[{QPid, QName} | QuoPidAcc], MPidAcc, SPidAcc}; - (#amqqueue{pid = QPid, slave_pids = SPids}, + (Q, {QuoPidAcc, MPidAcc, SPidAcc}) -> + QPid = amqqueue:get_pid(Q), + SPids = amqqueue:get_slave_pids(Q), {QuoPidAcc, [QPid | MPidAcc], [SPids | SPidAcc]} end, {[], [], []}, Qs), {QuoPids, MPids, lists:append(SPids)}. diff --git a/src/rabbit_amqqueue_process.erl b/src/rabbit_amqqueue_process.erl index 5782bc6e23..c3ba4a5c59 100644 --- a/src/rabbit_amqqueue_process.erl +++ b/src/rabbit_amqqueue_process.erl @@ -17,6 +17,7 @@ -module(rabbit_amqqueue_process). -include_lib("rabbit_common/include/rabbit.hrl"). -include_lib("rabbit_common/include/rabbit_framing.hrl"). +-include("amqqueue.hrl"). -behaviour(gen_server2). @@ -35,7 +36,7 @@ %% Queue's state -record(q, { %% an #amqqueue record - q, + q :: amqqueue:amqqueue(), %% none | {exclusive consumer channel PID, consumer tag} | {single active consumer channel PID, consumer} active_consumer, %% Set to true if a queue has ever had a consumer. @@ -101,14 +102,6 @@ %%---------------------------------------------------------------------------- --spec info_keys() -> rabbit_types:info_keys(). --spec init_with_backing_queue_state - (rabbit_types:amqqueue(), atom(), tuple(), any(), - [rabbit_types:delivery()], pmon:pmon(), gb_trees:tree()) -> - #q{}. - -%%---------------------------------------------------------------------------- - -define(STATISTICS_KEYS, [messages_ready, messages_unacknowledged, @@ -146,6 +139,8 @@ %%---------------------------------------------------------------------------- +-spec info_keys() -> rabbit_types:info_keys(). + info_keys() -> ?INFO_KEYS ++ rabbit_backing_queue:info_keys(). statistics_keys() -> ?STATISTICS_KEYS ++ rabbit_backing_queue:info_keys(). @@ -153,13 +148,13 @@ statistics_keys() -> ?STATISTICS_KEYS ++ rabbit_backing_queue:info_keys(). init(Q) -> process_flag(trap_exit, true), - ?store_proc_name(Q#amqqueue.name), - {ok, init_state(Q#amqqueue{pid = self()}), hibernate, + ?store_proc_name(amqqueue:get_name(Q)), + {ok, init_state(amqqueue:set_pid(Q, self())), hibernate, {backoff, ?HIBERNATE_AFTER_MIN, ?HIBERNATE_AFTER_MIN, ?DESIRED_HIBERNATE}, ?MODULE}. init_state(Q) -> - SingleActiveConsumerOn = case rabbit_misc:table_lookup(Q#amqqueue.arguments, <<"x-single-active-consumer">>) of + SingleActiveConsumerOn = case rabbit_misc:table_lookup(amqqueue:get_arguments(Q), <<"x-single-active-consumer">>) of {bool, true} -> true; _ -> false end, @@ -175,14 +170,16 @@ init_state(Q) -> single_active_consumer_on = SingleActiveConsumerOn}, rabbit_event:init_stats_timer(State, #q.stats_timer). -init_it(Recover, From, State = #q{q = #amqqueue{exclusive_owner = none}}) -> +init_it(Recover, From, State = #q{q = Q}) + when ?amqqueue_exclusive_owner_is(Q, none) -> init_it2(Recover, From, State); %% You used to be able to declare an exclusive durable queue. Sadly we %% need to still tidy up after that case, there could be the remnants %% of one left over from an upgrade. So that's why we don't enforce %% Recover = new here. -init_it(Recover, From, State = #q{q = #amqqueue{exclusive_owner = Owner}}) -> +init_it(Recover, From, State = #q{q = Q0}) -> + Owner = amqqueue:get_exclusive_owner(Q0), case rabbit_misc:is_process_alive(Owner) of true -> erlang:monitor(process, Owner), init_it2(Recover, From, State); @@ -204,7 +201,9 @@ init_it2(Recover, From, State = #q{q = Q, backing_queue_state = undefined}) -> {Barrier, TermsOrNew} = recovery_status(Recover), case rabbit_amqqueue:internal_declare(Q, Recover /= new) of - {Res, #amqqueue{} = Q1} when Res == created orelse Res == existing -> + {Res, Q1} + when ?is_amqqueue(Q1) andalso + (Res == created orelse Res == existing) -> case matches(Recover, Q, Q1) of true -> ok = file_handle_cache:register_callback( @@ -240,13 +239,14 @@ send_reply(From, Q) -> gen_server2:reply(From, Q). matches(new, Q1, Q2) -> %% i.e. not policy - Q1#amqqueue.name =:= Q2#amqqueue.name andalso - Q1#amqqueue.durable =:= Q2#amqqueue.durable andalso - Q1#amqqueue.auto_delete =:= Q2#amqqueue.auto_delete andalso - Q1#amqqueue.exclusive_owner =:= Q2#amqqueue.exclusive_owner andalso - Q1#amqqueue.arguments =:= Q2#amqqueue.arguments andalso - Q1#amqqueue.pid =:= Q2#amqqueue.pid andalso - Q1#amqqueue.slave_pids =:= Q2#amqqueue.slave_pids; + amqqueue:get_name(Q1) =:= amqqueue:get_name(Q2) andalso + amqqueue:is_durable(Q1) =:= amqqueue:is_durable(Q2) andalso + amqqueue:is_auto_delete(Q1) =:= amqqueue:is_auto_delete(Q2) andalso + amqqueue:get_exclusive_owner(Q1) =:= amqqueue:get_exclusive_owner(Q2) andalso + amqqueue:get_arguments(Q1) =:= amqqueue:get_arguments(Q2) andalso + amqqueue:get_pid(Q1) =:= amqqueue:get_pid(Q2) andalso + amqqueue:get_slave_pids(Q1) =:= amqqueue:get_slave_pids(Q2); +%% FIXME: Should v1 vs. v2 of the same record match? matches(_, Q, Q) -> true; matches(_, _Q, _Q1) -> false. @@ -259,8 +259,14 @@ recovery_barrier(BarrierPid) -> {'DOWN', MRef, process, _, _} -> ok end. -init_with_backing_queue_state(Q = #amqqueue{exclusive_owner = Owner}, BQ, BQS, +-spec init_with_backing_queue_state + (amqqueue:amqqueue(), atom(), tuple(), any(), + [rabbit_types:delivery()], pmon:pmon(), gb_trees:tree()) -> + #q{}. + +init_with_backing_queue_state(Q, BQ, BQS, RateTRef, Deliveries, Senders, MTC) -> + Owner = amqqueue:get_exclusive_owner(Q), case Owner of none -> ok; _ -> erlang:monitor(process, Owner) @@ -278,14 +284,18 @@ init_with_backing_queue_state(Q = #amqqueue{exclusive_owner = Owner}, BQ, BQS, notify_decorators(startup, State3), State3. -terminate(shutdown = R, State = #q{backing_queue = BQ, q = #amqqueue{ name = QName }}) -> +terminate(shutdown = R, State = #q{backing_queue = BQ, q = Q0}) -> + QName = amqqueue:get_name(Q0), rabbit_core_metrics:queue_deleted(qname(State)), terminate_shutdown( fun (BQS) -> rabbit_misc:execute_mnesia_transaction( fun() -> [Q] = mnesia:read({rabbit_queue, QName}), - Q2 = Q#amqqueue{state = stopped}, + Q2 = amqqueue:set_state(Q, stopped), + %% amqqueue migration: + %% The amqqueue was read from this transaction, no need + %% to handle migration. rabbit_amqqueue:store_queue(Q2) end), BQ:terminate(R, BQS) @@ -309,18 +319,24 @@ terminate(normal, State) -> %% delete case %% If we crashed don't try to clean up the BQS, probably best to leave it. terminate(_Reason, State = #q{q = Q}) -> terminate_shutdown(fun (BQS) -> - Q2 = Q#amqqueue{state = crashed}, + Q2 = amqqueue:set_state(Q, crashed), rabbit_misc:execute_mnesia_transaction( fun() -> - rabbit_amqqueue:store_queue(Q2) + ?try_mnesia_tx_or_upgrade_amqqueue_and_retry( + rabbit_amqqueue:store_queue(Q2), + begin + Q3 = amqqueue:upgrade(Q2), + rabbit_amqqueue:store_queue(Q3) + end) end), BQS end, State). terminate_delete(EmitStats, Reason0, - State = #q{q = #amqqueue{name = QName}, + State = #q{q = Q, backing_queue = BQ, status = Status}) -> + QName = amqqueue:get_name(Q), ActingUser = terminated_by(Status), fun (BQS) -> Reason = case Reason0 of @@ -389,7 +405,8 @@ notify_decorators(State = #q{consumers = Consumers, decorator_callback(QName, F, A) -> %% Look up again in case policy and hence decorators have changed case rabbit_amqqueue:lookup(QName) of - {ok, Q = #amqqueue{decorators = Ds}} -> + {ok, Q} -> + Ds = amqqueue:get_decorators(Q), [ok = apply(M, F, [Q|A]) || M <- rabbit_queue_decorator:select(Ds)]; {error, not_found} -> ok @@ -418,7 +435,8 @@ process_args_policy(State = #q{q = Q, Fun(args_policy_lookup(Name, Resolve, Q), StateN) end, State#q{args_policy_version = N + 1}, ArgsTable)). -args_policy_lookup(Name, Resolve, Q = #amqqueue{arguments = Args}) -> +args_policy_lookup(Name, Resolve, Q) -> + Args = amqqueue:get_arguments(Q), AName = <<"x-", Name/binary>>, case {rabbit_policy:get(Name, Q), rabbit_misc:table_lookup(Args, AName)} of {undefined, undefined} -> undefined; @@ -442,7 +460,8 @@ init_ttl(TTL, State) -> (init_ttl(undefined, State))#q{ttl = TTL}. init_dlx(undefined, State) -> State#q{dlx = undefined}; -init_dlx(DLX, State = #q{q = #amqqueue{name = QName}}) -> +init_dlx(DLX, State = #q{q = Q}) -> + QName = amqqueue:get_name(Q), State#q{dlx = rabbit_misc:r(QName, exchange, DLX)}. init_dlx_rkey(RoutingKey, State) -> State#q{dlx_routing_key = RoutingKey}. @@ -596,8 +615,9 @@ send_or_record_confirm(#delivery{confirm = true, message = #basic_message { is_persistent = true, id = MsgId}}, - State = #q{q = #amqqueue{durable = true}, - msg_id_to_channel = MTC}) -> + State = #q{q = Q, + msg_id_to_channel = MTC}) + when ?amqqueue_is_durable(Q) -> MTC1 = gb_trees:insert(MsgId, {SenderPid, MsgSeqNo}, MTC), {eventually, State#q{msg_id_to_channel = MTC1}}; send_or_record_confirm(#delivery{confirm = true, @@ -606,6 +626,18 @@ send_or_record_confirm(#delivery{confirm = true, rabbit_misc:confirm_to_sender(SenderPid, [MsgSeqNo]), {immediately, State}. +%% This feature was used by `rabbit_amqqueue_process` and +%% `rabbit_mirror_queue_slave` up-to and including RabbitMQ 3.7.x. It is +%% unused in 3.8.x and thus deprecated. We keep it to support in-place +%% upgrades to 3.8.x (i.e. mixed-version clusters), but it is a no-op +%% starting with that version. +send_mandatory(#delivery{mandatory = false}) -> + ok; +send_mandatory(#delivery{mandatory = true, + sender = SenderPid, + msg_seq_no = MsgSeqNo}) -> + gen_server2:cast(SenderPid, {mandatory_received, MsgSeqNo}). + discard(#delivery{confirm = Confirm, sender = SenderPid, flow = Flow, @@ -669,6 +701,7 @@ maybe_deliver_or_enqueue(Delivery = #delivery{message = Message}, State = #q{overflow = Overflow, backing_queue = BQ, backing_queue_state = BQS}) -> + send_mandatory(Delivery), %% must do this before confirms case {will_overflow(Delivery, State), Overflow} of {true, 'reject-publish'} -> %% Drop publish and nack to publisher @@ -815,7 +848,8 @@ possibly_unblock(Update, ChPid, State = #q{consumers = Consumers}) -> run_message_queue(true, State1) end. -should_auto_delete(#q{q = #amqqueue{auto_delete = false}}) -> false; +should_auto_delete(#q{q = Q}) + when not ?amqqueue_is_auto_delete(Q) -> false; should_auto_delete(#q{has_had_consumers = false}) -> false; should_auto_delete(State) -> is_unused(State). @@ -896,7 +930,7 @@ is_unused(_State) -> rabbit_queue_consumers:count() == 0. maybe_send_reply(_ChPid, undefined) -> ok; maybe_send_reply(ChPid, Msg) -> ok = rabbit_channel:send_command(ChPid, Msg). -qname(#q{q = #amqqueue{name = QName}}) -> QName. +qname(#q{q = Q}) -> amqqueue:get_name(Q). backing_queue_timeout(State = #q{backing_queue = BQ, backing_queue_state = BQS}) -> @@ -1001,17 +1035,18 @@ stop(Reply, State) -> {stop, normal, Reply, State}. infos(Items, State) -> [{Item, i(Item, State)} || Item <- Items]. -i(name, #q{q = #amqqueue{name = Name}}) -> Name; -i(durable, #q{q = #amqqueue{durable = Durable}}) -> Durable; -i(auto_delete, #q{q = #amqqueue{auto_delete = AutoDelete}}) -> AutoDelete; -i(arguments, #q{q = #amqqueue{arguments = Arguments}}) -> Arguments; +i(name, #q{q = Q}) -> amqqueue:get_name(Q); +i(durable, #q{q = Q}) -> amqqueue:is_durable(Q); +i(auto_delete, #q{q = Q}) -> amqqueue:is_auto_delete(Q); +i(arguments, #q{q = Q}) -> amqqueue:get_arguments(Q); i(pid, _) -> self(); -i(owner_pid, #q{q = #amqqueue{exclusive_owner = none}}) -> +i(owner_pid, #q{q = Q}) when ?amqqueue_exclusive_owner_is(Q, none) -> ''; -i(owner_pid, #q{q = #amqqueue{exclusive_owner = ExclusiveOwner}}) -> - ExclusiveOwner; -i(exclusive, #q{q = #amqqueue{exclusive_owner = ExclusiveOwner}}) -> +i(owner_pid, #q{q = Q}) -> + amqqueue:get_exclusive_owner(Q); +i(exclusive, #q{q = Q}) -> + ExclusiveOwner = amqqueue:get_exclusive_owner(Q), is_pid(ExclusiveOwner); i(policy, #q{q = Q}) -> case rabbit_policy:name(Q) of @@ -1061,27 +1096,27 @@ i(consumer_utilisation, #q{consumers = Consumers}) -> i(memory, _) -> {memory, M} = process_info(self(), memory), M; -i(slave_pids, #q{q = #amqqueue{name = Name}}) -> - {ok, Q = #amqqueue{slave_pids = SPids}} = - rabbit_amqqueue:lookup(Name), +i(slave_pids, #q{q = Q0}) -> + Name = amqqueue:get_name(Q0), + {ok, Q} = rabbit_amqqueue:lookup(Name), case rabbit_mirror_queue_misc:is_mirrored(Q) of false -> ''; - true -> SPids + true -> amqqueue:get_slave_pids(Q) end; -i(synchronised_slave_pids, #q{q = #amqqueue{name = Name}}) -> - {ok, Q = #amqqueue{sync_slave_pids = SSPids}} = - rabbit_amqqueue:lookup(Name), +i(synchronised_slave_pids, #q{q = Q0}) -> + Name = amqqueue:get_name(Q0), + {ok, Q} = rabbit_amqqueue:lookup(Name), case rabbit_mirror_queue_misc:is_mirrored(Q) of false -> ''; - true -> SSPids + true -> amqqueue:get_sync_slave_pids(Q) end; -i(recoverable_slaves, #q{q = #amqqueue{name = Name, - durable = Durable}}) -> - {ok, Q = #amqqueue{recoverable_slaves = Nodes}} = - rabbit_amqqueue:lookup(Name), +i(recoverable_slaves, #q{q = Q0}) -> + Name = amqqueue:get_name(Q0), + Durable = amqqueue:is_durable(Q0), + {ok, Q} = rabbit_amqqueue:lookup(Name), case Durable andalso rabbit_mirror_queue_misc:is_mirrored(Q) of false -> ''; - true -> Nodes + true -> amqqueue:get_recoverable_slaves(Q) end; i(state, #q{status = running}) -> credit_flow:state(); i(state, #q{status = State}) -> State; @@ -1090,7 +1125,8 @@ i(garbage_collection, _State) -> i(reductions, _State) -> {reductions, Reductions} = erlang:process_info(self(), reductions), Reductions; -i(user_who_performed_action, #q{q = #amqqueue{options = Opts}}) -> +i(user_who_performed_action, #q{q = Q}) -> + Opts = amqqueue:get_options(Q), maps:get(user, Opts, ?UNKNOWN_USER); i(Item, #q{backing_queue_state = BQS, backing_queue = BQ}) -> BQ:info(Item, BQS). @@ -1174,7 +1210,8 @@ consumer_bias(#q{backing_queue = BQ, backing_queue_state = BQS}, Low, High) -> {_, _} -> Low end. -prioritise_info(Msg, _Len, #q{q = #amqqueue{exclusive_owner = DownPid}}) -> +prioritise_info(Msg, _Len, #q{q = Q}) -> + DownPid = amqqueue:get_exclusive_owner(Q), case Msg of {'DOWN', _, process, DownPid, _} -> 8; update_ram_duration -> 8; @@ -1225,7 +1262,8 @@ handle_call({notify_down, ChPid}, _From, State) -> end; handle_call({basic_get, ChPid, NoAck, LimiterPid}, _From, - State = #q{q = #amqqueue{name = QName}}) -> + State = #q{q = Q}) -> + QName = amqqueue:get_name(Q), AckRequired = not NoAck, State1 = ensure_expiry_timer(State), case fetch(AckRequired, State1) of @@ -1542,11 +1580,33 @@ handle_cast({credit, ChPid, CTag, Credit, Drain}, {unblocked, Consumers1} -> State1 = State#q{consumers = Consumers1}, run_message_queue(true, State1) end); + +handle_cast({force_event_refresh, Ref}, + State = #q{consumers = Consumers, + active_consumer = Holder}) -> + rabbit_event:notify(queue_created, infos(?CREATION_EVENT_KEYS, State), Ref), + QName = qname(State), + AllConsumers = rabbit_queue_consumers:all(Consumers), + case Holder of + none -> + [emit_consumer_created( + Ch, CTag, false, AckRequired, QName, Prefetch, + Args, Ref, ActingUser) || + {Ch, CTag, AckRequired, Prefetch, Args, ActingUser} + <- AllConsumers]; + {Ch, CTag} -> + [{Ch, CTag, AckRequired, Prefetch, Args, ActingUser}] = AllConsumers, + emit_consumer_created( + Ch, CTag, true, AckRequired, QName, Prefetch, Args, Ref, ActingUser) + end, + noreply(rabbit_event:init_stats_timer(State, #q.stats_timer)); + handle_cast(notify_decorators, State) -> notify_decorators(State), noreply(State); -handle_cast(policy_changed, State = #q{q = #amqqueue{name = Name}}) -> +handle_cast(policy_changed, State = #q{q = Q0}) -> + Name = amqqueue:get_name(Q0), %% We depend on the #q.q field being up to date at least WRT %% policy (but not slave pids) in various places, so when it %% changes we go and read it from Mnesia again. @@ -1556,7 +1616,8 @@ handle_cast(policy_changed, State = #q{q = #amqqueue{name = Name}}) -> {ok, Q} = rabbit_amqqueue:lookup(Name), noreply(process_args_policy(State#q{q = Q})); -handle_cast({sync_start, _, _}, State = #q{q = #amqqueue{name = Name}}) -> +handle_cast({sync_start, _, _}, State = #q{q = Q}) -> + Name = amqqueue:get_name(Q), %% Only a slave should receive this, it means we are a duplicated master rabbit_mirror_queue_misc:log_warning( Name, "Stopping after receiving sync_start from another master", []), @@ -1587,7 +1648,7 @@ handle_info(emit_stats, State) -> {noreply, State1, Timeout}; handle_info({'DOWN', _MonitorRef, process, DownPid, _Reason}, - State = #q{q = #amqqueue{exclusive_owner = DownPid}}) -> + State = #q{q = Q}) when ?amqqueue_exclusive_owner_is(Q, DownPid) -> %% Exclusively owned queues must disappear with their owner. In %% the case of clean shutdown we delete the queue synchronously in %% the reader - although not required by the spec this seems to @@ -1665,21 +1726,23 @@ format_message_queue(Opt, MQ) -> rabbit_misc:format_message_queue(Opt, MQ). log_delete_exclusive({ConPid, _ConRef}, State) -> log_delete_exclusive(ConPid, State); -log_delete_exclusive(ConPid, #q{ q = #amqqueue{ name = Resource } }) -> +log_delete_exclusive(ConPid, #q{ q = Q }) -> + Resource = amqqueue:get_name(Q), #resource{ name = QName, virtual_host = VHost } = Resource, rabbit_log_queue:debug("Deleting exclusive queue '~s' in vhost '~s' " ++ "because its declaring connection ~p was closed", [QName, VHost, ConPid]). -log_auto_delete(Reason, #q{ q = #amqqueue{ name = Resource } }) -> +log_auto_delete(Reason, #q{ q = Q }) -> + Resource = amqqueue:get_name(Q), #resource{ name = QName, virtual_host = VHost } = Resource, rabbit_log_queue:debug("Deleting auto-delete queue '~s' in vhost '~s' " ++ Reason, [QName, VHost]). needs_update_mirroring(Q, Version) -> - {ok, UpQ} = rabbit_amqqueue:lookup(Q#amqqueue.name), - DBVersion = UpQ#amqqueue.policy_version, + {ok, UpQ} = rabbit_amqqueue:lookup(amqqueue:get_name(Q)), + DBVersion = amqqueue:get_policy_version(UpQ), case DBVersion > Version of true -> {rabbit_policy:get(<<"ha-mode">>, UpQ), DBVersion}; false -> false diff --git a/src/rabbit_amqqueue_sup.erl b/src/rabbit_amqqueue_sup.erl index 54a97b717e..64efc01786 100644 --- a/src/rabbit_amqqueue_sup.erl +++ b/src/rabbit_amqqueue_sup.erl @@ -26,11 +26,9 @@ %%---------------------------------------------------------------------------- --spec start_link(rabbit_types:amqqueue(), rabbit_prequeue:start_mode()) -> +-spec start_link(amqqueue:amqqueue(), rabbit_prequeue:start_mode()) -> {'ok', pid(), pid()}. -%%---------------------------------------------------------------------------- - start_link(Q, StartMode) -> Marker = spawn_link(fun() -> receive stop -> ok end end), ChildSpec = {rabbit_amqqueue, diff --git a/src/rabbit_amqqueue_sup_sup.erl b/src/rabbit_amqqueue_sup_sup.erl index d8045276c6..9e30a7f0ae 100644 --- a/src/rabbit_amqqueue_sup_sup.erl +++ b/src/rabbit_amqqueue_sup_sup.erl @@ -31,17 +31,16 @@ %%---------------------------------------------------------------------------- -spec start_link() -> rabbit_types:ok_pid_or_error(). --spec start_queue_process - (node(), rabbit_types:amqqueue(), 'declare' | 'recovery' | 'slave') -> - pid(). - -%%---------------------------------------------------------------------------- start_link() -> supervisor2:start_link(?MODULE, []). +-spec start_queue_process + (node(), amqqueue:amqqueue(), 'declare' | 'recovery' | 'slave') -> + pid(). + start_queue_process(Node, Q, StartMode) -> - #amqqueue{name = #resource{virtual_host = VHost}} = Q, + #resource{virtual_host = VHost} = amqqueue:get_name(Q), {ok, Sup} = find_for_vhost(VHost, Node), {ok, _SupPid, QPid} = supervisor2:start_child(Sup, [Q, StartMode]), QPid. diff --git a/src/rabbit_auth_backend_internal.erl b/src/rabbit_auth_backend_internal.erl index 83580e8b9c..5732ac4b30 100644 --- a/src/rabbit_auth_backend_internal.erl +++ b/src/rabbit_auth_backend_internal.erl @@ -47,46 +47,6 @@ -type regexp() :: binary(). --spec add_user(rabbit_types:username(), rabbit_types:password(), - rabbit_types:username()) -> 'ok' | {'error', string()}. --spec delete_user(rabbit_types:username(), rabbit_types:username()) -> 'ok'. --spec lookup_user - (rabbit_types:username()) -> - rabbit_types:ok(rabbit_types:internal_user()) | - rabbit_types:error('not_found'). --spec change_password - (rabbit_types:username(), rabbit_types:password(), rabbit_types:username()) -> 'ok'. --spec clear_password(rabbit_types:username(), rabbit_types:username()) -> 'ok'. --spec hash_password - (module(), rabbit_types:password()) -> rabbit_types:password_hash(). --spec change_password_hash - (rabbit_types:username(), rabbit_types:password_hash()) -> 'ok'. --spec set_tags(rabbit_types:username(), [atom()], rabbit_types:username()) -> 'ok'. --spec set_permissions - (rabbit_types:username(), rabbit_types:vhost(), regexp(), regexp(), - regexp(), rabbit_types:username()) -> - 'ok'. --spec clear_permissions - (rabbit_types:username(), rabbit_types:vhost(), rabbit_types:username()) -> 'ok'. --spec user_info_keys() -> rabbit_types:info_keys(). --spec perms_info_keys() -> rabbit_types:info_keys(). --spec user_perms_info_keys() -> rabbit_types:info_keys(). --spec vhost_perms_info_keys() -> rabbit_types:info_keys(). --spec user_vhost_perms_info_keys() -> rabbit_types:info_keys(). --spec list_users() -> [rabbit_types:infos()]. --spec list_users(reference(), pid()) -> 'ok'. --spec list_permissions() -> [rabbit_types:infos()]. --spec list_user_permissions - (rabbit_types:username()) -> [rabbit_types:infos()]. --spec list_user_permissions - (rabbit_types:username(), reference(), pid()) -> 'ok'. --spec list_vhost_permissions - (rabbit_types:vhost()) -> [rabbit_types:infos()]. --spec list_vhost_permissions - (rabbit_types:vhost(), reference(), pid()) -> 'ok'. --spec list_user_vhost_permissions - (rabbit_types:username(), rabbit_types:vhost()) -> [rabbit_types:infos()]. - %%---------------------------------------------------------------------------- %% Implementation of rabbit_auth_backend @@ -238,6 +198,9 @@ validate_and_alternate_credentials(Username, Password, ActingUser, Fun) -> {error, Err} end. +-spec add_user(rabbit_types:username(), rabbit_types:password(), + rabbit_types:username()) -> 'ok' | {'error', string()}. + add_user(Username, Password, ActingUser) -> validate_and_alternate_credentials(Username, Password, ActingUser, fun add_user_sans_validation/3). @@ -265,6 +228,8 @@ add_user_sans_validation(Username, Password, ActingUser) -> {user_who_performed_action, ActingUser}]), R. +-spec delete_user(rabbit_types:username(), rabbit_types:username()) -> 'ok'. + delete_user(Username, ActingUser) -> rabbit_log:info("Deleting user '~s'~n", [Username]), R = rabbit_misc:execute_mnesia_transaction( @@ -291,9 +256,17 @@ delete_user(Username, ActingUser) -> {user_who_performed_action, ActingUser}]), R. +-spec lookup_user + (rabbit_types:username()) -> + rabbit_types:ok(rabbit_types:internal_user()) | + rabbit_types:error('not_found'). + lookup_user(Username) -> rabbit_misc:dirty_read({rabbit_user, Username}). +-spec change_password + (rabbit_types:username(), rabbit_types:password(), rabbit_types:username()) -> 'ok'. + change_password(Username, Password, ActingUser) -> validate_and_alternate_credentials(Username, Password, ActingUser, fun change_password_sans_validation/3). @@ -310,6 +283,8 @@ change_password_sans_validation(Username, Password, ActingUser) -> {user_who_performed_action, ActingUser}]), R. +-spec clear_password(rabbit_types:username(), rabbit_types:username()) -> 'ok'. + clear_password(Username, ActingUser) -> rabbit_log:info("Clearing password for '~s'~n", [Username]), R = change_password_hash(Username, <<"">>), @@ -318,9 +293,15 @@ clear_password(Username, ActingUser) -> {user_who_performed_action, ActingUser}]), R. +-spec hash_password + (module(), rabbit_types:password()) -> rabbit_types:password_hash(). + hash_password(HashingMod, Cleartext) -> rabbit_password:hash(HashingMod, Cleartext). +-spec change_password_hash + (rabbit_types:username(), rabbit_types:password_hash()) -> 'ok'. + change_password_hash(Username, PasswordHash) -> change_password_hash(Username, PasswordHash, rabbit_password:hashing_mod()). @@ -332,6 +313,8 @@ change_password_hash(Username, PasswordHash, HashingAlgorithm) -> hashing_algorithm = HashingAlgorithm } end). +-spec set_tags(rabbit_types:username(), [atom()], rabbit_types:username()) -> 'ok'. + set_tags(Username, Tags, ActingUser) -> ConvertedTags = [rabbit_data_coercion:to_atom(I) || I <- Tags], rabbit_log:info("Setting user tags for user '~s' to ~p~n", @@ -343,6 +326,11 @@ set_tags(Username, Tags, ActingUser) -> {user_who_performed_action, ActingUser}]), R. +-spec set_permissions + (rabbit_types:username(), rabbit_types:vhost(), regexp(), regexp(), + regexp(), rabbit_types:username()) -> + 'ok'. + set_permissions(Username, VHostPath, ConfigurePerm, WritePerm, ReadPerm, ActingUser) -> rabbit_log:info("Setting permissions for " "'~s' in '~s' to '~s', '~s', '~s'~n", @@ -378,6 +366,9 @@ set_permissions(Username, VHostPath, ConfigurePerm, WritePerm, ReadPerm, ActingU {user_who_performed_action, ActingUser}]), R. +-spec clear_permissions + (rabbit_types:username(), rabbit_types:vhost(), rabbit_types:username()) -> 'ok'. + clear_permissions(Username, VHostPath, ActingUser) -> R = rabbit_misc:execute_mnesia_transaction( rabbit_vhost:with_user_and_vhost( @@ -481,11 +472,24 @@ clear_topic_permissions(Username, VHostPath, Exchange, ActingUser) -> -define(PERMS_INFO_KEYS, [configure, write, read]). -define(USER_INFO_KEYS, [user, tags]). +-spec user_info_keys() -> rabbit_types:info_keys(). + user_info_keys() -> ?USER_INFO_KEYS. +-spec perms_info_keys() -> rabbit_types:info_keys(). + perms_info_keys() -> [user, vhost | ?PERMS_INFO_KEYS]. + +-spec vhost_perms_info_keys() -> rabbit_types:info_keys(). + vhost_perms_info_keys() -> [user | ?PERMS_INFO_KEYS]. + +-spec user_perms_info_keys() -> rabbit_types:info_keys(). + user_perms_info_keys() -> [vhost | ?PERMS_INFO_KEYS]. + +-spec user_vhost_perms_info_keys() -> rabbit_types:info_keys(). + user_vhost_perms_info_keys() -> ?PERMS_INFO_KEYS. topic_perms_info_keys() -> [user, vhost, exchange, write, read]. @@ -493,16 +497,22 @@ user_topic_perms_info_keys() -> [vhost, exchange, write, read]. vhost_topic_perms_info_keys() -> [user, exchange, write, read]. user_vhost_topic_perms_info_keys() -> [exchange, write, read]. +-spec list_users() -> [rabbit_types:infos()]. + list_users() -> [extract_internal_user_params(U) || U <- mnesia:dirty_match_object(rabbit_user, #internal_user{_ = '_'})]. +-spec list_users(reference(), pid()) -> 'ok'. + list_users(Ref, AggregatorPid) -> rabbit_control_misc:emitting_map( AggregatorPid, Ref, fun(U) -> extract_internal_user_params(U) end, mnesia:dirty_match_object(rabbit_user, #internal_user{_ = '_'})). +-spec list_permissions() -> [rabbit_types:infos()]. + list_permissions() -> list_permissions(perms_info_keys(), match_user_vhost('_', '_')). @@ -517,28 +527,43 @@ list_permissions(Keys, QueryThunk, Ref, AggregatorPid) -> filter_props(Keys, Props) -> [T || T = {K, _} <- Props, lists:member(K, Keys)]. +-spec list_user_permissions + (rabbit_types:username()) -> [rabbit_types:infos()]. + list_user_permissions(Username) -> list_permissions( user_perms_info_keys(), rabbit_misc:with_user(Username, match_user_vhost(Username, '_'))). +-spec list_user_permissions + (rabbit_types:username(), reference(), pid()) -> 'ok'. + list_user_permissions(Username, Ref, AggregatorPid) -> list_permissions( user_perms_info_keys(), rabbit_misc:with_user(Username, match_user_vhost(Username, '_')), Ref, AggregatorPid). +-spec list_vhost_permissions + (rabbit_types:vhost()) -> [rabbit_types:infos()]. + list_vhost_permissions(VHostPath) -> list_permissions( vhost_perms_info_keys(), rabbit_vhost:with(VHostPath, match_user_vhost('_', VHostPath))). +-spec list_vhost_permissions + (rabbit_types:vhost(), reference(), pid()) -> 'ok'. + list_vhost_permissions(VHostPath, Ref, AggregatorPid) -> list_permissions( vhost_perms_info_keys(), rabbit_vhost:with(VHostPath, match_user_vhost('_', VHostPath)), Ref, AggregatorPid). +-spec list_user_vhost_permissions + (rabbit_types:username(), rabbit_types:vhost()) -> [rabbit_types:infos()]. + list_user_vhost_permissions(Username, VHostPath) -> list_permissions( user_vhost_perms_info_keys(), diff --git a/src/rabbit_backing_queue.erl b/src/rabbit_backing_queue.erl new file mode 100644 index 0000000000..c3e570b26f --- /dev/null +++ b/src/rabbit_backing_queue.erl @@ -0,0 +1,273 @@ +%% The contents of this file are subject to the Mozilla Public License +%% Version 1.1 (the "License"); you may not use this file except in +%% compliance with the License. You may obtain a copy of the License +%% at http://www.mozilla.org/MPL/ +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and +%% limitations under the License. +%% +%% The Original Code is RabbitMQ. +%% +%% The Initial Developer of the Original Code is GoPivotal, Inc. +%% Copyright (c) 2007-2017 Pivotal Software, Inc. All rights reserved. +%% + +-module(rabbit_backing_queue). + +-export([info_keys/0]). + +-define(INFO_KEYS, [messages_ram, messages_ready_ram, + messages_unacknowledged_ram, messages_persistent, + message_bytes, message_bytes_ready, + message_bytes_unacknowledged, message_bytes_ram, + message_bytes_persistent, head_message_timestamp, + disk_reads, disk_writes, backing_queue_status, + messages_paged_out, message_bytes_paged_out]). + +%% We can't specify a per-queue ack/state with callback signatures +-type ack() :: any(). +-type state() :: any(). + +-type flow() :: 'flow' | 'noflow'. +-type msg_ids() :: [rabbit_types:msg_id()]. +-type publish() :: {rabbit_types:basic_message(), + rabbit_types:message_properties(), boolean()}. +-type delivered_publish() :: {rabbit_types:basic_message(), + rabbit_types:message_properties()}. +-type fetch_result(Ack) :: + ('empty' | {rabbit_types:basic_message(), boolean(), Ack}). +-type drop_result(Ack) :: + ('empty' | {rabbit_types:msg_id(), Ack}). +-type recovery_terms() :: [term()] | 'non_clean_shutdown'. +-type recovery_info() :: 'new' | recovery_terms(). +-type purged_msg_count() :: non_neg_integer(). +-type async_callback() :: + fun ((atom(), fun ((atom(), state()) -> state())) -> 'ok'). +-type duration() :: ('undefined' | 'infinity' | number()). + +-type msg_fun(A) :: fun ((rabbit_types:basic_message(), ack(), A) -> A). +-type msg_pred() :: fun ((rabbit_types:message_properties()) -> boolean()). + +-type queue_mode() :: atom(). + +%% Called on startup with a vhost and a list of durable queue names on this vhost. +%% The queues aren't being started at this point, but this call allows the +%% backing queue to perform any checking necessary for the consistency +%% of those queues, or initialise any other shared resources. +%% +%% The list of queue recovery terms returned as {ok, Terms} must be given +%% in the same order as the list of queue names supplied. +-callback start(rabbit_types:vhost(), [rabbit_amqqueue:name()]) -> rabbit_types:ok(recovery_terms()). + +%% Called to tear down any state/resources for vhost. NB: Implementations should +%% not depend on this function being called on shutdown and instead +%% should hook into the rabbit supervision hierarchy. +-callback stop(rabbit_types:vhost()) -> 'ok'. + +%% Initialise the backing queue and its state. +%% +%% Takes +%% 1. the amqqueue record +%% 2. a term indicating whether the queue is an existing queue that +%% should be recovered or not. When 'new' is given, no recovery is +%% taking place, otherwise a list of recovery terms is given, or +%% the atom 'non_clean_shutdown' if no recovery terms are available. +%% 3. an asynchronous callback which accepts a function of type +%% backing-queue-state to backing-queue-state. This callback +%% function can be safely invoked from any process, which makes it +%% useful for passing messages back into the backing queue, +%% especially as the backing queue does not have control of its own +%% mailbox. +-callback init(amqqueue:amqqueue(), recovery_info(), + async_callback()) -> state(). + +%% Called on queue shutdown when queue isn't being deleted. +-callback terminate(any(), state()) -> state(). + +%% Called when the queue is terminating and needs to delete all its +%% content. +-callback delete_and_terminate(any(), state()) -> state(). + +%% Called to clean up after a crashed queue. In this case we don't +%% have a process and thus a state(), we are just removing on-disk data. +-callback delete_crashed(amqqueue:amqqueue()) -> 'ok'. + +%% Remove all 'fetchable' messages from the queue, i.e. all messages +%% except those that have been fetched already and are pending acks. +-callback purge(state()) -> {purged_msg_count(), state()}. + +%% Remove all messages in the queue which have been fetched and are +%% pending acks. +-callback purge_acks(state()) -> state(). + +%% Publish a message. +-callback publish(rabbit_types:basic_message(), + rabbit_types:message_properties(), boolean(), pid(), flow(), + state()) -> state(). + +%% Like publish/6 but for batches of publishes. +-callback batch_publish([publish()], pid(), flow(), state()) -> state(). + +%% Called for messages which have already been passed straight +%% out to a client. The queue will be empty for these calls +%% (i.e. saves the round trip through the backing queue). +-callback publish_delivered(rabbit_types:basic_message(), + rabbit_types:message_properties(), pid(), flow(), + state()) + -> {ack(), state()}. + +%% Like publish_delivered/5 but for batches of publishes. +-callback batch_publish_delivered([delivered_publish()], pid(), flow(), + state()) + -> {[ack()], state()}. + +%% Called to inform the BQ about messages which have reached the +%% queue, but are not going to be further passed to BQ. +-callback discard(rabbit_types:msg_id(), pid(), flow(), state()) -> state(). + +%% Return ids of messages which have been confirmed since the last +%% invocation of this function (or initialisation). +%% +%% Message ids should only appear in the result of drain_confirmed +%% under the following circumstances: +%% +%% 1. The message appears in a call to publish_delivered/4 and the +%% first argument (ack_required) is false; or +%% 2. The message is fetched from the queue with fetch/2 and the first +%% argument (ack_required) is false; or +%% 3. The message is acked (ack/2 is called for the message); or +%% 4. The message is fully fsync'd to disk in such a way that the +%% recovery of the message is guaranteed in the event of a crash of +%% this rabbit node (excluding hardware failure). +%% +%% In addition to the above conditions, a message id may only appear +%% in the result of drain_confirmed if +%% #message_properties.needs_confirming = true when the msg was +%% published (through whichever means) to the backing queue. +%% +%% It is legal for the same message id to appear in the results of +%% multiple calls to drain_confirmed, which means that the backing +%% queue is not required to keep track of which messages it has +%% already confirmed. The confirm will be issued to the publisher the +%% first time the message id appears in the result of +%% drain_confirmed. All subsequent appearances of that message id will +%% be ignored. +-callback drain_confirmed(state()) -> {msg_ids(), state()}. + +%% Drop messages from the head of the queue while the supplied +%% predicate on message properties returns true. Returns the first +%% message properties for which the predictate returned false, or +%% 'undefined' if the whole backing queue was traversed w/o the +%% predicate ever returning false. +-callback dropwhile(msg_pred(), state()) + -> {rabbit_types:message_properties() | undefined, state()}. + +%% Like dropwhile, except messages are fetched in "require +%% acknowledgement" mode and are passed, together with their ack tag, +%% to the supplied function. The function is also fed an +%% accumulator. The result of fetchwhile is as for dropwhile plus the +%% accumulator. +-callback fetchwhile(msg_pred(), msg_fun(A), A, state()) + -> {rabbit_types:message_properties() | undefined, + A, state()}. + +%% Produce the next message. +-callback fetch(true, state()) -> {fetch_result(ack()), state()}; + (false, state()) -> {fetch_result(undefined), state()}. + +%% Remove the next message. +-callback drop(true, state()) -> {drop_result(ack()), state()}; + (false, state()) -> {drop_result(undefined), state()}. + +%% Acktags supplied are for messages which can now be forgotten +%% about. Must return 1 msg_id per Ack, in the same order as Acks. +-callback ack([ack()], state()) -> {msg_ids(), state()}. + +%% Reinsert messages into the queue which have already been delivered +%% and were pending acknowledgement. +-callback requeue([ack()], state()) -> {msg_ids(), state()}. + +%% Fold over messages by ack tag. The supplied function is called with +%% each message, its ack tag, and an accumulator. +-callback ackfold(msg_fun(A), A, state(), [ack()]) -> {A, state()}. + +%% Fold over all the messages in a queue and return the accumulated +%% results, leaving the queue undisturbed. +-callback fold(fun((rabbit_types:basic_message(), + rabbit_types:message_properties(), + boolean(), A) -> {('stop' | 'cont'), A}), + A, state()) -> {A, state()}. + +%% How long is my queue? +-callback len(state()) -> non_neg_integer(). + +%% Is my queue empty? +-callback is_empty(state()) -> boolean(). + +%% What's the queue depth, where depth = length + number of pending acks +-callback depth(state()) -> non_neg_integer(). + +%% For the next three functions, the assumption is that you're +%% monitoring something like the ingress and egress rates of the +%% queue. The RAM duration is thus the length of time represented by +%% the messages held in RAM given the current rates. If you want to +%% ignore all of this stuff, then do so, and return 0 in +%% ram_duration/1. + +%% The target is to have no more messages in RAM than indicated by the +%% duration and the current queue rates. +-callback set_ram_duration_target(duration(), state()) -> state(). + +%% Optionally recalculate the duration internally (likely to be just +%% update your internal rates), and report how many seconds the +%% messages in RAM represent given the current rates of the queue. +-callback ram_duration(state()) -> {duration(), state()}. + +%% Should 'timeout' be called as soon as the queue process can manage +%% (either on an empty mailbox, or when a timer fires)? +-callback needs_timeout(state()) -> 'false' | 'timed' | 'idle'. + +%% Called (eventually) after needs_timeout returns 'idle' or 'timed'. +%% Note this may be called more than once for each 'idle' or 'timed' +%% returned from needs_timeout +-callback timeout(state()) -> state(). + +%% Called immediately before the queue hibernates. +-callback handle_pre_hibernate(state()) -> state(). + +%% Called when more credit has become available for credit_flow. +-callback resume(state()) -> state(). + +%% Used to help prioritisation in rabbit_amqqueue_process. The rate of +%% inbound messages and outbound messages at the moment. +-callback msg_rates(state()) -> {float(), float()}. + +-callback info(atom(), state()) -> any(). + +%% Passed a function to be invoked with the relevant backing queue's +%% state. Useful for when the backing queue or other components need +%% to pass functions into the backing queue. +-callback invoke(atom(), fun ((atom(), A) -> A), state()) -> state(). + +%% Called prior to a publish or publish_delivered call. Allows the BQ +%% to signal that it's already seen this message, (e.g. it was published +%% or discarded previously) specifying whether to drop the message or reject it. +-callback is_duplicate(rabbit_types:basic_message(), state()) + -> {{true, drop} | {true, reject} | boolean(), state()}. + +-callback set_queue_mode(queue_mode(), state()) -> state(). + +-callback zip_msgs_and_acks(delivered_publish(), + [ack()], Acc, state()) + -> Acc. + +%% Called when rabbit_amqqueue_process receives a message via +%% handle_info and it should be processed by the backing +%% queue +-callback handle_info(term(), state()) -> state(). + +-spec info_keys() -> rabbit_types:info_keys(). + +info_keys() -> ?INFO_KEYS. diff --git a/src/rabbit_basic.erl b/src/rabbit_basic.erl index 6cb91a9243..40c60ece45 100644 --- a/src/rabbit_basic.erl +++ b/src/rabbit_basic.erl @@ -37,61 +37,27 @@ -type exchange_input() :: rabbit_types:exchange() | rabbit_exchange:name(). -type body_input() :: binary() | [binary()]. +%%---------------------------------------------------------------------------- + +%% Convenience function, for avoiding round-trips in calls across the +%% erlang distributed network. + -spec publish (exchange_input(), rabbit_router:routing_key(), properties_input(), body_input()) -> publish_result(). --spec publish - (exchange_input(), rabbit_router:routing_key(), boolean(), - properties_input(), body_input()) -> - publish_result(). --spec publish(rabbit_types:delivery()) -> publish_result(). --spec delivery - (boolean(), boolean(), rabbit_types:message(), undefined | integer()) -> - rabbit_types:delivery(). --spec message - (rabbit_exchange:name(), rabbit_router:routing_key(), properties_input(), - binary()) -> - rabbit_types:message(). --spec message - (rabbit_exchange:name(), rabbit_router:routing_key(), - rabbit_types:decoded_content()) -> - rabbit_types:ok_or_error2(rabbit_types:message(), any()). --spec properties - (properties_input()) -> rabbit_framing:amqp_property_record(). - --spec prepend_table_header - (binary(), rabbit_framing:amqp_table(), headers()) -> headers(). --spec header(header(), headers()) -> 'undefined' | any(). --spec header(header(), headers(), any()) -> 'undefined' | any(). - --spec extract_headers(rabbit_types:content()) -> headers(). - --spec map_headers - (fun((headers()) -> headers()), rabbit_types:content()) -> - rabbit_types:content(). - --spec header_routes(undefined | rabbit_framing:amqp_table()) -> [string()]. --spec build_content - (rabbit_framing:amqp_property_record(), binary() | [binary()]) -> - rabbit_types:content(). --spec from_content - (rabbit_types:content()) -> - {rabbit_framing:amqp_property_record(), binary()}. --spec parse_expiration - (rabbit_framing:amqp_property_record()) -> - rabbit_types:ok_or_error2('undefined' | non_neg_integer(), any()). - -%%---------------------------------------------------------------------------- - -%% Convenience function, for avoiding round-trips in calls across the -%% erlang distributed network. publish(Exchange, RoutingKeyBin, Properties, Body) -> publish(Exchange, RoutingKeyBin, false, Properties, Body). %% Convenience function, for avoiding round-trips in calls across the %% erlang distributed network. + +-spec publish + (exchange_input(), rabbit_router:routing_key(), boolean(), + properties_input(), body_input()) -> + publish_result(). + publish(X = #exchange{name = XName}, RKey, Mandatory, Props, Body) -> Message = message(XName, RKey, properties(Props), Body), publish(X, delivery(Mandatory, false, Message, undefined)); @@ -99,6 +65,8 @@ publish(XName, RKey, Mandatory, Props, Body) -> Message = message(XName, RKey, properties(Props), Body), publish(delivery(Mandatory, false, Message, undefined)). +-spec publish(rabbit_types:delivery()) -> publish_result(). + publish(Delivery = #delivery{ message = #basic_message{exchange_name = XName}}) -> case rabbit_exchange:lookup(XName) of @@ -111,10 +79,18 @@ publish(X, Delivery) -> DeliveredQPids = rabbit_amqqueue:deliver(Qs, Delivery), {ok, DeliveredQPids}. +-spec delivery + (boolean(), boolean(), rabbit_types:message(), undefined | integer()) -> + rabbit_types:delivery(). + delivery(Mandatory, Confirm, Message, MsgSeqNo) -> #delivery{mandatory = Mandatory, confirm = Confirm, sender = self(), message = Message, msg_seq_no = MsgSeqNo, flow = noflow}. +-spec build_content + (rabbit_framing:amqp_property_record(), binary() | [binary()]) -> + rabbit_types:content(). + build_content(Properties, BodyBin) when is_binary(BodyBin) -> build_content(Properties, [BodyBin]); @@ -128,6 +104,10 @@ build_content(Properties, PFR) -> protocol = none, payload_fragments_rev = PFR}. +-spec from_content + (rabbit_types:content()) -> + {rabbit_framing:amqp_property_record(), binary()}. + from_content(Content) -> #content{class_id = ClassId, properties = Props, @@ -153,6 +133,11 @@ strip_header(#content{properties = Props = #'P_basic'{headers = Headers}} headers = Headers0}}) end. +-spec message + (rabbit_exchange:name(), rabbit_router:routing_key(), + rabbit_types:decoded_content()) -> + rabbit_types:ok_or_error2(rabbit_types:message(), any()). + message(XName, RoutingKey, #content{properties = Props} = DecodedContent) -> try {ok, #basic_message{ @@ -166,12 +151,20 @@ message(XName, RoutingKey, #content{properties = Props} = DecodedContent) -> {error, _Reason} = Error -> Error end. +-spec message + (rabbit_exchange:name(), rabbit_router:routing_key(), properties_input(), + binary()) -> + rabbit_types:message(). + message(XName, RoutingKey, RawProperties, Body) -> Properties = properties(RawProperties), Content = build_content(Properties, Body), {ok, Msg} = message(XName, RoutingKey, Content), Msg. +-spec properties + (properties_input()) -> rabbit_framing:amqp_property_record(). + properties(P = #'P_basic'{}) -> P; properties(P) when is_list(P) -> @@ -185,6 +178,9 @@ properties(P) when is_list(P) -> end end, #'P_basic'{}, P). +-spec prepend_table_header + (binary(), rabbit_framing:amqp_table(), headers()) -> headers(). + prepend_table_header(Name, Info, undefined) -> prepend_table_header(Name, Info, []); prepend_table_header(Name, Info, Headers) -> @@ -224,6 +220,8 @@ update_invalid(Name, Value, ExistingHdr, Header) -> NewHdr = rabbit_misc:set_table_value(ExistingHdr, Name, array, Values), set_invalid(NewHdr, Header). +-spec header(header(), headers()) -> 'undefined' | any(). + header(_Header, undefined) -> undefined; header(_Header, []) -> @@ -231,12 +229,16 @@ header(_Header, []) -> header(Header, Headers) -> header(Header, Headers, undefined). +-spec header(header(), headers(), any()) -> 'undefined' | any(). + header(Header, Headers, Default) -> case lists:keysearch(Header, 1, Headers) of false -> Default; {value, Val} -> Val end. +-spec extract_headers(rabbit_types:content()) -> headers(). + extract_headers(Content) -> #content{properties = #'P_basic'{headers = Headers}} = rabbit_binary_parser:ensure_content_decoded(Content), @@ -247,6 +249,10 @@ extract_timestamp(Content) -> rabbit_binary_parser:ensure_content_decoded(Content), Timestamp. +-spec map_headers + (fun((headers()) -> headers()), rabbit_types:content()) -> + rabbit_types:content(). + map_headers(F, Content) -> Content1 = rabbit_binary_parser:ensure_content_decoded(Content), #content{properties = #'P_basic'{headers = Headers} = Props} = Content1, @@ -270,6 +276,9 @@ is_message_persistent(#content{properties = #'P_basic'{ end. %% Extract CC routes from headers + +-spec header_routes(undefined | rabbit_framing:amqp_table()) -> [string()]. + header_routes(undefined) -> []; header_routes(HeadersTable) -> @@ -281,6 +290,10 @@ header_routes(HeadersTable) -> binary_to_list(HeaderKey), Type}}) end || HeaderKey <- ?ROUTING_HEADERS]). +-spec parse_expiration + (rabbit_framing:amqp_property_record()) -> + rabbit_types:ok_or_error2('undefined' | non_neg_integer(), any()). + parse_expiration(#'P_basic'{expiration = undefined}) -> {ok, undefined}; parse_expiration(#'P_basic'{expiration = Expiration}) -> diff --git a/src/rabbit_binding.erl b/src/rabbit_binding.erl index bb1c754b5c..ab3bc6c819 100644 --- a/src/rabbit_binding.erl +++ b/src/rabbit_binding.erl @@ -15,7 +15,8 @@ %% -module(rabbit_binding). --include("rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include("amqqueue.hrl"). -export([recover/0, recover/2, exists/1, add/2, add/3, remove/1, remove/3, list/1]). -export([list_for_source/1, list_for_destination/1, @@ -44,7 +45,7 @@ {'resources_missing', [{'not_found', (rabbit_types:binding_source() | rabbit_types:binding_destination())} | - {'absent', rabbit_types:amqqueue()}]}). + {'absent', amqqueue:amqqueue()}]}). -type bind_ok_or_error() :: 'ok' | bind_errors() | rabbit_types:error( @@ -53,7 +54,7 @@ -type bind_res() :: bind_ok_or_error() | rabbit_misc:thunk(bind_ok_or_error()). -type inner_fun() :: fun((rabbit_types:exchange(), - rabbit_types:exchange() | rabbit_types:amqqueue()) -> + rabbit_types:exchange() | amqqueue:amqqueue()) -> rabbit_types:ok_or_error(rabbit_types:amqp_error())). -type bindings() :: [rabbit_types:binding()]. @@ -61,47 +62,6 @@ %% dialyzer into objecting to everything that uses it. -type deletions() :: dict:dict(). --spec recover([rabbit_exchange:name()], [rabbit_amqqueue:name()]) -> - 'ok'. --spec exists(rabbit_types:binding()) -> boolean() | bind_errors(). --spec add(rabbit_types:binding(), rabbit_types:username()) -> bind_res(). --spec add(rabbit_types:binding(), inner_fun(), rabbit_types:username()) -> bind_res(). --spec remove(rabbit_types:binding()) -> bind_res(). --spec remove(rabbit_types:binding(), inner_fun(), rabbit_types:username()) -> bind_res(). --spec list(rabbit_types:vhost()) -> bindings(). --spec list_for_source - (rabbit_types:binding_source()) -> bindings(). --spec list_for_destination - (rabbit_types:binding_destination()) -> bindings(). --spec list_for_source_and_destination - (rabbit_types:binding_source(), rabbit_types:binding_destination()) -> - bindings(). --spec info_keys() -> rabbit_types:info_keys(). --spec info(rabbit_types:binding()) -> rabbit_types:infos(). --spec info(rabbit_types:binding(), rabbit_types:info_keys()) -> - rabbit_types:infos(). --spec info_all(rabbit_types:vhost()) -> [rabbit_types:infos()]. --spec info_all(rabbit_types:vhost(), rabbit_types:info_keys()) -> - [rabbit_types:infos()]. --spec info_all(rabbit_types:vhost(), rabbit_types:info_keys(), - reference(), pid()) -> 'ok'. --spec has_for_source(rabbit_types:binding_source()) -> boolean(). --spec remove_for_source(rabbit_types:binding_source()) -> bindings(). --spec remove_for_destination - (rabbit_types:binding_destination(), boolean()) -> deletions(). --spec remove_transient_for_destination - (rabbit_types:binding_destination()) -> deletions(). --spec process_deletions(deletions(), rabbit_types:username()) -> rabbit_misc:thunk('ok'). --spec combine_deletions(deletions(), deletions()) -> deletions(). --spec add_deletion - (rabbit_exchange:name(), - {'undefined' | rabbit_types:exchange(), - 'deleted' | 'not_deleted', - bindings()}, - deletions()) -> - deletions(). --spec new_deletions() -> deletions(). - %%---------------------------------------------------------------------------- -define(INFO_KEYS, [source_name, source_kind, @@ -110,6 +70,10 @@ vhost]). %% Global table recovery + +-spec recover([rabbit_exchange:name()], [rabbit_amqqueue:name()]) -> + 'ok'. + recover() -> rabbit_misc:table_filter( fun (Route) -> @@ -163,6 +127,8 @@ recover_semi_durable_route_txn(R = #route{binding = B}, X) -> (Serial, false) -> x_callback(Serial, X, add_binding, B) end). +-spec exists(rabbit_types:binding()) -> boolean() | bind_errors(). + exists(#binding{source = ?DEFAULT_EXCHANGE(_), destination = #resource{kind = queue, name = QName} = Queue, key = QName, @@ -177,8 +143,12 @@ exists(Binding) -> rabbit_misc:const(mnesia:read({rabbit_route, B}) /= []) end, fun not_found_or_absent_errs/1). +-spec add(rabbit_types:binding(), rabbit_types:username()) -> bind_res(). + add(Binding, ActingUser) -> add(Binding, fun (_Src, _Dst) -> ok end, ActingUser). +-spec add(rabbit_types:binding(), inner_fun(), rabbit_types:username()) -> bind_res(). + add(Binding, InnerFun, ActingUser) -> binding_action( Binding, @@ -223,8 +193,12 @@ add(Src, Dst, B, ActingUser) -> true -> rabbit_misc:const({error, binding_not_found}) end. +-spec remove(rabbit_types:binding()) -> bind_res(). + remove(Binding) -> remove(Binding, fun (_Src, _Dst) -> ok end, ?INTERNAL_USER). +-spec remove(rabbit_types:binding(), inner_fun(), rabbit_types:username()) -> bind_res(). + remove(Binding, InnerFun, ActingUser) -> binding_action( Binding, @@ -254,9 +228,13 @@ remove(Src, Dst, B, ActingUser) -> %% Implicit bindings are implicit as of rabbitmq/rabbitmq-server#1721. remove_default_exchange_binding_rows_of(Dst = #resource{}) -> - case rabbit_binding:implicit_for_destination(Dst) of + case implicit_for_destination(Dst) of [Binding] -> - mnesia:dirty_delete(rabbit_durable_route, Binding); + mnesia:dirty_delete(rabbit_durable_route, Binding), + mnesia:dirty_delete(rabbit_semi_durable_route, Binding), + mnesia:dirty_delete(rabbit_reverse_route, + reverse_binding(Binding)), + mnesia:dirty_delete(rabbit_route, Binding); _ -> %% no binding to remove or %% a competing tx has beaten us to it? @@ -264,6 +242,8 @@ remove_default_exchange_binding_rows_of(Dst = #resource{}) -> end, ok. +-spec list(rabbit_types:vhost()) -> bindings(). + list(VHostPath) -> VHostResource = rabbit_misc:r(VHostPath, '_'), Route = #route{binding = #binding{source = VHostResource, @@ -279,6 +259,9 @@ list(VHostPath) -> end, AllBindings), implicit_bindings(VHostPath) ++ Filtered. +-spec list_for_source + (rabbit_types:binding_source()) -> bindings(). + list_for_source(?DEFAULT_EXCHANGE(VHostPath)) -> implicit_bindings(VHostPath); list_for_source(SrcName) -> @@ -289,6 +272,9 @@ list_for_source(SrcName) -> <- mnesia:match_object(rabbit_route, Route, read)] end). +-spec list_for_destination + (rabbit_types:binding_destination()) -> bindings(). + list_for_destination(DstName) -> implicit_for_destination(DstName) ++ mnesia:async_dirty( @@ -310,8 +296,8 @@ implicit_bindings(VHostPath) -> || DstQueue = #resource{name = QName} <- DstQueues ]. implicit_for_destination(DstQueue = #resource{kind = queue, - virtual_host = VHostPath, - name = QName}) -> + virtual_host = VHostPath, + name = QName}) -> [#binding{source = ?DEFAULT_EXCHANGE(VHostPath), destination = DstQueue, key = QName, @@ -319,6 +305,10 @@ implicit_for_destination(DstQueue = #resource{kind = queue, implicit_for_destination(_) -> []. +-spec list_for_source_and_destination + (rabbit_types:binding_source(), rabbit_types:binding_destination()) -> + bindings(). + list_for_source_and_destination(?DEFAULT_EXCHANGE(VHostPath), #resource{kind = queue, virtual_host = VHostPath, @@ -337,6 +327,8 @@ list_for_source_and_destination(SrcName, DstName) -> Route, read)] end). +-spec info_keys() -> rabbit_types:info_keys(). + info_keys() -> ?INFO_KEYS. map(VHostPath, F) -> @@ -355,18 +347,33 @@ i(routing_key, #binding{key = RoutingKey}) -> RoutingKey; i(arguments, #binding{args = Arguments}) -> Arguments; i(Item, _) -> throw({bad_argument, Item}). +-spec info(rabbit_types:binding()) -> rabbit_types:infos(). + info(B = #binding{}) -> infos(?INFO_KEYS, B). +-spec info(rabbit_types:binding(), rabbit_types:info_keys()) -> + rabbit_types:infos(). + info(B = #binding{}, Items) -> infos(Items, B). +-spec info_all(rabbit_types:vhost()) -> [rabbit_types:infos()]. + info_all(VHostPath) -> map(VHostPath, fun (B) -> info(B) end). +-spec info_all(rabbit_types:vhost(), rabbit_types:info_keys()) -> + [rabbit_types:infos()]. + info_all(VHostPath, Items) -> map(VHostPath, fun (B) -> info(B, Items) end). +-spec info_all(rabbit_types:vhost(), rabbit_types:info_keys(), + reference(), pid()) -> 'ok'. + info_all(VHostPath, Items, Ref, AggregatorPid) -> rabbit_control_misc:emitting_map( AggregatorPid, Ref, fun(B) -> info(B, Items) end, list(VHostPath)). +-spec has_for_source(rabbit_types:binding_source()) -> boolean(). + has_for_source(SrcName) -> Match = #route{binding = #binding{source = SrcName, _ = '_'}}, %% we need to check for semi-durable routes (which subsumes @@ -376,6 +383,8 @@ has_for_source(SrcName) -> contains(rabbit_route, Match) orelse contains(rabbit_semi_durable_route, Match). +-spec remove_for_source(rabbit_types:binding_source()) -> bindings(). + remove_for_source(SrcName) -> lock_resource(SrcName), Match = #route{binding = #binding{source = SrcName, _ = '_'}}, @@ -384,16 +393,23 @@ remove_for_source(SrcName) -> mnesia:dirty_match_object(rabbit_route, Match) ++ mnesia:dirty_match_object(rabbit_semi_durable_route, Match))). +-spec remove_for_destination + (rabbit_types:binding_destination(), boolean()) -> deletions(). + remove_for_destination(DstName, OnlyDurable) -> remove_for_destination(DstName, OnlyDurable, fun remove_routes/1). +-spec remove_transient_for_destination + (rabbit_types:binding_destination()) -> deletions(). + remove_transient_for_destination(DstName) -> remove_for_destination(DstName, false, fun remove_transient_routes/1). %%---------------------------------------------------------------------------- durable(#exchange{durable = D}) -> D; -durable(#amqqueue{durable = D}) -> D. +durable(Q) when ?is_amqqueue(Q) -> + amqqueue:is_durable(Q). binding_action(Binding = #binding{source = SrcName, destination = DstName, @@ -591,12 +607,24 @@ anything_but( NotThis, NotThis, This) -> This; anything_but( NotThis, This, NotThis) -> This; anything_but(_NotThis, This, This) -> This. +-spec new_deletions() -> deletions(). + new_deletions() -> dict:new(). +-spec add_deletion + (rabbit_exchange:name(), + {'undefined' | rabbit_types:exchange(), + 'deleted' | 'not_deleted', + bindings()}, + deletions()) -> + deletions(). + add_deletion(XName, Entry, Deletions) -> dict:update(XName, fun (Entry1) -> merge_entry(Entry1, Entry) end, Entry, Deletions). +-spec combine_deletions(deletions(), deletions()) -> deletions(). + combine_deletions(Deletions1, Deletions2) -> dict:merge(fun (_XName, Entry1, Entry2) -> merge_entry(Entry1, Entry2) end, Deletions1, Deletions2). @@ -606,6 +634,8 @@ merge_entry({X1, Deleted1, Bindings1}, {X2, Deleted2, Bindings2}) -> anything_but(not_deleted, Deleted1, Deleted2), [Bindings1 | Bindings2]}. +-spec process_deletions(deletions(), rabbit_types:username()) -> rabbit_misc:thunk('ok'). + process_deletions(Deletions, ActingUser) -> AugmentedDeletions = dict:map(fun (_XName, {X, deleted, Bindings}) -> diff --git a/src/rabbit_channel.erl b/src/rabbit_channel.erl index 634789adab..ffd7c8fabc 100644 --- a/src/rabbit_channel.erl +++ b/src/rabbit_channel.erl @@ -51,6 +51,7 @@ -include_lib("rabbit_common/include/rabbit_framing.hrl"). -include_lib("rabbit_common/include/rabbit.hrl"). +-include("amqqueue.hrl"). -behaviour(gen_server2). @@ -61,10 +62,14 @@ emit_info_all/4, info_local/1]). -export([refresh_config_local/0, ready_for_close/1]). -export([refresh_interceptors/0]). +-export([force_event_refresh/1]). -export([init/1, terminate/2, code_change/3, handle_call/3, handle_cast/2, handle_info/2, handle_pre_hibernate/1, prioritise_call/4, prioritise_cast/3, prioritise_info/3, format_message_queue/2]). + +-deprecated([{force_event_refresh, 1, eventually}]). + %% Internal -export([list_local/0, emit_info_local/3, deliver_reply_local/3]). -export([get_vhost/1, get_user/1]). @@ -220,40 +225,13 @@ -type channel() :: #ch{}. +%%---------------------------------------------------------------------------- + -spec start_link (channel_number(), pid(), pid(), pid(), string(), rabbit_types:protocol(), rabbit_types:user(), rabbit_types:vhost(), rabbit_framing:amqp_table(), pid(), pid()) -> rabbit_types:ok_pid_or_error(). --spec do(pid(), rabbit_framing:amqp_method_record()) -> 'ok'. --spec do - (pid(), rabbit_framing:amqp_method_record(), - rabbit_types:maybe(rabbit_types:content())) -> - 'ok'. --spec do_flow - (pid(), rabbit_framing:amqp_method_record(), - rabbit_types:maybe(rabbit_types:content())) -> - 'ok'. --spec flush(pid()) -> 'ok'. --spec shutdown(pid()) -> 'ok'. --spec send_command(pid(), rabbit_framing:amqp_method_record()) -> 'ok'. --spec deliver - (pid(), rabbit_types:ctag(), boolean(), rabbit_amqqueue:qmsg()) -> 'ok'. --spec deliver_reply(binary(), rabbit_types:delivery()) -> 'ok'. --spec deliver_reply_local(pid(), binary(), rabbit_types:delivery()) -> 'ok'. --spec send_credit_reply(pid(), non_neg_integer()) -> 'ok'. --spec send_drained(pid(), [{rabbit_types:ctag(), non_neg_integer()}]) -> 'ok'. --spec list() -> [pid()]. --spec list_local() -> [pid()]. --spec info_keys() -> rabbit_types:info_keys(). --spec info(pid()) -> rabbit_types:infos(). --spec info(pid(), rabbit_types:info_keys()) -> rabbit_types:infos(). --spec info_all() -> [rabbit_types:infos()]. --spec info_all(rabbit_types:info_keys()) -> [rabbit_types:infos()]. --spec refresh_config_local() -> 'ok'. --spec ready_for_close(pid()) -> 'ok'. - -%%---------------------------------------------------------------------------- start_link(Channel, ReaderPid, WriterPid, ConnPid, ConnName, Protocol, User, VHost, Capabilities, CollectorPid, Limiter) -> @@ -261,27 +239,50 @@ start_link(Channel, ReaderPid, WriterPid, ConnPid, ConnName, Protocol, User, ?MODULE, [Channel, ReaderPid, WriterPid, ConnPid, ConnName, Protocol, User, VHost, Capabilities, CollectorPid, Limiter], []). +-spec do(pid(), rabbit_framing:amqp_method_record()) -> 'ok'. + do(Pid, Method) -> rabbit_channel_common:do(Pid, Method). +-spec do + (pid(), rabbit_framing:amqp_method_record(), + rabbit_types:maybe(rabbit_types:content())) -> + 'ok'. + do(Pid, Method, Content) -> rabbit_channel_common:do(Pid, Method, Content). +-spec do_flow + (pid(), rabbit_framing:amqp_method_record(), + rabbit_types:maybe(rabbit_types:content())) -> + 'ok'. + do_flow(Pid, Method, Content) -> rabbit_channel_common:do_flow(Pid, Method, Content). +-spec flush(pid()) -> 'ok'. + flush(Pid) -> gen_server2:call(Pid, flush, infinity). +-spec shutdown(pid()) -> 'ok'. + shutdown(Pid) -> gen_server2:cast(Pid, terminate). +-spec send_command(pid(), rabbit_framing:amqp_method_record()) -> 'ok'. + send_command(Pid, Msg) -> gen_server2:cast(Pid, {command, Msg}). +-spec deliver + (pid(), rabbit_types:ctag(), boolean(), rabbit_amqqueue:qmsg()) -> 'ok'. + deliver(Pid, ConsumerTag, AckRequired, Msg) -> gen_server2:cast(Pid, {deliver, ConsumerTag, AckRequired, Msg}). +-spec deliver_reply(binary(), rabbit_types:delivery()) -> 'ok'. + deliver_reply(<<"amq.rabbitmq.reply-to.", Rest/binary>>, Delivery) -> case decode_fast_reply_to(Rest) of {ok, Pid, Key} -> @@ -293,6 +294,9 @@ deliver_reply(<<"amq.rabbitmq.reply-to.", Rest/binary>>, Delivery) -> %% We want to ensure people can't use this mechanism to send a message %% to an arbitrary process and kill it! + +-spec deliver_reply_local(pid(), binary(), rabbit_types:delivery()) -> 'ok'. + deliver_reply_local(Pid, Key, Delivery) -> case pg_local:in_group(rabbit_channels, Pid) of true -> gen_server2:cast(Pid, {deliver_reply, Key, Delivery}); @@ -321,21 +325,33 @@ decode_fast_reply_to(Rest) -> _ -> error end. +-spec send_credit_reply(pid(), non_neg_integer()) -> 'ok'. + send_credit_reply(Pid, Len) -> gen_server2:cast(Pid, {send_credit_reply, Len}). +-spec send_drained(pid(), [{rabbit_types:ctag(), non_neg_integer()}]) -> 'ok'. + send_drained(Pid, CTagCredit) -> gen_server2:cast(Pid, {send_drained, CTagCredit}). +-spec list() -> [pid()]. + list() -> rabbit_misc:append_rpc_all_nodes(rabbit_mnesia:cluster_nodes(running), rabbit_channel, list_local, []). +-spec list_local() -> [pid()]. + list_local() -> pg_local:get_members(rabbit_channels). +-spec info_keys() -> rabbit_types:info_keys(). + info_keys() -> ?INFO_KEYS. +-spec info(pid()) -> rabbit_types:infos(). + info(Pid) -> {Timeout, Deadline} = get_operation_timeout_and_deadline(), try @@ -349,6 +365,8 @@ info(Pid) -> throw(timeout) end. +-spec info(pid(), rabbit_types:info_keys()) -> rabbit_types:infos(). + info(Pid, Items) -> {Timeout, Deadline} = get_operation_timeout_and_deadline(), try @@ -362,9 +380,13 @@ info(Pid, Items) -> throw(timeout) end. +-spec info_all() -> [rabbit_types:infos()]. + info_all() -> rabbit_misc:filter_exit_map(fun (C) -> info(C) end, list()). +-spec info_all(rabbit_types:info_keys()) -> [rabbit_types:infos()]. + info_all(Items) -> rabbit_misc:filter_exit_map(fun (C) -> info(C, Items) end, list()). @@ -382,6 +404,8 @@ emit_info(PidList, InfoItems, Ref, AggregatorPid) -> rabbit_control_misc:emitting_map_with_exit_handler( AggregatorPid, Ref, fun(C) -> info(C, InfoItems) end, PidList). +-spec refresh_config_local() -> 'ok'. + refresh_config_local() -> rabbit_misc:upmap( fun (C) -> @@ -410,9 +434,17 @@ refresh_interceptors() -> list_local()), ok. +-spec ready_for_close(pid()) -> 'ok'. + ready_for_close(Pid) -> rabbit_channel_common:ready_for_close(Pid). +-spec force_event_refresh(reference()) -> 'ok'. + +force_event_refresh(Ref) -> + [gen_server2:cast(C, {force_event_refresh, Ref}) || C <- list()], + ok. + list_queue_states(Pid) -> gen_server2:call(Pid, list_queue_states). @@ -630,6 +662,21 @@ handle_cast({send_drained, CTagCredit}, State = #ch{writer_pid = WriterPid}) -> || {ConsumerTag, CreditDrained} <- CTagCredit], noreply(State); +handle_cast({force_event_refresh, Ref}, State) -> + rabbit_event:notify(channel_created, infos(?CREATION_EVENT_KEYS, State), + Ref), + noreply(rabbit_event:init_stats_timer(State, #ch.stats_timer)); + +handle_cast({mandatory_received, _MsgSeqNo}, State) -> + %% This feature was used by `rabbit_amqqueue_process` and + %% `rabbit_mirror_queue_slave` up-to and including RabbitMQ 3.7.x. + %% It is unused in 3.8.x and thus deprecated. We keep it to support + %% in-place upgrades to 3.8.x (i.e. mixed-version clusters), but it + %% is a no-op starting with that version. + %% + %% NB: don't call noreply/1 since we don't want to send confirms. + noreply_coalesce(State); + handle_cast({reject_publish, MsgSeqNo, _QPid}, State = #ch{unconfirmed = UC}) -> %% It does not matter which queue rejected the message, %% if any queue rejected it - it should not be confirmed. @@ -1366,7 +1413,8 @@ handle_method(#'basic.cancel'{consumer_tag = ConsumerTag, nowait = NoWait}, error -> %% Spec requires we ignore this situation. return_ok(State, NoWait, OkMsg); - {ok, {Q = #amqqueue{pid = QPid}, _CParams}} -> + {ok, {Q, _CParams}} when ?is_amqqueue(Q) -> + QPid = amqqueue:get_pid(Q), ConsumerMapping1 = maps:remove(ConsumerTag, ConsumerMapping), QRef = qpid_to_ref(QPid), QCons1 = @@ -1636,7 +1684,9 @@ basic_consume(QueueName, NoAck, ConsumerPrefetch, ActualConsumerTag, Username, QueueStates0), Q} end) of - {{ok, QueueStates}, Q = #amqqueue{pid = QPid, name = QName}} -> + {{ok, QueueStates}, Q} when ?is_amqqueue(Q) -> + QPid = amqqueue:get_pid(Q), + QName = amqqueue:get_name(Q), CM1 = maps:put( ActualConsumerTag, {Q, {NoAck, ConsumerPrefetch, ExclusiveConsume, Args}}, @@ -1649,7 +1699,9 @@ basic_consume(QueueName, NoAck, ConsumerPrefetch, ActualConsumerTag, true -> consumer_monitor(ActualConsumerTag, State1); false -> State1 end}; - {ok, Q = #amqqueue{pid = QPid, name = QName}} -> + {ok, Q} when ?is_amqqueue(Q) -> + QPid = amqqueue:get_pid(Q), + QName = amqqueue:get_name(Q), CM1 = maps:put( ActualConsumerTag, {Q, {NoAck, ConsumerPrefetch, ExclusiveConsume, Args}}, @@ -1674,7 +1726,8 @@ consumer_monitor(ConsumerTag, State = #ch{consumer_mapping = ConsumerMapping, queue_monitors = QMons, queue_consumers = QCons}) -> - {#amqqueue{pid = QPid}, _} = maps:get(ConsumerTag, ConsumerMapping), + {Q, _} = maps:get(ConsumerTag, ConsumerMapping), + QPid = amqqueue:get_pid(Q), QRef = qpid_to_ref(QPid), CTags1 = case maps:find(QRef, QCons) of {ok, CTags} -> gb_sets:insert(ConsumerTag, CTags); @@ -1782,7 +1835,7 @@ binding_action(Fun, SourceNameBin0, DestinationType, DestinationNameBin0, destination = DestinationName, key = RoutingKey, args = Arguments}, - fun (_X, Q = #amqqueue{}) -> + fun (_X, Q) when ?is_amqqueue(Q) -> try rabbit_amqqueue:check_exclusive_access(Q, ConnPid) catch exit:Reason -> {error, Reason} end; @@ -1791,9 +1844,9 @@ binding_action(Fun, SourceNameBin0, DestinationType, DestinationNameBin0, end, Username) of {error, {resources_missing, [{not_found, Name} | _]}} -> - rabbit_misc:not_found(Name); + rabbit_amqqueue:not_found(Name); {error, {resources_missing, [{absent, Q, Reason} | _]}} -> - rabbit_misc:absent(Q, Reason); + rabbit_amqqueue:absent(Q, Reason); {error, binding_not_found} -> rabbit_misc:protocol_error( not_found, "no binding ~s between ~s and ~s", @@ -1956,8 +2009,9 @@ foreach_per_queue(F, UAL, Acc) -> rabbit_misc:gb_trees_fold(fun (Key, Val, Acc0) -> F(Key, Val, Acc0) end, Acc, T). consumer_queue_refs(Consumers) -> - lists:usort([qpid_to_ref(QPid) || {_Key, {#amqqueue{pid = QPid}, _CParams}} - <- maps:to_list(Consumers)]). + lists:usort([qpid_to_ref(amqqueue:get_pid(Q)) + || {_Key, {Q, _CParams}} <- maps:to_list(Consumers), + amqqueue:is_amqqueue(Q)]). %% tell the limiter about the number of acks that have been received %% for messages delivered to subscribed consumers, but not acks for @@ -2011,9 +2065,10 @@ deliver_to_queues({Delivery = #delivery{message = Message = #basic_message{ %% since alternative algorithms to update queue_names less %% frequently would in fact be more expensive in the common case. {QNames1, QMons1} = - lists:foldl(fun (#amqqueue{pid = QPid, name = QName}, - {QNames0, QMons0}) -> + lists:foldl(fun (Q, {QNames0, QMons0}) when ?is_amqqueue(Q) -> + QPid = amqqueue:get_pid(Q), QRef = qpid_to_ref(QPid), + QName = amqqueue:get_name(Q), {case maps:is_key(QRef, QNames0) of true -> QNames0; false -> maps:put(QRef, QName, QNames0) @@ -2287,7 +2342,7 @@ handle_method(#'queue.declare'{queue = <<"amq.rabbitmq.reply-to", QueueName = rabbit_misc:r(VHost, queue, StrippedQueueNameBin), case declare_fast_reply_to(StrippedQueueNameBin) of exists -> {ok, QueueName, 0, 1}; - not_found -> rabbit_misc:not_found(QueueName) + not_found -> rabbit_amqqueue:not_found(QueueName) end; handle_method(#'queue.declare'{queue = QueueNameBin, passive = false, @@ -2338,11 +2393,12 @@ handle_method(#'queue.declare'{queue = QueueNameBin, end, case rabbit_amqqueue:declare(QueueName, Durable, AutoDelete, Args, Owner, Username) of - {new, #amqqueue{pid = QPid}} -> + {new, Q} when ?is_amqqueue(Q) -> %% We need to notify the reader within the channel %% process so that we can be sure there are no %% outstanding exclusive queues being declared as %% the connection shuts down. + QPid = amqqueue:get_pid(Q), ok = case {Owner, CollectorPid} of {none, _} -> ok; {_, none} -> ok; %% Supports call from mgmt API @@ -2357,7 +2413,7 @@ handle_method(#'queue.declare'{queue = QueueNameBin, handle_method(Declare, ConnPid, CollectorPid, VHostPath, User); {absent, Q, Reason} -> - rabbit_misc:absent(Q, Reason); + rabbit_amqqueue:absent(Q, Reason); {owner_died, _Q} -> %% Presumably our own days are numbered since the %% connection has died. Pretend the queue exists though, @@ -2365,7 +2421,7 @@ handle_method(#'queue.declare'{queue = QueueNameBin, {ok, QueueName, 0, 0} end; {error, {absent, Q, Reason}} -> - rabbit_misc:absent(Q, Reason) + rabbit_amqqueue:absent(Q, Reason) end; handle_method(#'queue.declare'{queue = QueueNameBin, nowait = NoWait, @@ -2373,9 +2429,12 @@ handle_method(#'queue.declare'{queue = QueueNameBin, ConnPid, _CollectorPid, VHostPath, _User) -> StrippedQueueNameBin = strip_cr_lf(QueueNameBin), QueueName = rabbit_misc:r(VHostPath, queue, StrippedQueueNameBin), - {{ok, MessageCount, ConsumerCount}, #amqqueue{} = Q} = - rabbit_amqqueue:with_or_die( - QueueName, fun (Q) -> {maybe_stat(NoWait, Q), Q} end), + Fun = fun (Q0) -> + QStat = maybe_stat(NoWait, Q0), + {QStat, Q0} + end, + %% Note: no need to check if Q is an #amqqueue, with_or_die does it + {{ok, MessageCount, ConsumerCount}, Q} = rabbit_amqqueue:with_or_die(QueueName, Fun), ok = rabbit_amqqueue:check_exclusive_access(Q, ConnPid), {ok, QueueName, MessageCount, ConsumerCount}; handle_method(#'queue.delete'{queue = QueueNameBin, @@ -2399,7 +2458,7 @@ handle_method(#'queue.delete'{queue = QueueNameBin, {ok, 0}; ({absent, Q, stopped}) -> rabbit_amqqueue:delete_crashed(Q, Username), {ok, 0}; - ({absent, Q, Reason}) -> rabbit_misc:absent(Q, Reason) + ({absent, Q, Reason}) -> rabbit_amqqueue:absent(Q, Reason) end) of {error, in_use} -> precondition_failed("~s in use", [rabbit_misc:rs(QueueName)]); diff --git a/src/rabbit_channel_sup.erl b/src/rabbit_channel_sup.erl index e2e24e2f38..86a0f15650 100644 --- a/src/rabbit_channel_sup.erl +++ b/src/rabbit_channel_sup.erl @@ -47,12 +47,12 @@ rabbit_types:protocol(), rabbit_types:user(), rabbit_types:vhost(), rabbit_framing:amqp_table(), pid()}. --spec start_link(start_link_args()) -> {'ok', pid(), {pid(), any()}}. - -define(FAIR_WAIT, 70000). %%---------------------------------------------------------------------------- +-spec start_link(start_link_args()) -> {'ok', pid(), {pid(), any()}}. + start_link({tcp, Sock, Channel, FrameMax, ReaderPid, ConnName, Protocol, User, VHost, Capabilities, Collector}) -> {ok, SupPid} = supervisor2:start_link( diff --git a/src/rabbit_channel_sup_sup.erl b/src/rabbit_channel_sup_sup.erl index d39e3f3e9b..b813b42e89 100644 --- a/src/rabbit_channel_sup_sup.erl +++ b/src/rabbit_channel_sup_sup.erl @@ -32,14 +32,13 @@ %%---------------------------------------------------------------------------- -spec start_link() -> rabbit_types:ok_pid_or_error(). --spec start_channel(pid(), rabbit_channel_sup:start_link_args()) -> - {'ok', pid(), {pid(), any()}}. - -%%---------------------------------------------------------------------------- start_link() -> supervisor2:start_link(?MODULE, []). +-spec start_channel(pid(), rabbit_channel_sup:start_link_args()) -> + {'ok', pid(), {pid(), any()}}. + start_channel(Pid, Args) -> supervisor2:start_child(Pid, [Args]). diff --git a/src/rabbit_client_sup.erl b/src/rabbit_client_sup.erl index 982c1d1e62..8b49244982 100644 --- a/src/rabbit_client_sup.erl +++ b/src/rabbit_client_sup.erl @@ -28,19 +28,19 @@ -spec start_link(rabbit_types:mfargs()) -> rabbit_types:ok_pid_or_error(). --spec start_link({'local', atom()}, rabbit_types:mfargs()) -> - rabbit_types:ok_pid_or_error(). --spec start_link_worker({'local', atom()}, rabbit_types:mfargs()) -> - rabbit_types:ok_pid_or_error(). - -%%---------------------------------------------------------------------------- start_link(Callback) -> supervisor2:start_link(?MODULE, Callback). +-spec start_link({'local', atom()}, rabbit_types:mfargs()) -> + rabbit_types:ok_pid_or_error(). + start_link(SupName, Callback) -> supervisor2:start_link(SupName, ?MODULE, Callback). +-spec start_link_worker({'local', atom()}, rabbit_types:mfargs()) -> + rabbit_types:ok_pid_or_error(). + start_link_worker(SupName, Callback) -> supervisor2:start_link(SupName, ?MODULE, {Callback, worker}). diff --git a/src/rabbit_connection_helper_sup.erl b/src/rabbit_connection_helper_sup.erl index 8c4ba88da2..c5a0825d83 100644 --- a/src/rabbit_connection_helper_sup.erl +++ b/src/rabbit_connection_helper_sup.erl @@ -38,21 +38,21 @@ %%---------------------------------------------------------------------------- -spec start_link() -> rabbit_types:ok_pid_or_error(). --spec start_channel_sup_sup(pid()) -> rabbit_types:ok_pid_or_error(). --spec start_queue_collector(pid(), rabbit_types:proc_name()) -> - rabbit_types:ok_pid_or_error(). - -%%---------------------------------------------------------------------------- start_link() -> supervisor2:start_link(?MODULE, []). +-spec start_channel_sup_sup(pid()) -> rabbit_types:ok_pid_or_error(). + start_channel_sup_sup(SupPid) -> supervisor2:start_child( SupPid, {channel_sup_sup, {rabbit_channel_sup_sup, start_link, []}, intrinsic, infinity, supervisor, [rabbit_channel_sup_sup]}). +-spec start_queue_collector(pid(), rabbit_types:proc_name()) -> + rabbit_types:ok_pid_or_error(). + start_queue_collector(SupPid, Identity) -> supervisor2:start_child( SupPid, diff --git a/src/rabbit_connection_sup.erl b/src/rabbit_connection_sup.erl index 6e85514d44..51661dd8b6 100644 --- a/src/rabbit_connection_sup.erl +++ b/src/rabbit_connection_sup.erl @@ -38,9 +38,6 @@ -spec start_link(any(), rabbit_net:socket(), module(), any()) -> {'ok', pid(), pid()}. --spec reader(pid()) -> pid(). - -%%-------------------------------------------------------------------------- start_link(Ref, _Sock, _Transport, _Opts) -> {ok, SupPid} = supervisor2:start_link(?MODULE, []), @@ -66,6 +63,8 @@ start_link(Ref, _Sock, _Transport, _Opts) -> intrinsic, ?WORKER_WAIT, worker, [rabbit_reader]}), {ok, SupPid, ReaderPid}. +-spec reader(pid()) -> pid(). + reader(Pid) -> hd(supervisor2:find_child(Pid, reader)). diff --git a/src/rabbit_core_ff.erl b/src/rabbit_core_ff.erl new file mode 100644 index 0000000000..a13099e304 --- /dev/null +++ b/src/rabbit_core_ff.erl @@ -0,0 +1,97 @@ +%% The contents of this file are subject to the Mozilla Public License +%% Version 1.1 (the "License"); you may not use this file except in +%% compliance with the License. You may obtain a copy of the License +%% at http://www.mozilla.org/MPL/ +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and +%% limitations under the License. +%% +%% The Original Code is RabbitMQ. +%% +%% The Initial Developer of the Original Code is GoPivotal, Inc. +%% Copyright (c) 2018 Pivotal Software, Inc. All rights reserved. +%% + +-module(rabbit_core_ff). + +-export([quorum_queue_migration/3, + implicit_default_bindings_migration/3]). + +-rabbit_feature_flag( + {quorum_queue, + #{desc => "Support queues of type `quorum`", + doc_url => "http://www.rabbitmq.com/quorum-queues.html", + stability => stable, + migration_fun => {?MODULE, quorum_queue_migration} + }}). + +-rabbit_feature_flag( + {implicit_default_bindings, + #{desc => "Default bindings are now implicit, instead of " + "being stored in the database", + stability => stable, + migration_fun => {?MODULE, implicit_default_bindings_migration} + }}). + +%% ------------------------------------------------------------------- +%% Quorum queues. +%% ------------------------------------------------------------------- + +-define(quorum_queue_tables, [rabbit_queue, + rabbit_durable_queue]). + +quorum_queue_migration(FeatureName, _FeatureProps, enable) -> + Tables = ?quorum_queue_tables, + rabbit_table:wait(Tables), + Fields = amqqueue:fields(amqqueue_v2), + migrate_to_amqqueue_with_type(FeatureName, Tables, Fields); +quorum_queue_migration(_FeatureName, _FeatureProps, is_enabled) -> + Tables = ?quorum_queue_tables, + rabbit_table:wait(Tables), + Fields = amqqueue:fields(amqqueue_v2), + mnesia:table_info(rabbit_queue, attributes) =:= Fields andalso + mnesia:table_info(rabbit_durable_queue, attributes) =:= Fields. + +migrate_to_amqqueue_with_type(FeatureName, [Table | Rest], Fields) -> + rabbit_log:info("Feature flag `~s`: migrating Mnesia table ~s...", + [FeatureName, Table]), + Fun = fun(Queue) -> amqqueue:upgrade_to(amqqueue_v2, Queue) end, + case mnesia:transform_table(Table, Fun, Fields) of + {atomic, ok} -> migrate_to_amqqueue_with_type(FeatureName, + Rest, + Fields); + {aborted, Reason} -> {error, Reason} + end; +migrate_to_amqqueue_with_type(FeatureName, [], _) -> + rabbit_log:info("Feature flag `~s`: Mnesia tables migration done", + [FeatureName]), + ok. + +%% ------------------------------------------------------------------- +%% Default bindings. +%% ------------------------------------------------------------------- + +implicit_default_bindings_migration(FeatureName, _FeatureProps, + enable) -> + %% Default exchange bindings are now implicit (not stored in the + %% route tables). It should be safe to remove them outside of a + %% transaction. + rabbit_table:wait([rabbit_queue]), + Queues = mnesia:dirty_all_keys(rabbit_queue), + remove_explicit_default_bindings(FeatureName, Queues); +implicit_default_bindings_migration(_Feature_Name, _FeatureProps, + is_enabled) -> + undefined. + +remove_explicit_default_bindings(_FeatureName, []) -> + ok; +remove_explicit_default_bindings(FeatureName, Queues) -> + rabbit_log:info("Feature flag `~s`: deleting explicit " + "default bindings for ~b queues " + "(it may take some time)...", + [FeatureName, length(Queues)]), + [rabbit_binding:remove_default_exchange_binding_rows_of(Q) + || Q <- Queues], + ok. diff --git a/src/rabbit_core_metrics_gc.erl b/src/rabbit_core_metrics_gc.erl index d4c065e64f..586913ea2c 100644 --- a/src/rabbit_core_metrics_gc.erl +++ b/src/rabbit_core_metrics_gc.erl @@ -19,12 +19,12 @@ interval }). --spec start_link() -> rabbit_types:ok_pid_or_error(). - -export([start_link/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-spec start_link() -> rabbit_types:ok_pid_or_error(). + start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). diff --git a/src/rabbit_credential_validation.erl b/src/rabbit_credential_validation.erl index 4b24d35f9d..ec0dea7893 100644 --- a/src/rabbit_credential_validation.erl +++ b/src/rabbit_credential_validation.erl @@ -27,8 +27,6 @@ -export([validate/2, backend/0]). --spec validate(rabbit_types:username(), rabbit_types:password()) -> 'ok' | {'error', string()}. - %% Validates a username/password pair by delegating to the effective %% `rabbit_credential_validator`. Used by `rabbit_auth_backend_internal`. %% Note that some validators may choose to only validate passwords. @@ -38,6 +36,8 @@ %% * ok: provided credentials passed validation. %% * {error, Error, Args}: provided password password failed validation. +-spec validate(rabbit_types:username(), rabbit_types:password()) -> 'ok' | {'error', string()}. + validate(Username, Password) -> Backend = backend(), Backend:validate(Username, Password). diff --git a/src/rabbit_dead_letter.erl b/src/rabbit_dead_letter.erl index 71ad8814b1..e26ea8297b 100644 --- a/src/rabbit_dead_letter.erl +++ b/src/rabbit_dead_letter.erl @@ -25,11 +25,11 @@ -type reason() :: 'expired' | 'rejected' | 'maxlen'. +%%---------------------------------------------------------------------------- + -spec publish(rabbit_types:message(), reason(), rabbit_types:exchange(), 'undefined' | binary(), rabbit_amqqueue:name()) -> 'ok'. -%%---------------------------------------------------------------------------- - publish(Msg, Reason, X, RK, QName) -> DLMsg = make_msg(Msg, Reason, X#exchange.name, RK, QName), Delivery = rabbit_basic:delivery(false, false, DLMsg, undefined), diff --git a/src/rabbit_direct.erl b/src/rabbit_direct.erl index f45ba3b1ca..50e8f3d2b0 100644 --- a/src/rabbit_direct.erl +++ b/src/rabbit_direct.erl @@ -16,8 +16,11 @@ -module(rabbit_direct). --export([boot/0, list/0, connect/5, +-export([boot/0, force_event_refresh/1, list/0, connect/5, start_channel/9, disconnect/2]). + +-deprecated([{force_event_refresh, 1, eventually}]). + %% Internal -export([list_local/0]). @@ -29,34 +32,25 @@ %%---------------------------------------------------------------------------- -spec boot() -> 'ok'. --spec list() -> [pid()]. --spec list_local() -> [pid()]. --spec connect - (({'none', 'none'} | {rabbit_types:username(), 'none'} | - {rabbit_types:username(), rabbit_types:password()}), - rabbit_types:vhost(), rabbit_types:protocol(), pid(), - rabbit_event:event_props()) -> - rabbit_types:ok_or_error2( - {rabbit_types:user(), rabbit_framing:amqp_table()}, - 'broker_not_found_on_node' | - {'auth_failure', string()} | 'access_refused'). --spec start_channel - (rabbit_channel:channel_number(), pid(), pid(), string(), - rabbit_types:protocol(), rabbit_types:user(), rabbit_types:vhost(), - rabbit_framing:amqp_table(), pid()) -> - {'ok', pid()}. --spec disconnect(pid(), rabbit_event:event_props()) -> 'ok'. - -%%---------------------------------------------------------------------------- boot() -> rabbit_sup:start_supervisor_child( rabbit_direct_client_sup, rabbit_client_sup, [{local, rabbit_direct_client_sup}, {rabbit_channel_sup, start_link, []}]). +-spec force_event_refresh(reference()) -> 'ok'. + +force_event_refresh(Ref) -> + [Pid ! {force_event_refresh, Ref} || Pid <- list()], + ok. + +-spec list_local() -> [pid()]. + list_local() -> pg_local:get_members(rabbit_direct). +-spec list() -> [pid()]. + list() -> rabbit_misc:append_rpc_all_nodes(rabbit_mnesia:cluster_nodes(running), rabbit_direct, list_local, []). @@ -76,6 +70,16 @@ auth_fun({Username, Password}, VHost, ExtraAuthProps) -> [{password, Password}, {vhost, VHost}] ++ ExtraAuthProps) end. +-spec connect + (({'none', 'none'} | {rabbit_types:username(), 'none'} | + {rabbit_types:username(), rabbit_types:password()}), + rabbit_types:vhost(), rabbit_types:protocol(), pid(), + rabbit_event:event_props()) -> + rabbit_types:ok_or_error2( + {rabbit_types:user(), rabbit_framing:amqp_table()}, + 'broker_not_found_on_node' | + {'auth_failure', string()} | 'access_refused'). + connect(Creds, VHost, Protocol, Pid, Infos) -> ExtraAuthProps = extract_extra_auth_props(Creds, VHost, Pid, Infos), AuthFun = auth_fun(Creds, VHost, ExtraAuthProps), @@ -194,6 +198,12 @@ connect1(User, VHost, Protocol, Pid, Infos) -> {error, Reason} end. +-spec start_channel + (rabbit_channel:channel_number(), pid(), pid(), string(), + rabbit_types:protocol(), rabbit_types:user(), rabbit_types:vhost(), + rabbit_framing:amqp_table(), pid()) -> + {'ok', pid()}. + start_channel(Number, ClientChannelPid, ConnPid, ConnName, Protocol, User, VHost, Capabilities, Collector) -> {ok, _, {ChannelPid, _}} = @@ -203,6 +213,8 @@ start_channel(Number, ClientChannelPid, ConnPid, ConnName, Protocol, User, User, VHost, Capabilities, Collector}]), {ok, ChannelPid}. +-spec disconnect(pid(), rabbit_event:event_props()) -> 'ok'. + disconnect(Pid, Infos) -> pg_local:leave(rabbit_direct, Pid), rabbit_core_metrics:connection_closed(Pid), diff --git a/src/rabbit_disk_monitor.erl b/src/rabbit_disk_monitor.erl index 3ba1f35a6b..f48b0001fb 100644 --- a/src/rabbit_disk_monitor.erl +++ b/src/rabbit_disk_monitor.erl @@ -76,37 +76,43 @@ %%---------------------------------------------------------------------------- -type disk_free_limit() :: (integer() | string() | {'mem_relative', float() | integer()}). --spec start_link(disk_free_limit()) -> rabbit_types:ok_pid_or_error(). --spec get_disk_free_limit() -> integer(). --spec set_disk_free_limit(disk_free_limit()) -> 'ok'. --spec get_min_check_interval() -> integer(). --spec set_min_check_interval(integer()) -> 'ok'. --spec get_max_check_interval() -> integer(). --spec set_max_check_interval(integer()) -> 'ok'. --spec get_disk_free() -> (integer() | 'unknown'). %%---------------------------------------------------------------------------- %% Public API %%---------------------------------------------------------------------------- +-spec get_disk_free_limit() -> integer(). + get_disk_free_limit() -> gen_server:call(?MODULE, get_disk_free_limit, infinity). +-spec set_disk_free_limit(disk_free_limit()) -> 'ok'. + set_disk_free_limit(Limit) -> gen_server:call(?MODULE, {set_disk_free_limit, Limit}, infinity). +-spec get_min_check_interval() -> integer(). + get_min_check_interval() -> gen_server:call(?MODULE, get_min_check_interval, infinity). +-spec set_min_check_interval(integer()) -> 'ok'. + set_min_check_interval(Interval) -> gen_server:call(?MODULE, {set_min_check_interval, Interval}, infinity). +-spec get_max_check_interval() -> integer(). + get_max_check_interval() -> gen_server:call(?MODULE, get_max_check_interval, infinity). +-spec set_max_check_interval(integer()) -> 'ok'. + set_max_check_interval(Interval) -> gen_server:call(?MODULE, {set_max_check_interval, Interval}, infinity). +-spec get_disk_free() -> (integer() | 'unknown'). + get_disk_free() -> gen_server:call(?MODULE, get_disk_free, infinity). @@ -114,6 +120,8 @@ get_disk_free() -> %% gen_server callbacks %%---------------------------------------------------------------------------- +-spec start_link(disk_free_limit()) -> rabbit_types:ok_pid_or_error(). + start_link(Args) -> gen_server:start_link({local, ?SERVER}, ?MODULE, [Args], []). diff --git a/src/rabbit_epmd_monitor.erl b/src/rabbit_epmd_monitor.erl index 15c7489e99..fb13f61ea3 100644 --- a/src/rabbit_epmd_monitor.erl +++ b/src/rabbit_epmd_monitor.erl @@ -29,10 +29,6 @@ -define(CHECK_FREQUENCY, 60000). %%---------------------------------------------------------------------------- - --spec start_link() -> rabbit_types:ok_pid_or_error(). - -%%---------------------------------------------------------------------------- %% It's possible for epmd to be killed out from underneath us. If that %% happens, then obviously clustering and rabbitmqctl stop %% working. This process checks up on epmd and restarts it / @@ -48,6 +44,8 @@ %% epmd" as a shutdown or uninstall step. %% ---------------------------------------------------------------------------- +-spec start_link() -> rabbit_types:ok_pid_or_error(). + start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). diff --git a/src/rabbit_exchange.erl b/src/rabbit_exchange.erl index 9c879ad041..1b262e9a48 100644 --- a/src/rabbit_exchange.erl +++ b/src/rabbit_exchange.erl @@ -36,77 +36,13 @@ -type type() :: atom(). -type fun_name() :: atom(). --spec recover(rabbit_types:vhost()) -> [name()]. --spec callback - (rabbit_types:exchange(), fun_name(), - fun((boolean()) -> non_neg_integer()) | atom(), [any()]) -> 'ok'. --spec policy_changed - (rabbit_types:exchange(), rabbit_types:exchange()) -> 'ok'. --spec declare - (name(), type(), boolean(), boolean(), boolean(), - rabbit_framing:amqp_table(), rabbit_types:username()) - -> rabbit_types:exchange(). --spec check_type - (binary()) -> atom() | rabbit_types:connection_exit(). --spec assert_equivalence - (rabbit_types:exchange(), atom(), boolean(), boolean(), boolean(), - rabbit_framing:amqp_table()) - -> 'ok' | rabbit_types:connection_exit(). --spec assert_args_equivalence - (rabbit_types:exchange(), rabbit_framing:amqp_table()) - -> 'ok' | rabbit_types:connection_exit(). --spec lookup - (name()) -> rabbit_types:ok(rabbit_types:exchange()) | - rabbit_types:error('not_found'). --spec lookup_or_die - (name()) -> rabbit_types:exchange() | - rabbit_types:channel_exit(). --spec list() -> [rabbit_types:exchange()]. --spec list_names() -> [rabbit_exchange:name()]. --spec list(rabbit_types:vhost()) -> [rabbit_types:exchange()]. --spec lookup_scratch(name(), atom()) -> - rabbit_types:ok(term()) | - rabbit_types:error('not_found'). --spec update_scratch(name(), atom(), fun((any()) -> any())) -> 'ok'. --spec update - (name(), - fun((rabbit_types:exchange()) -> rabbit_types:exchange())) - -> not_found | rabbit_types:exchange(). --spec update_decorators(name()) -> 'ok'. --spec immutable(rabbit_types:exchange()) -> rabbit_types:exchange(). --spec info_keys() -> rabbit_types:info_keys(). --spec info(rabbit_types:exchange()) -> rabbit_types:infos(). --spec info - (rabbit_types:exchange(), rabbit_types:info_keys()) - -> rabbit_types:infos(). --spec info_all(rabbit_types:vhost()) -> [rabbit_types:infos()]. --spec info_all(rabbit_types:vhost(), rabbit_types:info_keys()) - -> [rabbit_types:infos()]. --spec info_all(rabbit_types:vhost(), rabbit_types:info_keys(), - reference(), pid()) - -> 'ok'. --spec route(rabbit_types:exchange(), rabbit_types:delivery()) - -> [rabbit_amqqueue:name()]. --spec delete - (name(), 'true', rabbit_types:username()) -> - 'ok'| rabbit_types:error('not_found' | 'in_use'); - (name(), 'false', rabbit_types:username()) -> - 'ok' | rabbit_types:error('not_found'). --spec validate_binding - (rabbit_types:exchange(), rabbit_types:binding()) - -> rabbit_types:ok_or_error({'binding_invalid', string(), [any()]}). --spec maybe_auto_delete - (rabbit_types:exchange(), boolean()) - -> 'not_deleted' | {'deleted', rabbit_binding:deletions()}. --spec serial(rabbit_types:exchange()) -> - fun((boolean()) -> 'none' | pos_integer()). --spec peek_serial(name()) -> pos_integer() | 'undefined'. - %%---------------------------------------------------------------------------- -define(INFO_KEYS, [name, type, durable, auto_delete, internal, arguments, policy, user_who_performed_action]). +-spec recover(rabbit_types:vhost()) -> [name()]. + recover(VHost) -> Xs = rabbit_misc:table_filter( fun (#exchange{name = XName}) -> @@ -123,6 +59,10 @@ recover(VHost) -> rabbit_durable_exchange), [XName || #exchange{name = XName} <- Xs]. +-spec callback + (rabbit_types:exchange(), fun_name(), + fun((boolean()) -> non_neg_integer()) | atom(), [any()]) -> 'ok'. + callback(X = #exchange{type = XType, decorators = Decorators}, Fun, Serial0, Args) -> Serial = if is_function(Serial0) -> Serial0; @@ -133,6 +73,9 @@ callback(X = #exchange{type = XType, Module = type_to_module(XType), apply(Module, Fun, [Serial(Module:serialise_events()) | Args]). +-spec policy_changed + (rabbit_types:exchange(), rabbit_types:exchange()) -> 'ok'. + policy_changed(X = #exchange{type = XType, decorators = Decorators}, X1 = #exchange{decorators = Decorators1}) -> @@ -147,6 +90,9 @@ serialise_events(X = #exchange{type = Type, decorators = Decorators}) -> rabbit_exchange_decorator:select(all, Decorators)) orelse (type_to_module(Type)):serialise_events(). +-spec serial(rabbit_types:exchange()) -> + fun((boolean()) -> 'none' | pos_integer()). + serial(#exchange{name = XName} = X) -> Serial = case serialise_events(X) of true -> next_serial(XName); @@ -156,6 +102,11 @@ serial(#exchange{name = XName} = X) -> (false) -> none end. +-spec declare + (name(), type(), boolean(), boolean(), boolean(), + rabbit_framing:amqp_table(), rabbit_types:username()) + -> rabbit_types:exchange(). + declare(XName, Type, Durable, AutoDelete, Internal, Args, Username) -> X = rabbit_exchange_decorator:set( rabbit_policy:set(#exchange{name = XName, @@ -218,6 +169,10 @@ store_ram(X) -> X1. %% Used with binaries sent over the wire; the type may not exist. + +-spec check_type + (binary()) -> atom() | rabbit_types:connection_exit(). + check_type(TypeBin) -> case rabbit_registry:binary_to_type(TypeBin) of {error, not_found} -> @@ -232,6 +187,11 @@ check_type(TypeBin) -> end end. +-spec assert_equivalence + (rabbit_types:exchange(), atom(), boolean(), boolean(), boolean(), + rabbit_framing:amqp_table()) + -> 'ok' | rabbit_types:connection_exit(). + assert_equivalence(X = #exchange{ name = XName, durable = Durable, auto_delete = AutoDelete, @@ -245,6 +205,10 @@ assert_equivalence(X = #exchange{ name = XName, AFE(Internal, ReqInternal, XName, internal), (type_to_module(Type)):assert_args_equivalence(X, ReqArgs). +-spec assert_args_equivalence + (rabbit_types:exchange(), rabbit_framing:amqp_table()) + -> 'ok' | rabbit_types:connection_exit(). + assert_args_equivalence(#exchange{ name = Name, arguments = Args }, RequiredArgs) -> %% The spec says "Arguments are compared for semantic @@ -253,21 +217,36 @@ assert_args_equivalence(#exchange{ name = Name, arguments = Args }, rabbit_misc:assert_args_equivalence(Args, RequiredArgs, Name, [<<"alternate-exchange">>]). +-spec lookup + (name()) -> rabbit_types:ok(rabbit_types:exchange()) | + rabbit_types:error('not_found'). + lookup(Name) -> rabbit_misc:dirty_read({rabbit_exchange, Name}). +-spec lookup_or_die + (name()) -> rabbit_types:exchange() | + rabbit_types:channel_exit(). + lookup_or_die(Name) -> case lookup(Name) of {ok, X} -> X; - {error, not_found} -> rabbit_misc:not_found(Name) + {error, not_found} -> rabbit_amqqueue:not_found(Name) end. +-spec list() -> [rabbit_types:exchange()]. + list() -> mnesia:dirty_match_object(rabbit_exchange, #exchange{_ = '_'}). +-spec list_names() -> [rabbit_exchange:name()]. + list_names() -> mnesia:dirty_all_keys(rabbit_exchange). %% Not dirty_match_object since that would not be transactional when used in a %% tx context + +-spec list(rabbit_types:vhost()) -> [rabbit_types:exchange()]. + list(VHostPath) -> mnesia:async_dirty( fun () -> @@ -277,6 +256,10 @@ list(VHostPath) -> read) end). +-spec lookup_scratch(name(), atom()) -> + rabbit_types:ok(term()) | + rabbit_types:error('not_found'). + lookup_scratch(Name, App) -> case lookup(Name) of {ok, #exchange{scratches = undefined}} -> @@ -290,6 +273,8 @@ lookup_scratch(Name, App) -> {error, not_found} end. +-spec update_scratch(name(), atom(), fun((any()) -> any())) -> 'ok'. + update_scratch(Name, App, Fun) -> rabbit_misc:execute_mnesia_transaction( fun() -> @@ -310,6 +295,8 @@ update_scratch(Name, App, Fun) -> ok end). +-spec update_decorators(name()) -> 'ok'. + update_decorators(Name) -> rabbit_misc:execute_mnesia_transaction( fun() -> @@ -320,6 +307,11 @@ update_decorators(Name) -> end end). +-spec update + (name(), + fun((rabbit_types:exchange()) -> rabbit_types:exchange())) + -> not_found | rabbit_types:exchange(). + update(Name, Fun) -> case mnesia:wread({rabbit_exchange, Name}) of [X] -> X1 = Fun(X), @@ -327,10 +319,14 @@ update(Name, Fun) -> [] -> not_found end. +-spec immutable(rabbit_types:exchange()) -> rabbit_types:exchange(). + immutable(X) -> X#exchange{scratches = none, policy = none, decorators = none}. +-spec info_keys() -> rabbit_types:info_keys(). + info_keys() -> ?INFO_KEYS. map(VHostPath, F) -> @@ -358,20 +354,38 @@ i(Item, #exchange{type = Type} = X) -> [] -> throw({bad_argument, Item}) end. +-spec info(rabbit_types:exchange()) -> rabbit_types:infos(). + info(X = #exchange{type = Type}) -> infos(?INFO_KEYS, X) ++ (type_to_module(Type)):info(X). +-spec info + (rabbit_types:exchange(), rabbit_types:info_keys()) + -> rabbit_types:infos(). + info(X = #exchange{type = _Type}, Items) -> infos(Items, X). +-spec info_all(rabbit_types:vhost()) -> [rabbit_types:infos()]. + info_all(VHostPath) -> map(VHostPath, fun (X) -> info(X) end). +-spec info_all(rabbit_types:vhost(), rabbit_types:info_keys()) + -> [rabbit_types:infos()]. + info_all(VHostPath, Items) -> map(VHostPath, fun (X) -> info(X, Items) end). +-spec info_all(rabbit_types:vhost(), rabbit_types:info_keys(), + reference(), pid()) + -> 'ok'. + info_all(VHostPath, Items, Ref, AggregatorPid) -> rabbit_control_misc:emitting_map( AggregatorPid, Ref, fun(X) -> info(X, Items) end, list(VHostPath)). +-spec route(rabbit_types:exchange(), rabbit_types:delivery()) + -> [rabbit_amqqueue:name()]. + route(#exchange{name = #resource{virtual_host = VHost, name = RName} = XName, decorators = Decorators} = X, #delivery{message = #basic_message{routing_keys = RKs}} = Delivery) -> @@ -447,6 +461,12 @@ call_with_exchange(XName, Fun) -> end end). +-spec delete + (name(), 'true', rabbit_types:username()) -> + 'ok'| rabbit_types:error('not_found' | 'in_use'); + (name(), 'false', rabbit_types:username()) -> + 'ok' | rabbit_types:error('not_found'). + delete(XName, IfUnused, Username) -> Fun = case IfUnused of true -> fun conditional_delete/2; @@ -478,10 +498,18 @@ delete(XName, IfUnused, Username) -> XName#resource.name, Username) end. +-spec validate_binding + (rabbit_types:exchange(), rabbit_types:binding()) + -> rabbit_types:ok_or_error({'binding_invalid', string(), [any()]}). + validate_binding(X = #exchange{type = XType}, Binding) -> Module = type_to_module(XType), Module:validate_binding(X, Binding). +-spec maybe_auto_delete + (rabbit_types:exchange(), boolean()) + -> 'not_deleted' | {'deleted', rabbit_binding:deletions()}. + maybe_auto_delete(#exchange{auto_delete = false}, _OnlyDurable) -> not_deleted; maybe_auto_delete(#exchange{auto_delete = true} = X, OnlyDurable) -> @@ -516,6 +544,8 @@ next_serial(XName) -> #exchange_serial{name = XName, next = Serial + 1}, write), Serial. +-spec peek_serial(name()) -> pos_integer() | 'undefined'. + peek_serial(XName) -> peek_serial(XName, read). peek_serial(XName, LockType) -> diff --git a/src/rabbit_exchange_type_headers.erl b/src/rabbit_exchange_type_headers.erl index 4d6d49ef58..8b94bd343b 100644 --- a/src/rabbit_exchange_type_headers.erl +++ b/src/rabbit_exchange_type_headers.erl @@ -33,10 +33,6 @@ {requires, rabbit_registry}, {enables, kernel_ready}]}). --spec headers_match - (rabbit_framing:amqp_table(), rabbit_framing:amqp_table()) -> - boolean(). - info(_X) -> []. info(_X, _) -> []. @@ -84,6 +80,11 @@ parse_x_match(_) -> all. %% legacy; we didn't validate %% In other words: REQUIRES BOTH PATTERN AND DATA TO BE SORTED ASCENDING BY KEY. %% !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! %% + +-spec headers_match + (rabbit_framing:amqp_table(), rabbit_framing:amqp_table()) -> + boolean(). + headers_match(Args, Data) -> MK = parse_x_match(rabbit_misc:table_lookup(Args, <<"x-match">>)), headers_match(Args, Data, true, false, MK). diff --git a/src/rabbit_feature_flags.erl b/src/rabbit_feature_flags.erl new file mode 100644 index 0000000000..e5d59b1a47 --- /dev/null +++ b/src/rabbit_feature_flags.erl @@ -0,0 +1,1797 @@ +%% The contents of this file are subject to the Mozilla Public License +%% Version 1.1 (the "License"); you may not use this file except in +%% compliance with the License. You may obtain a copy of the License +%% at http://www.mozilla.org/MPL/ +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and +%% limitations under the License. +%% +%% The Original Code is RabbitMQ. +%% +%% The Initial Developer of the Original Code is GoPivotal, Inc. +%% Copyright (c) 2018-2019 Pivotal Software, Inc. All rights reserved. +%% + +%% @author The RabbitMQ team +%% @copyright 2018-2019 Pivotal Software, Inc. +%% +%% @doc +%% This module offers a framework to declare capabilities a RabbitMQ node +%% supports and therefore a way to determine if multiple RabbitMQ nodes in +%% a cluster are compatible and can work together. +%% +%% == What a feature flag is == +%% +%% A <strong>feature flag</strong> is a name and several properties given +%% to a change in RabbitMQ which impacts its communication with other +%% RabbitMQ nodes. This kind of change can be: +%% <ul> +%% <li>an update to an Erlang record</li> +%% <li>a modification to a replicated Mnesia table schema</li> +%% <li>a modification to Erlang messages exchanged between Erlang processes +%% which might run on remote nodes</li> +%% </ul> +%% +%% A feature flag is qualified by: +%% <ul> +%% <li>a <strong>name</strong></li> +%% <li>a <strong>description</strong> (optional)</li> +%% <li>a list of other <strong>feature flags this feature flag depends on +%% </strong> (optional). This can be useful when the change builds up on +%% top of a previous change. For instance, it expands a record which was +%% already modified by a previous feature flag.</li> +%% <li>a <strong>migration function</strong> (optional). If provided, this +%% function is called when the feature flag is enabled. It is responsible +%% for doing all the data conversion, if any, and confirming the feature +%% flag can be enabled.</li> +%% <li>a level of stability (stable or experimental). For now, this is only +%% informational. But it might be used for specific purposes in the +%% future.</li> +%% </ul> +%% +%% == How to declare a feature flag == +%% +%% To define a new feature flag, you need to use the +%% `rabbitmq_feature_flag()' module attribute: +%% +%% ``` +%% -rabitmq_feature_flag(FeatureFlag). +%% ''' +%% +%% `FeatureFlag' is a {@type feature_flag_modattr()}. +%% +%% == How to enable a feature flag == +%% +%% To enable a supported feature flag, you have the following solutions: +%% +%% <ul> +%% <li>Using this module API: +%% ``` +%% rabbit_feature_flags:enable(FeatureFlagName). +%% ''' +%% </li> +%% <li>Using the `rabbitmqctl' CLI: +%% ``` +%% rabbitmqctl enable_feature_flag "$feature_flag_name" +%% ''' +%% </li> +%% </ul> +%% +%% == How to disable a feature flag == +%% +%% Once enabled, there is <strong>currently no way to disable</strong> a +%% feature flag. + +-module(rabbit_feature_flags). + +-export([list/0, + list/1, + list/2, + enable/1, + enable_all/0, + disable/1, + disable_all/0, + is_supported/1, + is_supported/2, + is_supported_locally/1, + is_supported_remotely/1, + is_supported_remotely/2, + is_supported_remotely/3, + is_enabled/1, + is_enabled/2, + is_disabled/1, + is_disabled/2, + info/0, + info/1, + init/0, + get_state/1, + get_stability/1, + check_node_compatibility/1, + check_node_compatibility/2, + is_node_compatible/1, + is_node_compatible/2, + sync_feature_flags_with_cluster/1, + sync_feature_flags_with_cluster/2, + enabled_feature_flags_list_file/0 + ]). + +%% RabbitMQ internal use only. +-export([initialize_registry/0, + mark_as_enabled_locally/2, + remote_nodes/0, + running_remote_nodes/0, + does_node_support/3]). + +-ifdef(TEST). +-export([mark_as_enabled_remotely/2, + mark_as_enabled_remotely/4]). +-endif. + +%% Default timeout for operations on remote nodes. +-define(TIMEOUT, 60000). + +-define(FF_REGISTRY_LOADING_LOCK, {feature_flags_registry_loading, self()}). +-define(FF_STATE_CHANGE_LOCK, {feature_flags_state_change, self()}). + +-type feature_flag_modattr() :: {feature_name(), + feature_props()}. +%% The value of a `-rabbitmq_feature_flag()' module attribute used to +%% declare a new feature flag. + +-type feature_name() :: atom(). +%% The feature flag's name. It is used in many places to identify a +%% specific feature flag. In particular, this is how an end-user (or +%% the CLI) can enable a feature flag. This is also the only bit which +%% is persisted so a node remember which feature flags are enabled. + +-type feature_props() :: #{desc => string(), + doc_url => string(), + stability => stability(), + depends_on => [feature_name()], + migration_fun => migration_fun_name()}. +%% The feature flag properties. +%% +%% All properties are optional. +%% +%% The properties are: +%% <ul> +%% <li>`desc': a description of the feature flag</li> +%% <li>`doc_url': a URL pointing to more documentation about the feature +%% flag</li> +%% <li>`stability': the level of stability</li> +%% <li>`depends_on': a list of feature flags name which must be enabled +%% before this one</li> +%% <li>`migration_fun': a migration function specified by its module and +%% function names</li> +%% </ul> +%% +%% Note that the `migration_fun' is a {@type migration_fun_name()}, +%% not a {@type migration_fun()}. However, the function signature +%% must conform to the {@type migration_fun()} signature. The reason +%% is that we must be able to represent it as an Erlang term when +%% we regenerate the registry module source code (using {@link +%% erl_syntax:abstract/1}). + +-type feature_flags() :: #{feature_name() => feature_props_extended()}. +%% The feature flags map as returned or accepted by several functions in +%% this module. In particular, this what the {@link list/0} function +%% returns. + +-type feature_props_extended() :: #{desc => string(), + doc_url => string(), + stability => stability(), + migration_fun => migration_fun_name(), + depends_on => [feature_name()], + provided_by => atom()}. +%% The feature flag properties, once expanded by this module when feature +%% flags are discovered. +%% +%% The new properties compared to {@type feature_props()} are: +%% <ul> +%% <li>`provided_by': the name of the application providing the feature flag</li> +%% </ul> + +-type stability() :: stable | experimental. +%% The level of stability of a feature flag. Currently, only informational. + +-type migration_fun_name() :: {Module :: atom(), Function :: atom()}. +%% The name of the module and function to call when changing the state of +%% the feature flag. + +-type migration_fun() :: fun((feature_name(), + feature_props_extended(), + migration_fun_context()) + -> ok | {error, any()} | % context = enable + boolean() | undefined). % context = is_enabled +%% The migration function signature. +%% +%% It is called with context `enable' when a feature flag is being enabled. +%% The function is responsible for this feature-flag-specific verification +%% and data conversion. It returns `ok' if RabbitMQ can mark the feature +%% flag as enabled an continue with the next one, if any. Otherwise, it +%% returns `{error, any()}' if there is an error and the feature flag should +%% remain disabled. The function must be idempotent: if the feature flag is +%% already enabled on another node and the local node is running this function +%% again because it is syncing its feature flags state, it should succeed. +%% +%% It is called with the context `is_enabled' to check if a feature flag +%% is actually enabled. It is useful on RabbitMQ startup, just in case +%% the previous instance failed to write the feature flags list file. + +-type migration_fun_context() :: enable | is_enabled. + +-export_type([feature_flag_modattr/0, + feature_props/0, + feature_name/0, + feature_flags/0, + feature_props_extended/0, + stability/0, + migration_fun_name/0, + migration_fun/0, + migration_fun_context/0]). + +-spec list() -> feature_flags(). +%% @doc +%% Lists all supported feature flags. +%% +%% @returns A map of all supported feature flags. + +list() -> list(all). + +-spec list(Which :: all | enabled | disabled) -> feature_flags(). +%% @doc +%% Lists all, enabled or disabled feature flags, depending on the argument. +%% +%% @param Which The group of feature flags to return: `all', `enabled' or +%% `disabled'. +%% @returns A map of selected feature flags. + +list(all) -> rabbit_ff_registry:list(all); +list(enabled) -> rabbit_ff_registry:list(enabled); +list(disabled) -> maps:filter( + fun(FeatureName, _) -> is_disabled(FeatureName) end, + list(all)). + +-spec list(all | enabled | disabled, stability()) -> feature_flags(). +%% @doc +%% Lists all, enabled or disabled feature flags, depending on the first +%% argument, only keeping those having the specified stability. +%% +%% @param Which The group of feature flags to return: `all', `enabled' or +%% `disabled'. +%% @param Stability The level of stability used to filter the map of feature +%% flags. +%% @returns A map of selected feature flags. + +list(Which, Stability) + when Stability =:= stable orelse Stability =:= experimental -> + maps:filter(fun(_, FeatureProps) -> + Stability =:= get_stability(FeatureProps) + end, list(Which)). + +-spec enable(feature_name() | [feature_name()]) -> ok | + {error, Reason :: any()}. +%% @doc +%% Enables the specified feature flag or set of feature flags. +%% +%% @param FeatureName The name or the list of names of feature flags to +%% enable. +%% @returns `ok' if the feature flags (and all the feature flags they +%% depend on) were successfully enabled, or `{error, Reason}' if one +%% feature flag could not be enabled (subsequent feature flags in the +%% dependency tree are left unchanged). + +enable(FeatureName) when is_atom(FeatureName) -> + rabbit_log:debug("Feature flag `~s`: REQUEST TO ENABLE", + [FeatureName]), + case is_enabled(FeatureName) of + true -> + rabbit_log:debug("Feature flag `~s`: already enabled", + [FeatureName]), + ok; + false -> + rabbit_log:debug("Feature flag `~s`: not enabled, " + "check if supported by cluster", + [FeatureName]), + %% The feature flag must be supported locally and remotely + %% (i.e. by all members of the cluster). + case is_supported(FeatureName) of + true -> + rabbit_log:info("Feature flag `~s`: supported, " + "attempt to enable...", + [FeatureName]), + do_enable(FeatureName); + false -> + rabbit_log:error("Feature flag `~s`: not supported", + [FeatureName]), + {error, unsupported} + end + end; +enable(FeatureNames) when is_list(FeatureNames) -> + with_feature_flags(FeatureNames, fun enable/1). + +-spec enable_all() -> ok | {error, any()}. +%% @doc +%% Enables all supported feature flags. +%% +%% @returns `ok' if the feature flags were successfully enabled, +%% or `{error, Reason}' if one feature flag could not be enabled +%% (subsequent feature flags in the dependency tree are left +%% unchanged). + +enable_all() -> + with_feature_flags(maps:keys(list(all)), fun enable/1). + +-spec disable(feature_name() | [feature_name()]) -> ok | {error, any()}. +%% @doc +%% Disables the specified feature flag or set of feature flags. +%% +%% @param FeatureName The name or the list of names of feature flags to +%% disable. +%% @returns `ok' if the feature flags (and all the feature flags they +%% depend on) were successfully disabled, or `{error, Reason}' if one +%% feature flag could not be disabled (subsequent feature flags in the +%% dependency tree are left unchanged). + +disable(FeatureName) when is_atom(FeatureName) -> + {error, unsupported}; +disable(FeatureNames) when is_list(FeatureNames) -> + with_feature_flags(FeatureNames, fun disable/1). + +-spec disable_all() -> ok | {error, any()}. +%% @doc +%% Disables all supported feature flags. +%% +%% @returns `ok' if the feature flags were successfully disabled, +%% or `{error, Reason}' if one feature flag could not be disabled +%% (subsequent feature flags in the dependency tree are left +%% unchanged). + +disable_all() -> + with_feature_flags(maps:keys(list(all)), fun disable/1). + +-spec with_feature_flags([feature_name()], + fun((feature_name()) -> ok | {error, any()})) -> + ok | {error, any()}. +%% @private + +with_feature_flags([FeatureName | Rest], Fun) -> + case Fun(FeatureName) of + ok -> with_feature_flags(Rest, Fun); + Error -> Error + end; +with_feature_flags([], _) -> + ok. + +-spec is_supported(feature_name() | [feature_name()]) -> boolean(). +%% @doc +%% Returns if a single feature flag or a set of feature flags is +%% supported by the entire cluster. +%% +%% This is the same as calling both {@link is_supported_locally/1} and +%% {@link is_supported_remotely/1} with a logical AND. +%% +%% @param FeatureNames The name or a list of names of the feature flag(s) +%% to be checked. +%% @returns `true' if the set of feature flags is entirely supported, or +%% `false' if one of them is not or the RPC timed out. + +is_supported(FeatureNames) -> + is_supported_locally(FeatureNames) andalso + is_supported_remotely(FeatureNames). + +-spec is_supported(feature_name() | [feature_name()], timeout()) -> + boolean(). +%% @doc +%% Returns if a single feature flag or a set of feature flags is +%% supported by the entire cluster. +%% +%% This is the same as calling both {@link is_supported_locally/1} and +%% {@link is_supported_remotely/2} with a logical AND. +%% +%% @param FeatureNames The name or a list of names of the feature flag(s) +%% to be checked. +%% @param Timeout Time in milliseconds after which the RPC gives up. +%% @returns `true' if the set of feature flags is entirely supported, or +%% `false' if one of them is not or the RPC timed out. + +is_supported(FeatureNames, Timeout) -> + is_supported_locally(FeatureNames) andalso + is_supported_remotely(FeatureNames, Timeout). + +-spec is_supported_locally(feature_name() | [feature_name()]) -> boolean(). +%% @doc +%% Returns if a single feature flag or a set of feature flags is +%% supported by the local node. +%% +%% @param FeatureNames The name or a list of names of the feature flag(s) +%% to be checked. +%% @returns `true' if the set of feature flags is entirely supported, or +%% `false' if one of them is not. + +is_supported_locally(FeatureName) when is_atom(FeatureName) -> + rabbit_ff_registry:is_supported(FeatureName); +is_supported_locally(FeatureNames) when is_list(FeatureNames) -> + lists:all(fun(F) -> rabbit_ff_registry:is_supported(F) end, FeatureNames). + +-spec is_supported_remotely(feature_name() | [feature_name()]) -> boolean(). +%% @doc +%% Returns if a single feature flag or a set of feature flags is +%% supported by all remote nodes. +%% +%% @param FeatureNames The name or a list of names of the feature flag(s) +%% to be checked. +%% @returns `true' if the set of feature flags is entirely supported, or +%% `false' if one of them is not or the RPC timed out. + +is_supported_remotely(FeatureNames) -> + is_supported_remotely(FeatureNames, ?TIMEOUT). + +-spec is_supported_remotely(feature_name() | [feature_name()], timeout()) -> boolean(). +%% @doc +%% Returns if a single feature flag or a set of feature flags is +%% supported by all remote nodes. +%% +%% @param FeatureNames The name or a list of names of the feature flag(s) +%% to be checked. +%% @param Timeout Time in milliseconds after which the RPC gives up. +%% @returns `true' if the set of feature flags is entirely supported, or +%% `false' if one of them is not or the RPC timed out. + +is_supported_remotely(FeatureName, Timeout) when is_atom(FeatureName) -> + is_supported_remotely([FeatureName], Timeout); +is_supported_remotely([], _) -> + rabbit_log:debug("Feature flags: skipping query for feature flags " + "support as the given list is empty", + []), + true; +is_supported_remotely(FeatureNames, Timeout) when is_list(FeatureNames) -> + case running_remote_nodes() of + [] -> + rabbit_log:debug("Feature flags: isolated node; " + "skipping remote node query " + "=> consider `~p` supported", + [FeatureNames]), + true; + RemoteNodes -> + rabbit_log:debug("Feature flags: about to query these remote nodes " + "about support for `~p`: ~p", + [FeatureNames, RemoteNodes]), + is_supported_remotely(RemoteNodes, FeatureNames, Timeout) + end. + +-spec is_supported_remotely([node()], + feature_name() | [feature_name()], + timeout()) -> boolean(). +%% @doc +%% Returns if a single feature flag or a set of feature flags is +%% supported by specified remote nodes. +%% +%% @param RemoteNodes The list of remote nodes to query. +%% @param FeatureNames The name or a list of names of the feature flag(s) +%% to be checked. +%% @param Timeout Time in milliseconds after which the RPC gives up. +%% @returns `true' if the set of feature flags is entirely supported by +%% all nodes, or `false' if one of them is not or the RPC timed out. + +is_supported_remotely(_, [], _) -> + rabbit_log:debug("Feature flags: skipping query for feature flags " + "support as the given list is empty", + []), + true; +is_supported_remotely([Node | Rest], FeatureNames, Timeout) -> + case does_node_support(Node, FeatureNames, Timeout) of + true -> + is_supported_remotely(Rest, FeatureNames, Timeout); + false -> + rabbit_log:debug("Feature flags: stopping query " + "for support for `~p` here", + [FeatureNames]), + false + end; +is_supported_remotely([], FeatureNames, _) -> + rabbit_log:info("Feature flags: all running remote nodes support `~p`", + [FeatureNames]), + true. + +-spec is_enabled(feature_name() | [feature_name()]) -> boolean(). +%% @doc +%% Returns if a single feature flag or a set of feature flags is +%% enabled. +%% +%% This is the same as calling {@link is_enabled/2} as a `blocking' +%% call. +%% +%% @param FeatureNames The name or a list of names of the feature flag(s) +%% to be checked. +%% @returns `true' if the set of feature flags is enabled, or +%% `false' if one of them is not. + +is_enabled(FeatureNames) -> + is_enabled(FeatureNames, blocking). + +-spec is_enabled +(feature_name() | [feature_name()], blocking) -> + boolean(); +(feature_name() | [feature_name()], non_blocking) -> + boolean() | state_changing. +%% @doc +%% Returns if a single feature flag or a set of feature flags is +%% enabled. +%% +%% When `blocking' is passed, the function waits (blocks) for the +%% state of a feature flag being disabled or enabled stabilizes before +%% returning its final state. +%% +%% When `non_blocking' is passed, the function returns immediately with +%% the state of the feature flag (`true' if enabled, `false' otherwise) +%% or `state_changing' is the state is being changed at the time of the +%% call. +%% +%% @param FeatureNames The name or a list of names of the feature flag(s) +%% to be checked. +%% @returns `true' if the set of feature flags is enabled, +%% `false' if one of them is not, or `state_changing' if one of them +%% is being worked on. Note that `state_changing' has precedence over +%% `false', so if one is `false' and another one is `state_changing', +%% `state_changing' is returned. + +is_enabled(FeatureNames, non_blocking) -> + is_enabled_nb(FeatureNames); +is_enabled(FeatureNames, blocking) -> + case is_enabled_nb(FeatureNames) of + state_changing -> + global:set_lock(?FF_STATE_CHANGE_LOCK), + global:del_lock(?FF_STATE_CHANGE_LOCK), + is_enabled(FeatureNames, blocking); + IsEnabled -> + IsEnabled + end. + +is_enabled_nb(FeatureName) when is_atom(FeatureName) -> + rabbit_ff_registry:is_enabled(FeatureName); +is_enabled_nb(FeatureNames) when is_list(FeatureNames) -> + lists:foldl( + fun + (_F, state_changing = Acc) -> + Acc; + (F, false = Acc) -> + case rabbit_ff_registry:is_enabled(F) of + state_changing -> state_changing; + _ -> Acc + end; + (F, _) -> + rabbit_ff_registry:is_enabled(F) + end, + true, FeatureNames). + +-spec is_disabled(feature_name() | [feature_name()]) -> boolean(). +%% @doc +%% Returns if a single feature flag or one feature flag in a set of +%% feature flags is disabled. +%% +%% This is the same as negating the result of {@link is_enabled/1}. +%% +%% @param FeatureNames The name or a list of names of the feature flag(s) +%% to be checked. +%% @returns `true' if one of the feature flags is disabled, or +%% `false' if they are all enabled. + +is_disabled(FeatureNames) -> + is_disabled(FeatureNames, blocking). + +-spec is_disabled +(feature_name() | [feature_name()], blocking) -> + boolean(); +(feature_name() | [feature_name()], non_blocking) -> + boolean() | state_changing. +%% @doc +%% Returns if a single feature flag or one feature flag in a set of +%% feature flags is disabled. +%% +%% This is the same as negating the result of {@link is_enabled/2}, +%% except that `state_changing' is returned as is. +%% +%% See {@link is_enabled/2} for a description of the `blocking' and +%% `non_blocking' modes. +%% +%% @param FeatureNames The name or a list of names of the feature flag(s) +%% to be checked. +%% @returns `true' if one feature flag in the set of feature flags is +%% disabled, `false' if they are all enabled, or `state_changing' if +%% one of them is being worked on. Note that `state_changing' has +%% precedence over `true', so if one is `true' (i.e. disabled) and +%% another one is `state_changing', `state_changing' is returned. +%% +%% @see is_enabled/2 + +is_disabled(FeatureName, Blocking) -> + case is_enabled(FeatureName, Blocking) of + state_changing -> state_changing; + IsEnabled -> not IsEnabled + end. + +-spec info() -> ok. +%% @doc +%% Displays a table on stdout summing up the supported feature flags, +%% their state and various informations about them. + +info() -> + info(#{}). + +-spec info(#{color => boolean(), + lines => boolean(), + verbose => non_neg_integer()}) -> ok. +%% @doc +%% Displays a table on stdout summing up the supported feature flags, +%% their state and various informations about them. +%% +%% Supported options are: +%% <ul> +%% <li>`color': a boolean to indicate if colors should be used to +%% highlight some elements.</li> +%% <li>`lines': a boolean to indicate if table borders should be drawn +%% using ASCII lines instead of regular characters.</li> +%% <li>`verbose': a non-negative integer to specify the level of +%% verbosity.</li> +%% </ul> +%% +%% @param Options A map of various options to tune the displayed table. + +info(Options) when is_map(Options) -> + rabbit_ff_extra:info(Options). + +-spec get_state(feature_name()) -> enabled | disabled | unavailable. +%% @doc +%% Returns the state of a feature flag. +%% +%% The possible states are: +%% <ul> +%% <li>`enabled': the feature flag is enabled.</li> +%% <li>`disabled': the feature flag is supported by all nodes in the +%% cluster but currently disabled.</li> +%% <li>`unavailable': the feature flag is unsupported by at least one +%% node in the cluster and can not be enabled for now.</li> +%% </ul> +%% +%% @param FeatureName The name of the feature flag to check. +%% @returns `enabled', `disabled' or `unavailable'. + +get_state(FeatureName) when is_atom(FeatureName) -> + IsEnabled = rabbit_feature_flags:is_enabled(FeatureName), + IsSupported = rabbit_feature_flags:is_supported(FeatureName), + case IsEnabled of + true -> enabled; + false -> case IsSupported of + true -> disabled; + false -> unavailable + end + end. + +-spec get_stability(feature_name() | feature_props_extended()) -> stability(). +%% @doc +%% Returns the stability of a feature flag. +%% +%% The possible stability levels are: +%% <ul> +%% <li>`stable': the feature flag is stable and will not change in future +%% releases: it can be enabled in production.</li> +%% <li>`experimental': the feature flag is experimental and may change in +%% the future (without a guaranteed upgrade path): enabling it in +%% production is not recommended.</li> +%% <li>`unavailable': the feature flag is unsupported by at least one +%% node in the cluster and can not be enabled for now.</li> +%% </ul> +%% +%% @param FeatureName The name of the feature flag to check. +%% @returns `stable' or `experimental'. + +get_stability(FeatureName) when is_atom(FeatureName) -> + case rabbit_ff_registry:get(FeatureName) of + undefined -> undefined; + FeatureProps -> get_stability(FeatureProps) + end; +get_stability(FeatureProps) when is_map(FeatureProps) -> + maps:get(stability, FeatureProps, stable). + +%% ------------------------------------------------------------------- +%% Feature flags registry. +%% ------------------------------------------------------------------- + +-spec init() -> ok | no_return(). +%% @private + +init() -> + %% We want to make sure the `feature_flags` file exists once + %% RabbitMQ was started at least once. This is not required by + %% this module (it works fine if the file is missing) but it helps + %% external tools. + _ = ensure_enabled_feature_flags_list_file_exists(), + + %% We also "list" supported feature flags. We are not interested in + %% that list, however, it triggers the first initialization of the + %% registry. + _ = list(all), + ok. + +-spec initialize_registry() -> ok | {error, any()} | no_return(). +%% @private +%% @doc +%% Initializes or reinitializes the registry. +%% +%% The registry is an Erlang module recompiled at runtime to hold the +%% state of all supported feature flags. +%% +%% That Erlang module is called {@link rabbit_ff_registry}. The initial +%% source code of this module simply calls this function so it is +%% replaced by a proper registry. +%% +%% Once replaced, the registry contains the map of all supported feature +%% flags and their state. This is makes it very efficient to query a +%% feature flag state or property. +%% +%% The registry is local to all RabbitMQ nodes. + +initialize_registry() -> + %% The first step is to get the list of enabled feature flags: if + %% this is the first time we initialize it, we read the list from + %% disk (the `feature_flags` file). Otherwise we query the existing + %% registry before it is replaced. + RegistryInitialized = rabbit_ff_registry:is_registry_initialized(), + EnabledFeatureNames = case RegistryInitialized of + true -> + maps:keys(rabbit_ff_registry:list(enabled)); + false -> + read_enabled_feature_flags_list() + end, + + %% We also record if the feature flags state was correctly written + %% to disk. Currently we don't use this information, but in the + %% future, we might want to retry the write if it failed so far. + %% + %% TODO: Retry to write the feature flags state if the first try + %% failed. + WrittenToDisk = case RegistryInitialized of + true -> + rabbit_ff_registry:is_registry_written_to_disk(); + false -> + true + end, + initialize_registry(EnabledFeatureNames, [], WrittenToDisk). + +-spec initialize_registry([feature_name()], [feature_name()], boolean()) -> + ok | {error, any()} | no_return(). +%% @private +%% @doc +%% Initializes or reinitializes the registry. +%% +%% See {@link initialize_registry/0} for a description of the registry. +%% +%% This function takes two list of feature flag names: +%% <ul> +%% <li>the complete list of feature flags to mark as enabled</li> +%% <li>the list of feature flags being enabled or disabled</li> +%% </ul> +%% +%% The latter is used to block callers asking if a feature flag is +%% enabled or disabled while its state is changing. + +initialize_registry(EnabledFeatureNames, + ChangingFeatureNames, + WrittenToDisk) -> + %% Query the list (it's a map to be exact) of supported feature + %% flags. That list comes from the `-rabbitmq_feature_flag().` + %% module attributes exposed by all currently loaded Erlang modules. + rabbit_log:debug("Feature flags: (re)initialize registry", []), + AllFeatureFlags = query_supported_feature_flags(), + + %% We log the state of those feature flags. + rabbit_log:info("Feature flags: list of feature flags found:", []), + lists:foreach( + fun(FeatureName) -> + rabbit_log:info( + "Feature flags: [~s] ~s", + [case lists:member(FeatureName, EnabledFeatureNames) of + true -> "x"; + false -> " " + end, + FeatureName]) + end, lists:sort(maps:keys(AllFeatureFlags))), + + %% We request the registry to be regenerated and reloaded with the + %% new state. + regen_registry_mod(AllFeatureFlags, + EnabledFeatureNames, + ChangingFeatureNames, + WrittenToDisk). + +-spec query_supported_feature_flags() -> feature_flags(). +%% @private + +query_supported_feature_flags() -> + rabbit_log:debug( + "Feature flags: query feature flags in loaded applications"), + AttributesPerApp = rabbit_misc:all_module_attributes(rabbit_feature_flag), + query_supported_feature_flags(AttributesPerApp, #{}). + +query_supported_feature_flags([{App, _Module, Attributes} | Rest], + AllFeatureFlags) -> + rabbit_log:debug("Feature flags: application `~s` " + "has ~b feature flags", + [App, length(Attributes)]), + AllFeatureFlags1 = lists:foldl( + fun({FeatureName, FeatureProps}, AllFF) -> + merge_new_feature_flags(AllFF, + App, + FeatureName, + FeatureProps) + end, AllFeatureFlags, Attributes), + query_supported_feature_flags(Rest, AllFeatureFlags1); +query_supported_feature_flags([], AllFeatureFlags) -> + AllFeatureFlags. + +-spec merge_new_feature_flags(feature_flags(), + atom(), + feature_name(), + feature_props()) -> feature_flags(). +%% @private + +merge_new_feature_flags(AllFeatureFlags, App, FeatureName, FeatureProps) + when is_atom(FeatureName) andalso is_map(FeatureProps) -> + %% We expand the feature flag properties map with: + %% - the name of the application providing it: only informational + %% for now, but can be handy to understand that a feature flag + %% comes from a plugin. + FeatureProps1 = maps:put(provided_by, App, FeatureProps), + maps:merge(AllFeatureFlags, + #{FeatureName => FeatureProps1}). + +-spec regen_registry_mod(feature_flags(), + [feature_name()], + [feature_name()], + boolean()) -> ok | {error, any()} | no_return(). +%% @private + +regen_registry_mod(AllFeatureFlags, + EnabledFeatureNames, + ChangingFeatureNames, + WrittenToDisk) -> + %% Here, we recreate the source code of the `rabbit_ff_registry` + %% module from scratch. + %% + %% IMPORTANT: We want both modules to have the exact same public + %% API in order to simplify the life of developers and their tools + %% (Dialyzer, completion, and so on). + + %% -module(rabbit_ff_registry). + ModuleAttr = erl_syntax:attribute( + erl_syntax:atom(module), + [erl_syntax:atom(rabbit_ff_registry)]), + ModuleForm = erl_syntax:revert(ModuleAttr), + %% -export([...]). + ExportAttr = erl_syntax:attribute( + erl_syntax:atom(export), + [erl_syntax:list( + [erl_syntax:arity_qualifier( + erl_syntax:atom(F), + erl_syntax:integer(A)) + || {F, A} <- [{get, 1}, + {list, 1}, + {is_supported, 1}, + {is_enabled, 1}, + {is_registry_initialized, 0}, + {is_registry_written_to_disk, 0}]] + ) + ] + ), + ExportForm = erl_syntax:revert(ExportAttr), + %% get(_) -> ... + GetClauses = [erl_syntax:clause( + [erl_syntax:atom(FeatureName)], + [], + [erl_syntax:abstract(maps:get(FeatureName, + AllFeatureFlags))]) + || FeatureName <- maps:keys(AllFeatureFlags) + ], + GetUnknownClause = erl_syntax:clause( + [erl_syntax:variable("_")], + [], + [erl_syntax:atom(undefined)]), + GetFun = erl_syntax:function( + erl_syntax:atom(get), + GetClauses ++ [GetUnknownClause]), + GetFunForm = erl_syntax:revert(GetFun), + %% list(_) -> ... + ListAllBody = erl_syntax:abstract(AllFeatureFlags), + ListAllClause = erl_syntax:clause([erl_syntax:atom(all)], + [], + [ListAllBody]), + EnabledFeatureFlags = maps:filter( + fun(FeatureName, _) -> + lists:member(FeatureName, + EnabledFeatureNames) + end, AllFeatureFlags), + ListEnabledBody = erl_syntax:abstract(EnabledFeatureFlags), + ListEnabledClause = erl_syntax:clause([erl_syntax:atom(enabled)], + [], + [ListEnabledBody]), + ListFun = erl_syntax:function( + erl_syntax:atom(list), + [ListAllClause, ListEnabledClause]), + ListFunForm = erl_syntax:revert(ListFun), + %% is_supported(_) -> ... + IsSupportedClauses = [erl_syntax:clause( + [erl_syntax:atom(FeatureName)], + [], + [erl_syntax:atom(true)]) + || FeatureName <- maps:keys(AllFeatureFlags) + ], + NotSupportedClause = erl_syntax:clause( + [erl_syntax:variable("_")], + [], + [erl_syntax:atom(false)]), + IsSupportedFun = erl_syntax:function( + erl_syntax:atom(is_supported), + IsSupportedClauses ++ [NotSupportedClause]), + IsSupportedFunForm = erl_syntax:revert(IsSupportedFun), + %% is_enabled(_) -> ... + IsEnabledClauses = [erl_syntax:clause( + [erl_syntax:atom(FeatureName)], + [], + [case lists:member(FeatureName, + ChangingFeatureNames) of + true -> + erl_syntax:atom(state_changing); + false -> + erl_syntax:atom( + lists:member(FeatureName, + EnabledFeatureNames)) + end]) + || FeatureName <- maps:keys(AllFeatureFlags) + ], + NotEnabledClause = erl_syntax:clause( + [erl_syntax:variable("_")], + [], + [erl_syntax:atom(false)]), + IsEnabledFun = erl_syntax:function( + erl_syntax:atom(is_enabled), + IsEnabledClauses ++ [NotEnabledClause]), + IsEnabledFunForm = erl_syntax:revert(IsEnabledFun), + %% is_registry_initialized() -> ... + IsInitializedClauses = [erl_syntax:clause( + [], + [], + [erl_syntax:atom(true)]) + ], + IsInitializedFun = erl_syntax:function( + erl_syntax:atom(is_registry_initialized), + IsInitializedClauses), + IsInitializedFunForm = erl_syntax:revert(IsInitializedFun), + %% is_registry_written_to_disk() -> ... + IsWrittenToDiskClauses = [erl_syntax:clause( + [], + [], + [erl_syntax:atom(WrittenToDisk)]) + ], + IsWrittenToDiskFun = erl_syntax:function( + erl_syntax:atom(is_registry_written_to_disk), + IsWrittenToDiskClauses), + IsWrittenToDiskFunForm = erl_syntax:revert(IsWrittenToDiskFun), + %% Compilation! + Forms = [ModuleForm, + ExportForm, + GetFunForm, + ListFunForm, + IsSupportedFunForm, + IsEnabledFunForm, + IsInitializedFunForm, + IsWrittenToDiskFunForm], + CompileOpts = [return_errors, + return_warnings], + case compile:forms(Forms, CompileOpts) of + {ok, Mod, Bin, _} -> + load_registry_mod(Mod, Bin); + {error, Errors, Warnings} -> + rabbit_log:error("Feature flags: registry compilation:~n" + "Errors: ~p~n" + "Warnings: ~p", + [Errors, Warnings]), + {error, {compilation_failure, Errors, Warnings}} + end. + +-spec load_registry_mod(atom(), binary()) -> + ok | {error, any()} | no_return(). +%% @private + +load_registry_mod(Mod, Bin) -> + rabbit_log:debug("Feature flags: registry module ready, loading it..."), + FakeFilename = "Compiled and loaded by " ++ ?MODULE_STRING, + %% Time to load the new registry, replacing the old one. We use a + %% lock here to synchronize concurrent reloads. + global:set_lock(?FF_REGISTRY_LOADING_LOCK, [node()]), + _ = code:soft_purge(Mod), + _ = code:delete(Mod), + Ret = code:load_binary(Mod, FakeFilename, Bin), + global:del_lock(?FF_REGISTRY_LOADING_LOCK, [node()]), + case Ret of + {module, _} -> + rabbit_log:debug("Feature flags: registry module loaded"), + ok; + {error, Reason} -> + rabbit_log:error("Feature flags: failed to load registry " + "module: ~p", [Reason]), + throw({feature_flag_registry_reload_failure, Reason}) + end. + +%% ------------------------------------------------------------------- +%% Feature flags state storage. +%% ------------------------------------------------------------------- + +-spec ensure_enabled_feature_flags_list_file_exists() -> ok | {error, any()}. +%% @private + +ensure_enabled_feature_flags_list_file_exists() -> + File = enabled_feature_flags_list_file(), + case filelib:is_regular(File) of + true -> ok; + false -> write_enabled_feature_flags_list([]) + end. + +-spec read_enabled_feature_flags_list() -> + [feature_name()] | no_return(). +%% @private + +read_enabled_feature_flags_list() -> + case try_to_read_enabled_feature_flags_list() of + {error, Reason} -> + File = enabled_feature_flags_list_file(), + throw({feature_flags_file_read_error, File, Reason}); + Ret -> + Ret + end. + +-spec try_to_read_enabled_feature_flags_list() -> + [feature_name()] | {error, any()}. +%% @private + +try_to_read_enabled_feature_flags_list() -> + File = enabled_feature_flags_list_file(), + case file:consult(File) of + {ok, [List]} -> + List; + {error, enoent} -> + %% If the file is missing, we consider the list of enabled + %% feature flags to be empty. + []; + {error, Reason} = Error -> + rabbit_log:error( + "Feature flags: failed to read the `feature_flags` " + "file at `~s`: ~s", + [File, file:format_error(Reason)]), + Error + end. + +-spec write_enabled_feature_flags_list([feature_name()]) -> + ok | no_return(). +%% @private + +write_enabled_feature_flags_list(FeatureNames) -> + case try_to_write_enabled_feature_flags_list(FeatureNames) of + {error, Reason} -> + File = enabled_feature_flags_list_file(), + throw({feature_flags_file_write_error, File, Reason}); + Ret -> + Ret + end. + +-spec try_to_write_enabled_feature_flags_list([feature_name()]) -> + ok | {error, any()}. +%% @private + +try_to_write_enabled_feature_flags_list(FeatureNames) -> + %% Before writing the new file, we read the existing one. If there + %% are unknown feature flags in that file, we want to keep their + %% state, even though they are unsupported at this time. It could be + %% that a plugin was disabled in the meantime. + PreviouslyEnabled = case try_to_read_enabled_feature_flags_list() of + {error, _} -> []; + List -> List + end, + FeatureNames1 = lists:foldl( + fun(Name, Acc) -> + case is_supported_locally(Name) of + true -> Acc; + false -> [Name | Acc] + end + end, FeatureNames, PreviouslyEnabled), + FeatureNames2 = lists:sort(FeatureNames1), + + File = enabled_feature_flags_list_file(), + Content = io_lib:format("~p.~n", [FeatureNames2]), + %% TODO: If we fail to write the the file, we should spawn a process + %% to retry the operation. + case file:write_file(File, Content) of + ok -> + ok; + {error, Reason} = Error -> + rabbit_log:error( + "Feature flags: failed to write the `feature_flags` " + "file at `~s`: ~s", + [File, file:format_error(Reason)]), + Error + end. + +-spec enabled_feature_flags_list_file() -> file:filename(). +%% @doc +%% Returns the path to the file where the state of feature flags is stored. +%% +%% @returns the path to the file. + +enabled_feature_flags_list_file() -> + case application:get_env(rabbit, feature_flags_file) of + {ok, Val} -> Val; + _ -> filename:join([rabbit_mnesia:dir(), "feature_flags"]) + end. + +%% ------------------------------------------------------------------- +%% Feature flags management: enabling. +%% ------------------------------------------------------------------- + +-spec do_enable(feature_name()) -> ok | {error, any()} | no_return(). +%% @private + +do_enable(FeatureName) -> + %% We mark this feature flag as "state changing" before doing the + %% actual state change. We also take a global lock: this permits + %% to block callers asking about a feature flag changing state. + global:set_lock(?FF_STATE_CHANGE_LOCK), + Ret = case mark_as_enabled(FeatureName, state_changing) of + ok -> + case enable_dependencies(FeatureName, true) of + ok -> + case run_migration_fun(FeatureName, enable) of + ok -> + mark_as_enabled(FeatureName, true); + {error, no_migration_fun} -> + mark_as_enabled(FeatureName, true); + Error -> + Error + end; + Error -> + Error + end; + Error -> + Error + end, + case Ret of + ok -> ok; + _ -> mark_as_enabled(FeatureName, false) + end, + global:del_lock(?FF_STATE_CHANGE_LOCK), + Ret. + +-spec enable_locally(feature_name()) -> ok | {error, any()} | no_return(). +%% @private + +enable_locally(FeatureName) when is_atom(FeatureName) -> + case is_enabled(FeatureName) of + true -> + ok; + false -> + rabbit_log:debug( + "Feature flag `~s`: enable locally (i.e. was enabled on the cluster " + "when this node was not part of it)", + [FeatureName]), + do_enable_locally(FeatureName) + end. + +-spec do_enable_locally(feature_name()) -> ok | {error, any()} | no_return(). +%% @private + +do_enable_locally(FeatureName) -> + case enable_dependencies(FeatureName, false) of + ok -> + case run_migration_fun(FeatureName, enable) of + ok -> + mark_as_enabled_locally(FeatureName, true); + {error, no_migration_fun} -> + mark_as_enabled_locally(FeatureName, true); + Error -> + Error + end; + Error -> + Error + end. + +-spec enable_dependencies(feature_name(), boolean()) -> + ok | {error, any()} | no_return(). +%% @private + +enable_dependencies(FeatureName, Everywhere) -> + FeatureProps = rabbit_ff_registry:get(FeatureName), + DependsOn = maps:get(depends_on, FeatureProps, []), + rabbit_log:debug("Feature flag `~s`: enable dependencies: ~p", + [FeatureName, DependsOn]), + enable_dependencies(FeatureName, DependsOn, Everywhere). + +-spec enable_dependencies(feature_name(), [feature_name()], boolean()) -> + ok | {error, any()} | no_return(). +%% @private + +enable_dependencies(TopLevelFeatureName, [FeatureName | Rest], Everywhere) -> + Ret = case Everywhere of + true -> enable(FeatureName); + false -> enable_locally(FeatureName) + end, + case Ret of + ok -> enable_dependencies(TopLevelFeatureName, Rest, Everywhere); + Error -> Error + end; +enable_dependencies(_, [], _) -> + ok. + +-spec run_migration_fun(feature_name(), any()) -> + any() | {error, any()}. +%% @private + +run_migration_fun(FeatureName, Arg) -> + FeatureProps = rabbit_ff_registry:get(FeatureName), + run_migration_fun(FeatureName, FeatureProps, Arg). + +run_migration_fun(FeatureName, FeatureProps, Arg) -> + case maps:get(migration_fun, FeatureProps, none) of + {MigrationMod, MigrationFun} + when is_atom(MigrationMod) andalso is_atom(MigrationFun) -> + rabbit_log:debug("Feature flag `~s`: run migration function ~p " + "with arg: ~p", + [FeatureName, MigrationFun, Arg]), + try + erlang:apply(MigrationMod, + MigrationFun, + [FeatureName, FeatureProps, Arg]) + catch + _:Reason:Stacktrace -> + rabbit_log:error("Feature flag `~s`: migration function " + "crashed: ~p~n~p", + [FeatureName, Reason, Stacktrace]), + {error, {migration_fun_crash, Reason, Stacktrace}} + end; + none -> + {error, no_migration_fun}; + Invalid -> + rabbit_log:error("Feature flag `~s`: invalid migration " + "function: ~p", + [FeatureName, Invalid]), + {error, {invalid_migration_fun, Invalid}} + end. + +-spec mark_as_enabled(feature_name(), boolean() | state_changing) -> + any() | {error, any()} | no_return(). +%% @private + +mark_as_enabled(FeatureName, IsEnabled) -> + case mark_as_enabled_locally(FeatureName, IsEnabled) of + ok -> + mark_as_enabled_remotely(FeatureName, IsEnabled); + Error -> + Error + end. + +-spec mark_as_enabled_locally(feature_name(), boolean() | state_changing) -> + any() | {error, any()} | no_return(). +%% @private + +mark_as_enabled_locally(FeatureName, IsEnabled) -> + rabbit_log:info("Feature flag `~s`: mark as enabled=~p", + [FeatureName, IsEnabled]), + EnabledFeatureNames = maps:keys(list(enabled)), + NewEnabledFeatureNames = case IsEnabled of + true -> + [FeatureName | EnabledFeatureNames]; + false -> + EnabledFeatureNames -- [FeatureName]; + state_changing -> + EnabledFeatureNames + end, + WrittenToDisk = case NewEnabledFeatureNames of + EnabledFeatureNames -> + rabbit_ff_registry:is_registry_written_to_disk(); + _ -> + ok =:= try_to_write_enabled_feature_flags_list( + NewEnabledFeatureNames) + end, + case IsEnabled of + state_changing -> + initialize_registry(EnabledFeatureNames, + [FeatureName], + WrittenToDisk); + _ -> + initialize_registry(NewEnabledFeatureNames, + [], + WrittenToDisk) + end. + +-spec mark_as_enabled_remotely(feature_name(), boolean() | state_changing) -> + any() | {error, any()} | no_return(). +%% @private + +mark_as_enabled_remotely(FeatureName, IsEnabled) -> + Nodes = running_remote_nodes(), + mark_as_enabled_remotely(Nodes, FeatureName, IsEnabled, ?TIMEOUT). + +-spec mark_as_enabled_remotely([node()], feature_name(), boolean() | state_changing, timeout()) -> + any() | {error, any()} | no_return(). +%% @private + +mark_as_enabled_remotely([], _FeatureName, _IsEnabled, _Timeout) -> + ok; +mark_as_enabled_remotely(Nodes, FeatureName, IsEnabled, Timeout) -> + T0 = erlang:timestamp(), + Rets = [{Node, rpc:call(Node, + ?MODULE, + mark_as_enabled_locally, + [FeatureName, IsEnabled], + Timeout)} + || Node <- Nodes], + FailedNodes = [Node || {Node, Ret} <- Rets, Ret =/= ok], + case FailedNodes of + [] -> + rabbit_log:debug( + "Feature flags: `~s` successfully marked as enabled=~p on all " + "nodes", [FeatureName, IsEnabled]), + ok; + _ -> + T1 = erlang:timestamp(), + rabbit_log:error( + "Feature flags: failed to mark feature flag `~s` as enabled=~p " + "on the following nodes:", [FeatureName, IsEnabled]), + [rabbit_log:error( + "Feature flags: - ~s: ~p", + [Node, Ret]) + || {Node, Ret} <- Rets, + Ret =/= ok], + NewTimeout = Timeout - (timer:now_diff(T1, T0) div 1000), + if + NewTimeout > 0 -> + rabbit_log:debug( + "Feature flags: retrying with a timeout of ~b " + "milliseconds", [NewTimeout]), + mark_as_enabled_remotely(FailedNodes, + FeatureName, + IsEnabled, + NewTimeout); + true -> + rabbit_log:debug( + "Feature flags: not retrying; RPC went over the " + "~b milliseconds timeout", [Timeout]), + %% FIXME: Is crashing the process the best solution here? + throw( + {failed_to_mark_feature_flag_as_enabled_on_remote_nodes, + FeatureName, IsEnabled, FailedNodes}) + end + end. + +%% ------------------------------------------------------------------- +%% Coordination with remote nodes. +%% ------------------------------------------------------------------- + +-spec remote_nodes() -> [node()]. +%% @private + +remote_nodes() -> + mnesia:system_info(db_nodes) -- [node()]. + +-spec running_remote_nodes() -> [node()]. +%% @private + +running_remote_nodes() -> + mnesia:system_info(running_db_nodes) -- [node()]. + +-spec does_node_support(node(), [feature_name()], timeout()) -> boolean(). +%% @private + +does_node_support(Node, FeatureNames, Timeout) -> + rabbit_log:debug("Feature flags: querying `~p` support on node ~s...", + [FeatureNames, Node]), + Ret = case node() of + Node -> + is_supported_locally(FeatureNames); + _ -> + rpc:call(Node, + ?MODULE, is_supported_locally, [FeatureNames], + Timeout) + end, + case Ret of + {badrpc, {'EXIT', + {undef, + [{?MODULE, is_supported_locally, [FeatureNames], []} + | _]}}} -> + %% If rabbit_feature_flags:is_supported_locally/1 is undefined + %% on the remote node, we consider it to be a 3.7.x node. + %% + %% Theoritically, it could be an older version (3.6.x and + %% older). But the RabbitMQ version consistency check + %% (rabbit_misc:version_minor_equivalent/2) called from + %% rabbit_mnesia:check_rabbit_consistency/2 already blocked + %% this situation from happening before we reach this point. + rabbit_log:debug( + "Feature flags: ?MODULE:is_supported_locally(~p) unavailable " + "on node `~s`: assuming it is a RabbitMQ 3.7.x node " + "=> consider the feature flags unsupported", + [FeatureNames, Node]), + false; + {badrpc, Reason} -> + rabbit_log:error("Feature flags: error while querying `~p` " + "support on node ~s: ~p", + [FeatureNames, Node, Reason]), + false; + true -> + rabbit_log:debug("Feature flags: node `~s` supports `~p`", + [Node, FeatureNames]), + true; + false -> + rabbit_log:debug("Feature flags: node `~s` does not support `~p`; " + "stopping query here", + [Node, FeatureNames]), + false + end. + +-spec check_node_compatibility(node()) -> ok | {error, any()}. +%% @doc +%% Checks if a node is compatible with the local node. +%% +%% To be compatible, the following two conditions must be met: +%% <ol> +%% <li>feature flags enabled on the local node must be supported by the +%% remote node</li> +%% <li>feature flags enabled on the remote node must be supported by the +%% local node</li> +%% </ol> +%% +%% @param Node the name of the remote node to test. +%% @returns `ok' if they are compatible, `{error, Reason}' if they are not. + +check_node_compatibility(Node) -> + check_node_compatibility(Node, ?TIMEOUT). + +-spec check_node_compatibility(node(), timeout()) -> ok | {error, any()}. +%% @doc +%% Checks if a node is compatible with the local node. +%% +%% See {@link check_node_compatibility/1} for the conditions required to +%% consider two nodes compatible. +%% +%% @param Node the name of the remote node to test. +%% @param Timeout Time in milliseconds after which the RPC gives up. +%% @returns `ok' if they are compatible, `{error, Reason}' if they are not. +%% +%% @see check_node_compatibility/1 + +check_node_compatibility(Node, Timeout) -> + rabbit_log:debug("Feature flags: node `~s` compatibility check, part 1/2", + [Node]), + Part1 = local_enabled_feature_flags_is_supported_remotely(Node, Timeout), + rabbit_log:debug("Feature flags: node `~s` compatibility check, part 2/2", + [Node]), + Part2 = remote_enabled_feature_flags_is_supported_locally(Node, Timeout), + case {Part1, Part2} of + {true, true} -> + rabbit_log:debug("Feature flags: node `~s` is compatible", [Node]), + ok; + {false, _} -> + rabbit_log:error("Feature flags: node `~s` is INCOMPATIBLE: " + "feature flags enabled locally are not " + "supported remotely", + [Node]), + {error, incompatible_feature_flags}; + {_, false} -> + rabbit_log:error("Feature flags: node `~s` is INCOMPATIBLE: " + "feature flags enabled remotely are not " + "supported locally", + [Node]), + {error, incompatible_feature_flags} + end. + +-spec is_node_compatible(node()) -> boolean(). +%% @doc +%% Returns if a node is compatible with the local node. +%% +%% This function calls {@link check_node_compatibility/2} and returns +%% `true' the latter returns `ok'. Therefore this is the same code, +%% except that this function returns a boolean, but not the reason of +%% the incompatibility if any. +%% +%% @param Node the name of the remote node to test. +%% @returns `true' if they are compatible, `false' otherwise. + +is_node_compatible(Node) -> + is_node_compatible(Node, ?TIMEOUT). + +-spec is_node_compatible(node(), timeout()) -> boolean(). +%% @doc +%% Returns if a node is compatible with the local node. +%% +%% This function calls {@link check_node_compatibility/2} and returns +%% `true' the latter returns `ok'. Therefore this is the same code, +%% except that this function returns a boolean, but not the reason +%% of the incompatibility if any. If the RPC times out, nodes are +%% considered incompatible. +%% +%% @param Node the name of the remote node to test. +%% @param Timeout Time in milliseconds after which the RPC gives up. +%% @returns `true' if they are compatible, `false' otherwise. + +is_node_compatible(Node, Timeout) -> + check_node_compatibility(Node, Timeout) =:= ok. + +-spec local_enabled_feature_flags_is_supported_remotely(node(), + timeout()) -> + boolean(). +%% @private + +local_enabled_feature_flags_is_supported_remotely(Node, Timeout) -> + LocalEnabledFeatureNames = maps:keys(list(enabled)), + is_supported_remotely([Node], LocalEnabledFeatureNames, Timeout). + +-spec remote_enabled_feature_flags_is_supported_locally(node(), + timeout()) -> + boolean(). +%% @private + +remote_enabled_feature_flags_is_supported_locally(Node, Timeout) -> + case query_remote_feature_flags(Node, enabled, Timeout) of + {error, _} -> + false; + RemoteEnabledFeatureFlags when is_map(RemoteEnabledFeatureFlags) -> + RemoteEnabledFeatureNames = maps:keys(RemoteEnabledFeatureFlags), + is_supported_locally(RemoteEnabledFeatureNames) + end. + +-spec query_remote_feature_flags(node(), + Which :: all | enabled | disabled, + timeout()) -> + feature_flags() | {error, any()}. +%% @private + +query_remote_feature_flags(Node, Which, Timeout) -> + rabbit_log:debug("Feature flags: querying ~s feature flags " + "on node `~s`...", + [Which, Node]), + case rpc:call(Node, ?MODULE, list, [Which], Timeout) of + {badrpc, {'EXIT', + {undef, + [{?MODULE, list, [Which], []} + | _]}}} -> + %% See does_node_support/3 for an explanation why we + %% consider this node a 3.7.x node. + rabbit_log:debug( + "Feature flags: ?MODULE:list(~s) unavailable on node `~s`: " + "assuming it is a RabbitMQ 3.7.x node " + "=> consider the list empty", + [Which, Node]), + #{}; + {badrpc, Reason} = Error -> + rabbit_log:error( + "Feature flags: error while querying ~s feature flags " + "on node `~s`: ~p", + [Which, Node, Reason]), + {error, Error}; + RemoteFeatureFlags when is_map(RemoteFeatureFlags) -> + RemoteFeatureNames = maps:keys(RemoteFeatureFlags), + rabbit_log:debug("Feature flags: querying ~s feature flags " + "on node `~s` done; ~s features: ~p", + [Which, Node, Which, RemoteFeatureNames]), + RemoteFeatureFlags + end. + +-spec sync_feature_flags_with_cluster([node()]) -> + ok | {error, any()} | no_return(). +%% @private + +sync_feature_flags_with_cluster(Nodes) -> + sync_feature_flags_with_cluster(Nodes, ?TIMEOUT). + +-spec sync_feature_flags_with_cluster([node()], timeout()) -> + ok | {error, any()} | no_return(). +%% @private + +sync_feature_flags_with_cluster([], _) -> + verify_which_feature_flags_are_actually_enabled(), + FeatureNames = get_forced_feature_flag_names(), + case remote_nodes() of + [] when FeatureNames =:= undefined -> + rabbit_log:debug( + "Feature flags: starting an unclustered node: " + "all feature flags will be enabled by default"), + enable_all(); + [] -> + case FeatureNames of + [] -> + rabbit_log:debug( + "Feature flags: starting an unclustered node: " + "all feature flags are forcibly left disabled " + "from the RABBITMQ_FEATURE_FLAGS environment " + "variable"); + _ -> + rabbit_log:debug( + "Feature flags: starting an unclustered node: " + "only the following feature flags specified in " + "the RABBITMQ_FEATURE_FLAGS environment variable " + "will be enabled: ~p", + [FeatureNames]) + end, + enable(FeatureNames); + _ -> + ok + end; +sync_feature_flags_with_cluster(Nodes, Timeout) -> + verify_which_feature_flags_are_actually_enabled(), + RemoteNodes = Nodes -- [node()], + sync_feature_flags_with_cluster1(RemoteNodes, Timeout). + +sync_feature_flags_with_cluster1([], _) -> + ok; +sync_feature_flags_with_cluster1(RemoteNodes, Timeout) -> + RandomRemoteNode = pick_one_node(RemoteNodes), + rabbit_log:debug("Feature flags: SYNCING FEATURE FLAGS with node `~s`...", + [RandomRemoteNode]), + case query_remote_feature_flags(RandomRemoteNode, enabled, Timeout) of + {error, _} = Error -> + Error; + RemoteFeatureFlags -> + RemoteFeatureNames = maps:keys(RemoteFeatureFlags), + do_sync_feature_flags_with_node1(RemoteFeatureNames) + end. + +pick_one_node(Nodes) -> + RandomIndex = rand:uniform(length(Nodes)), + lists:nth(RandomIndex, Nodes). + +do_sync_feature_flags_with_node1([FeatureFlag | Rest]) -> + case enable_locally(FeatureFlag) of + ok -> do_sync_feature_flags_with_node1(Rest); + Error -> Error + end; +do_sync_feature_flags_with_node1([]) -> + ok. + +-spec get_forced_feature_flag_names() -> [feature_name()] | undefined. +%% @private +%% @doc +%% Returns the (possibly empty) list of feature flags the user want +%% to enable out-of-the-box when starting a node for the first time. +%% +%% Without this, the default is to enable all the supported feature +%% flags. +%% +%% There are two ways to specify that list: +%% <ol> +%% <li>Using the `$RABBITMQ_FEATURE_FLAGS' environment variable; for +%% instance `RABBITMQ_FEATURE_FLAGS=quorum_queue,mnevis'.</li> +%% <li>Using the `forced_feature_flags_on_init' configuration parameter; +%% for instance +%% `{rabbit, [{forced_feature_flags_on_init, [quorum_queue, mnevis]}]}'.</li> +%% </ol> +%% +%% The environment variable has precedence over the configuration +%% parameter. + +get_forced_feature_flag_names() -> + Ret = case get_forced_feature_flag_names_from_env() of + undefined -> get_forced_feature_flag_names_from_config(); + List -> List + end, + case Ret of + undefined -> ok; + [] -> rabbit_log:info("Feature flags: automatic enablement " + "of feature flags disabled (i.e. none " + "will be enabled automatically)", []); + _ -> rabbit_log:info("Feature flags: automatic enablement " + "of feature flags limited to the " + "following list: ~p", [Ret]) + end, + Ret. + +-spec get_forced_feature_flag_names_from_env() -> [feature_name()] | undefined. +%% @private + +get_forced_feature_flag_names_from_env() -> + case os:getenv("RABBITMQ_FEATURE_FLAGS") of + false -> undefined; + Value -> [list_to_atom(V) ||V <- string:lexemes(Value, ",")] + end. + +-spec get_forced_feature_flag_names_from_config() -> [feature_name()] | undefined. +%% @private + +get_forced_feature_flag_names_from_config() -> + Value = application:get_env(rabbit, + forced_feature_flags_on_init, + undefined), + case Value of + undefined -> + Value; + _ when is_list(Value) -> + case lists:all(fun is_atom/1, Value) of + true -> Value; + false -> undefined + end; + _ -> + undefined + end. + +-spec verify_which_feature_flags_are_actually_enabled() -> + ok | {error, any()} | no_return(). +%% @private + +verify_which_feature_flags_are_actually_enabled() -> + AllFeatureFlags = list(all), + EnabledFeatureNames = read_enabled_feature_flags_list(), + rabbit_log:debug("Feature flags: double-checking feature flag states..."), + %% In case the previous instance of the node failed to write the + %% feature flags list file, we want to double-check the list of + %% enabled feature flags read from disk. For each feature flag, + %% we call the migration function to query if the feature flag is + %% actually enabled. + %% + %% If a feature flag doesn't provide a migration function (or if the + %% function fails), we keep the current state of the feature flag. + List1 = maps:fold( + fun(Name, Props, Acc) -> + Ret = run_migration_fun(Name, Props, is_enabled), + case Ret of + true -> + [Name | Acc]; + false -> + Acc; + _ -> + MarkedAsEnabled = is_enabled(Name), + case MarkedAsEnabled of + true -> [Name | Acc]; + false -> Acc + end + end + end, + [], AllFeatureFlags), + RepairedEnabledFeatureNames = lists:sort(List1), + %% We log the list of feature flags for which the state changes + %% after the check above. + WereEnabled = RepairedEnabledFeatureNames -- EnabledFeatureNames, + WereDisabled = EnabledFeatureNames -- RepairedEnabledFeatureNames, + case {WereEnabled, WereDisabled} of + {[], []} -> ok; + _ -> rabbit_log:warning( + "Feature flags: the previous instance of this node " + "must have failed to write the `feature_flags` " + "file at `~s`:", + [enabled_feature_flags_list_file()]) + end, + case WereEnabled of + [] -> ok; + _ -> rabbit_log:warning( + "Feature flags: - list of previously enabled " + "feature flags now marked as such: ~p", [WereEnabled]) + end, + case WereDisabled of + [] -> ok; + _ -> rabbit_log:warning( + "Feature flags: - list of previously disabled " + "feature flags now marked as such: ~p", [WereDisabled]) + end, + %% Finally, if the new list of enabled feature flags is different + %% than the one on disk, we write the new list and re-initialize the + %% registry. + case RepairedEnabledFeatureNames of + EnabledFeatureNames -> + ok; + _ -> + rabbit_log:debug( + "Feature flags: write the repaired list of enabled feature " + "flags"), + WrittenToDisk = ok =:= try_to_write_enabled_feature_flags_list( + RepairedEnabledFeatureNames), + initialize_registry( + RepairedEnabledFeatureNames, [], WrittenToDisk) + end. diff --git a/src/rabbit_ff_extra.erl b/src/rabbit_ff_extra.erl new file mode 100644 index 0000000000..35cd98d740 --- /dev/null +++ b/src/rabbit_ff_extra.erl @@ -0,0 +1,236 @@ +%% The contents of this file are subject to the Mozilla Public License +%% Version 1.1 (the "License"); you may not use this file except in +%% compliance with the License. You may obtain a copy of the License +%% at http://www.mozilla.org/MPL/ +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and +%% limitations under the License. +%% +%% The Original Code is RabbitMQ. +%% +%% The Initial Developer of the Original Code is GoPivotal, Inc. +%% Copyright (c) 2018-2019 Pivotal Software, Inc. All rights reserved. +%% + +%% @author The RabbitMQ team +%% @copyright 2018-2019 Pivotal Software, Inc. +%% +%% @doc +%% This module provides extra functions unused by the feature flags +%% subsystem core functionality. + +-module(rabbit_ff_extra). + +-export([cli_info/0, + info/1, + info/2, + format_error/1]). + +-type cli_info() :: [cli_info_entry()]. +%% A list of feature flags properties, formatted for the RabbitMQ CLI. + +-type cli_info_entry() :: [{name, rabbit_feature_flags:feature_name()} | + {state, enabled | disabled | unavailable} | + {stability, rabbit_feature_flags:stability()} | + {provided_by, atom()} | + {desc, string()} | + {doc_url, string()}]. +%% A list of properties for a single feature flag, formatted for the +%% RabbitMQ CLI. + +-type info_options() :: #{color => boolean(), + lines => boolean(), + verbose => non_neg_integer()}. +%% Options accepted by {@link info/1} and {@link info/2}. + +-export_type([info_options/0]). + +-spec cli_info() -> cli_info(). +%% @doc +%% Returns a list of all feature flags properties. +%% +%% @returns the list of all feature flags properties. + +cli_info() -> + cli_info(rabbit_feature_flags:list(all)). + +-spec cli_info(rabbit_feature_flags:feature_flags()) -> cli_info(). +%% @doc +%% Formats a map of feature flags and their properties into a list of +%% feature flags properties as expected by the RabbitMQ CLI. +%% +%% @param FeatureFlags A map of feature flags. +%% @returns the list of feature flags properties, created from the map +%% specified in arguments. + +cli_info(FeatureFlags) -> + lists:foldr( + fun(FeatureName, Acc) -> + FeatureProps = maps:get(FeatureName, FeatureFlags), + State = rabbit_feature_flags:get_state(FeatureName), + Stability = rabbit_feature_flags:get_stability(FeatureProps), + App = maps:get(provided_by, FeatureProps), + Desc = maps:get(desc, FeatureProps, ""), + DocUrl = maps:get(doc_url, FeatureProps, ""), + FFInfo = [{name, FeatureName}, + {desc, unicode:characters_to_binary(Desc)}, + {doc_url, unicode:characters_to_binary(DocUrl)}, + {state, State}, + {stability, Stability}, + {provided_by, App}], + [FFInfo | Acc] + end, [], lists:sort(maps:keys(FeatureFlags))). + +-spec info(info_options()) -> ok. +%% @doc +%% Displays an array of all supported feature flags and their properties +%% on `stdout'. +%% +%% @param Options Options to tune what is displayed and how. + +info(Options) -> + %% Two tables: one for stable feature flags, one for experimental ones. + StableFF = rabbit_feature_flags:list(all, stable), + case maps:size(StableFF) of + 0 -> + ok; + _ -> + io:format("~n~s## Stable feature flags:~s~n", + [rabbit_pretty_stdout:ascii_color(bright_white), + rabbit_pretty_stdout:ascii_color(default)]), + info(StableFF, Options) + end, + ExpFF = rabbit_feature_flags:list(all, experimental), + case maps:size(ExpFF) of + 0 -> + ok; + _ -> + io:format("~n~s## Experimental feature flags:~s~n", + [rabbit_pretty_stdout:ascii_color(bright_white), + rabbit_pretty_stdout:ascii_color(default)]), + info(ExpFF, Options) + end, + case maps:size(StableFF) + maps:size(ExpFF) of + 0 -> ok; + _ -> state_legend() + end. + +-spec info(rabbit_feature_flags:feature_flags(), info_options()) -> ok. +%% @doc +%% Displays an array of feature flags and their properties on `stdout', +%% based on the specified feature flags map. +%% +%% @param FeatureFlags Map of the feature flags to display. +%% @param Options Options to tune what is displayed and how. + +info(FeatureFlags, Options) -> + Verbose = maps:get(verbose, Options, 0), + IsaTty= case os:getenv("TERM") of + %% We don't have access to isatty(3), so let's + %% assume that is $TERM is defined, we can use + %% colors and drawing characters. + false -> false; + _ -> true + end, + UseColors = maps:get(color, Options, IsaTty), + UseLines = maps:get(lines, Options, IsaTty), + %% Table columns: + %% | Name | State | Provided by | Description + %% + %% where: + %% State = Enabled | Disabled | Unavailable (if a node doesn't + %% support it). + TableHeader = [ + [{"Name", bright_white}, + {"State", bright_white}, + {"Provided by", bright_white}, + {"Description", bright_white}] + ], + Nodes = lists:sort([node() | rabbit_feature_flags:remote_nodes()]), + Rows = lists:foldr( + fun(FeatureName, Acc) -> + FeatureProps = maps:get(FeatureName, FeatureFlags), + State0 = rabbit_feature_flags:get_state(FeatureName), + {State, StateColor} = case State0 of + enabled -> + {"Enabled", green}; + disabled -> + {"Disabled", yellow}; + unavailable -> + {"Unavailable", red_bg} + end, + App = maps:get(provided_by, FeatureProps), + Desc = maps:get(desc, FeatureProps, ""), + MainLine = [{atom_to_list(FeatureName), bright_white}, + {State, StateColor}, + {atom_to_list(App), default}, + {Desc, default}], + VFun = fun(Node) -> + Supported = + rabbit_feature_flags:does_node_support( + Node, [FeatureName], 60000), + {Label, LabelColor} = + case Supported of + true -> {"supported", default}; + false -> {"unsupported", red_bg} + end, + Uncolored = rabbit_misc:format( + " ~s: ~s", [Node, Label]), + Colored = rabbit_misc:format( + " ~s: ~s~s~s", + [Node, + rabbit_pretty_stdout: + ascii_color(LabelColor), + Label, + rabbit_pretty_stdout: + ascii_color(default)]), + [empty, + empty, + empty, + {Uncolored, Colored}] + end, + if + Verbose > 0 -> + [[MainLine, + empty, + [empty, + empty, + empty, + {"Per-node support level:", default}] + | lists:map(VFun, Nodes)] | Acc]; + true -> + [[MainLine] | Acc] + end + end, [], lists:sort(maps:keys(FeatureFlags))), + io:format("~n", []), + rabbit_pretty_stdout:display_table([TableHeader | Rows], + UseColors, + UseLines). + +state_legend() -> + io:format( + "~n" + "Possible states:~n" + " ~sEnabled~s: The feature flag is enabled on all nodes~n" + " ~sDisabled~s: The feature flag is disabled on all nodes~n" + " ~sUnavailable~s: The feature flag cannot be enabled because one or " + "more nodes do not support it~n" + "~n", + [rabbit_pretty_stdout:ascii_color(green), + rabbit_pretty_stdout:ascii_color(default), + rabbit_pretty_stdout:ascii_color(yellow), + rabbit_pretty_stdout:ascii_color(default), + rabbit_pretty_stdout:ascii_color(red_bg), + rabbit_pretty_stdout:ascii_color(default)]). + +-spec format_error(any()) -> string(). +%% @doc +%% Formats the error reason term so it can be presented to human beings. +%% +%% @param Reason The term in the `{error, Reason}' tuple. +%% @returns the formatted error reason. + +format_error(Reason) -> + rabbit_misc:format("~p", [Reason]). diff --git a/src/rabbit_ff_registry.erl b/src/rabbit_ff_registry.erl new file mode 100644 index 0000000000..e18a4b3456 --- /dev/null +++ b/src/rabbit_ff_registry.erl @@ -0,0 +1,167 @@ +%% The contents of this file are subject to the Mozilla Public License +%% Version 1.1 (the "License"); you may not use this file except in +%% compliance with the License. You may obtain a copy of the License +%% at http://www.mozilla.org/MPL/ +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and +%% limitations under the License. +%% +%% The Original Code is RabbitMQ. +%% +%% The Initial Developer of the Original Code is GoPivotal, Inc. +%% Copyright (c) 2018-2019 Pivotal Software, Inc. All rights reserved. +%% + +%% @author The RabbitMQ team +%% @copyright 2018-2019 Pivotal Software, Inc. +%% +%% @doc +%% This module exposes the API of the {@link rabbit_feature_flags} +%% registry. The feature flags registry is an Erlang module, compiled at +%% runtime, storing all the informations about feature flags: which are +%% supported, which are enabled, etc. +%% +%% Because it is compiled at runtime, the initial source code is mostly +%% an API reference. What the initial module does is merely ask {@link +%% rabbit_feature_flags} to generate the real registry. + +-module(rabbit_ff_registry). + +-export([get/1, + list/1, + is_supported/1, + is_enabled/1, + is_registry_initialized/0, + is_registry_written_to_disk/0]). + +-spec get(rabbit_feature_flags:feature_name()) -> + rabbit_feature_flags:feature_props() | undefined. +%% @doc +%% Returns the properties of a feature flag. +%% +%% Only the informations stored in the local registry is used to answer +%% this call. +%% +%% @param FeatureName The name of the feature flag. +%% @returns the properties of the specified feature flag. + +get(FeatureName) -> + rabbit_feature_flags:initialize_registry(), + %% Initially, is_registry_initialized/0 always returns `false` + %% and this ?MODULE:get(FeatureName) is always called. The case + %% statement is here to please Dialyzer. + case is_registry_initialized() of + false -> ?MODULE:get(FeatureName); + true -> undefined + end. + +-spec list(all | enabled | disabled) -> rabbit_feature_flags:feature_flags(). +%% @doc +%% Lists all, enabled or disabled feature flags, depending on the argument. +%% +%% Only the informations stored in the local registry is used to answer +%% this call. +%% +%% @param Which The group of feature flags to return: `all', `enabled' or +%% `disabled'. +%% @returns A map of selected feature flags. + +list(Which) -> + rabbit_feature_flags:initialize_registry(), + %% See get/1 for an explanation of the case statement below. + case is_registry_initialized() of + false -> ?MODULE:list(Which); + true -> #{} + end. + +-spec is_supported(rabbit_feature_flags:feature_name()) -> boolean(). +%% @doc +%% Returns if a feature flag is supported. +%% +%% Only the informations stored in the local registry is used to answer +%% this call. +%% +%% @param FeatureName The name of the feature flag to be checked. +%% @returns `true' if the feature flag is supported, or `false' +%% otherwise. + +is_supported(FeatureName) -> + rabbit_feature_flags:initialize_registry(), + %% See get/1 for an explanation of the case statement below. + case is_registry_initialized() of + false -> ?MODULE:is_supported(FeatureName); + true -> false + end. + +-spec is_enabled(rabbit_feature_flags:feature_name()) -> boolean() | state_changing. +%% @doc +%% Returns if a feature flag is supported or if its state is changing. +%% +%% Only the informations stored in the local registry is used to answer +%% this call. +%% +%% @param FeatureName The name of the feature flag to be checked. +%% @returns `true' if the feature flag is supported, `state_changing' if +%% its state is transient, or `false' otherwise. + +is_enabled(FeatureName) -> + rabbit_feature_flags:initialize_registry(), + %% See get/1 for an explanation of the case statement below. + case is_registry_initialized() of + false -> ?MODULE:is_enabled(FeatureName); + true -> false + end. + +-spec is_registry_initialized() -> boolean(). +%% @doc +%% Indicates if the registry is initialized. +%% +%% The registry is considered initialized once the initial Erlang module +%% was replaced by the copy compiled at runtime. +%% +%% @returns `true' when the module is the one compiled at runtime, +%% `false' when the module is the initial one compiled from RabbitMQ +%% source code. + +is_registry_initialized() -> + always_return_false(). + +-spec is_registry_written_to_disk() -> boolean(). +%% @doc +%% Indicates if the feature flags state was successfully persisted to disk. +%% +%% Note that on startup, {@link rabbit_feature_flags} tries to determine +%% the state of each supported feature flag, regardless of the +%% information on disk, to ensure maximum consistency. However, this can +%% be done for feature flags supporting it only. +%% +%% @returns `true' if the state was successfully written to disk and +%% the registry can be initialized from that during the next RabbitMQ +%% startup, `false' if the write failed and the node might loose feature +%% flags state on restart. + +is_registry_written_to_disk() -> + always_return_true(). + +always_return_true() -> + %% This function is here to trick Dialyzer. We want some functions + %% in this initial on-disk registry to always return `true` or + %% `false`. However the generated regsitry will return actual + %% booleans. The `-spec()` correctly advertises a return type of + %% `boolean()`. But in the meantime, Dialyzer only knows about this + %% copy which, without the trick below, would always return either + %% `true` (e.g. in is_registry_written_to_disk/0) or `false` (e.g. + %% is_registry_initialized/0). This obviously causes some warnings + %% where the registry functions are used: Dialyzer believes that + %% e.g. matching the return value of is_registry_initialized/0 + %% against `true` will never succeed. + %% + %% That's why this function makes a call which we know the result, + %% but not Dialyzer, to "create" that hard-coded `true` return + %% value. + rand:uniform(1) > 0. + +always_return_false() -> + not always_return_true(). diff --git a/src/rabbit_fhc_helpers.erl b/src/rabbit_fhc_helpers.erl index 90833795da..57dbd981f1 100644 --- a/src/rabbit_fhc_helpers.erl +++ b/src/rabbit_fhc_helpers.erl @@ -18,7 +18,7 @@ -export([clear_read_cache/0]). --include("rabbit.hrl"). % For #amqqueue record definition. +-include("amqqueue.hrl"). clear_read_cache() -> case application:get_env(rabbit, fhc_read_buffering) of @@ -37,7 +37,9 @@ clear_vhost_read_cache([VHost | Rest]) -> clear_queue_read_cache([]) -> ok; -clear_queue_read_cache([#amqqueue{pid = MPid, slave_pids = SPids} | Rest]) -> +clear_queue_read_cache([Q | Rest]) when ?is_amqqueue(Q) -> + MPid = amqqueue:get_pid(Q), + SPids = amqqueue:get_slave_pids(Q), %% Limit the action to the current node. Pids = [P || P <- [MPid | SPids], node(P) =:= node()], %% This function is executed in the context of the backing queue diff --git a/src/rabbit_file.erl b/src/rabbit_file.erl index 557bc6f02a..462ddb4e11 100644 --- a/src/rabbit_file.erl +++ b/src/rabbit_file.erl @@ -34,31 +34,10 @@ -type ok_or_error() :: rabbit_types:ok_or_error(any()). --spec is_file((file:filename())) -> boolean(). --spec is_dir((file:filename())) -> boolean(). --spec file_size((file:filename())) -> non_neg_integer(). --spec ensure_dir((file:filename())) -> ok_or_error(). --spec wildcard(string(), file:filename()) -> [file:filename()]. --spec list_dir(file:filename()) -> - rabbit_types:ok_or_error2([file:filename()], any()). --spec read_term_file - (file:filename()) -> {'ok', [any()]} | rabbit_types:error(any()). --spec write_term_file(file:filename(), [any()]) -> ok_or_error(). --spec write_file(file:filename(), iodata()) -> ok_or_error(). --spec write_file(file:filename(), iodata(), [any()]) -> ok_or_error(). --spec append_file(file:filename(), string()) -> ok_or_error(). --spec ensure_parent_dirs_exist(string()) -> 'ok'. --spec rename(file:filename(), file:filename()) -> ok_or_error(). --spec delete([file:filename()]) -> ok_or_error(). --spec recursive_delete([file:filename()]) -> - rabbit_types:ok_or_error({file:filename(), any()}). --spec recursive_copy(file:filename(), file:filename()) -> - rabbit_types:ok_or_error({file:filename(), file:filename(), any()}). --spec lock_file(file:filename()) -> rabbit_types:ok_or_error('eexist'). --spec filename_as_a_directory(file:filename()) -> file:filename(). - %%---------------------------------------------------------------------------- +-spec is_file((file:filename())) -> boolean(). + is_file(File) -> case read_file_info(File) of {ok, #file_info{type=regular}} -> true; @@ -66,6 +45,8 @@ is_file(File) -> _ -> false end. +-spec is_dir((file:filename())) -> boolean(). + is_dir(Dir) -> is_dir_internal(read_file_info(Dir)). is_dir_no_handle(Dir) -> is_dir_internal(prim_file:read_file_info(Dir)). @@ -73,12 +54,16 @@ is_dir_no_handle(Dir) -> is_dir_internal(prim_file:read_file_info(Dir)). is_dir_internal({ok, #file_info{type=directory}}) -> true; is_dir_internal(_) -> false. +-spec file_size((file:filename())) -> non_neg_integer(). + file_size(File) -> case read_file_info(File) of {ok, #file_info{size=Size}} -> Size; _ -> 0 end. +-spec ensure_dir((file:filename())) -> ok_or_error(). + ensure_dir(File) -> with_handle(fun () -> ensure_dir_internal(File) end). ensure_dir_internal("/") -> @@ -91,6 +76,8 @@ ensure_dir_internal(File) -> prim_file:make_dir(Dir) end. +-spec wildcard(string(), file:filename()) -> [file:filename()]. + wildcard(Pattern, Dir) -> case list_dir(Dir) of {ok, Files} -> {ok, RE} = re:compile(Pattern, [anchored]), @@ -99,11 +86,17 @@ wildcard(Pattern, Dir) -> {error, _} -> [] end. +-spec list_dir(file:filename()) -> + rabbit_types:ok_or_error2([file:filename()], any()). + list_dir(Dir) -> with_handle(fun () -> prim_file:list_dir(Dir) end). read_file_info(File) -> with_handle(fun () -> prim_file:read_file_info(File) end). +-spec read_term_file + (file:filename()) -> {'ok', [any()]} | rabbit_types:error(any()). + read_term_file(File) -> try {ok, Data} = with_handle(fun () -> prim_file:read_file(File) end), @@ -124,12 +117,18 @@ group_tokens(Cur, []) -> [Cur]; group_tokens(Cur, [T = {dot, _} | Ts]) -> [[T | Cur] | group_tokens([], Ts)]; group_tokens(Cur, [T | Ts]) -> group_tokens([T | Cur], Ts). +-spec write_term_file(file:filename(), [any()]) -> ok_or_error(). + write_term_file(File, Terms) -> write_file(File, list_to_binary([io_lib:format("~w.~n", [Term]) || Term <- Terms])). +-spec write_file(file:filename(), iodata()) -> ok_or_error(). + write_file(Path, Data) -> write_file(Path, Data, []). +-spec write_file(file:filename(), iodata(), [any()]) -> ok_or_error(). + write_file(Path, Data, Modes) -> Modes1 = [binary, write | (Modes -- [binary, write])], case make_binary(Data) of @@ -185,6 +184,9 @@ with_synced_copy(Path, Modes, Fun) -> end. %% TODO the semantics of this function are rather odd. But see bug 25021. + +-spec append_file(file:filename(), string()) -> ok_or_error(). + append_file(File, Suffix) -> case read_file_info(File) of {ok, FInfo} -> append_file(File, FInfo#file_info.size, Suffix); @@ -209,6 +211,8 @@ append_file(File, _, Suffix) -> Error -> Error end. +-spec ensure_parent_dirs_exist(string()) -> 'ok'. + ensure_parent_dirs_exist(Filename) -> case ensure_dir(Filename) of ok -> ok; @@ -216,10 +220,17 @@ ensure_parent_dirs_exist(Filename) -> throw({error, {cannot_create_parent_dirs, Filename, Reason}}) end. +-spec rename(file:filename(), file:filename()) -> ok_or_error(). + rename(Old, New) -> with_handle(fun () -> prim_file:rename(Old, New) end). +-spec delete([file:filename()]) -> ok_or_error(). + delete(File) -> with_handle(fun () -> prim_file:delete(File) end). +-spec recursive_delete([file:filename()]) -> + rabbit_types:ok_or_error({file:filename(), any()}). + recursive_delete(Files) -> with_handle( fun () -> lists:foldl(fun (Path, ok) -> recursive_delete1(Path); @@ -262,6 +273,9 @@ is_symlink_no_handle(File) -> _ -> false end. +-spec recursive_copy(file:filename(), file:filename()) -> + rabbit_types:ok_or_error({file:filename(), file:filename(), any()}). + recursive_copy(Src, Dest) -> %% Note that this uses the 'file' module and, hence, shouldn't be %% run on many processes at once. @@ -293,6 +307,9 @@ recursive_copy(Src, Dest) -> %% TODO: When we stop supporting Erlang prior to R14, this should be %% replaced with file:open [write, exclusive] + +-spec lock_file(file:filename()) -> rabbit_types:ok_or_error('eexist'). + lock_file(Path) -> case is_file(Path) of true -> {error, eexist}; @@ -302,6 +319,8 @@ lock_file(Path) -> end) end. +-spec filename_as_a_directory(file:filename()) -> file:filename(). + filename_as_a_directory(FileName) -> case lists:last(FileName) of "/" -> diff --git a/src/rabbit_guid.erl b/src/rabbit_guid.erl index 386702e86b..6f03a1a04f 100644 --- a/src/rabbit_guid.erl +++ b/src/rabbit_guid.erl @@ -36,15 +36,10 @@ -type guid() :: binary(). --spec start_link() -> rabbit_types:ok_pid_or_error(). --spec filename() -> string(). --spec gen() -> guid(). --spec gen_secure() -> guid(). --spec string(guid(), any()) -> string(). --spec binary(guid(), any()) -> binary(). - %%---------------------------------------------------------------------------- +-spec start_link() -> rabbit_types:ok_pid_or_error(). + start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [update_disk_serial()], []). @@ -52,6 +47,9 @@ start_link() -> %% We use this to detect a (possibly rather old) Mnesia directory, %% since it has existed since at least 1.7.0 (as far back as I cared %% to go). + +-spec filename() -> string(). + filename() -> filename:join(rabbit_mnesia:dir(), ?SERIAL_FILENAME). @@ -108,6 +106,9 @@ advance_blocks({B1, B2, B3, B4}, I) -> %% generate a GUID. This function should be used when performance is a %% priority and predictability is not an issue. Otherwise use %% gen_secure/0. + +-spec gen() -> guid(). + gen() -> %% We hash a fresh GUID with md5, split it in 4 blocks, and each %% time we need a new guid we rotate them producing a new hash @@ -129,6 +130,9 @@ gen() -> %% serial store hasn't been deleted. %% %% If you are not concerned with predictability, gen/0 is faster. + +-spec gen_secure() -> guid(). + gen_secure() -> %% Here instead of hashing once we hash the GUID and the counter %% each time, so that the GUID is not predictable. @@ -143,9 +147,14 @@ gen_secure() -> %% %% employs base64url encoding, which is safer in more contexts than %% plain base64. + +-spec string(guid(), any()) -> string(). + string(G, Prefix) -> Prefix ++ "-" ++ rabbit_misc:base64url(G). +-spec binary(guid(), any()) -> binary(). + binary(G, Prefix) -> list_to_binary(string(G, Prefix)). diff --git a/src/rabbit_health_check.erl b/src/rabbit_health_check.erl index 54508bfdb0..97d8d44dc3 100644 --- a/src/rabbit_health_check.erl +++ b/src/rabbit_health_check.erl @@ -21,19 +21,20 @@ %% Internal API -export([local/0]). --spec node(node(), timeout()) -> ok | {badrpc, term()} | {error_string, string()}. --spec local() -> ok | {error_string, string()}. - %%---------------------------------------------------------------------------- %% External functions %%---------------------------------------------------------------------------- +-spec node(node(), timeout()) -> ok | {badrpc, term()} | {error_string, string()}. + node(Node) -> %% same default as in CLI node(Node, 70000). node(Node, Timeout) -> rabbit_misc:rpc_call(Node, rabbit_health_check, local, [], Timeout). +-spec local() -> ok | {error_string, string()}. + local() -> run_checks([list_channels, list_queues, alarms, rabbit_node_monitor]). diff --git a/src/rabbit_limiter.erl b/src/rabbit_limiter.erl index b5f09a525b..9770172089 100644 --- a/src/rabbit_limiter.erl +++ b/src/rabbit_limiter.erl @@ -147,36 +147,6 @@ -type credit_mode() :: 'manual' | 'drain' | 'auto'. --spec start_link(rabbit_types:proc_name()) -> - rabbit_types:ok_pid_or_error(). --spec new(pid()) -> lstate(). - --spec limit_prefetch(lstate(), non_neg_integer(), non_neg_integer()) -> - lstate(). --spec unlimit_prefetch(lstate()) -> lstate(). --spec is_active(lstate()) -> boolean(). --spec get_prefetch_limit(lstate()) -> non_neg_integer(). --spec ack(lstate(), non_neg_integer()) -> 'ok'. --spec pid(lstate()) -> pid(). - --spec client(pid()) -> qstate(). --spec activate(qstate()) -> qstate(). --spec can_send(qstate(), boolean(), rabbit_types:ctag()) -> - {'continue' | 'suspend', qstate()}. --spec resume(qstate()) -> qstate(). --spec deactivate(qstate()) -> qstate(). --spec is_suspended(qstate()) -> boolean(). --spec is_consumer_blocked(qstate(), rabbit_types:ctag()) -> boolean(). --spec credit - (qstate(), rabbit_types:ctag(), non_neg_integer(), credit_mode(), - boolean()) -> - {boolean(), qstate()}. --spec ack_from_queue(qstate(), rabbit_types:ctag(), non_neg_integer()) -> - {boolean(), qstate()}. --spec drained(qstate()) -> - {[{rabbit_types:ctag(), non_neg_integer()}], qstate()}. --spec forget_consumer(qstate(), rabbit_types:ctag()) -> qstate(). - %%---------------------------------------------------------------------------- -record(lim, {prefetch_count = 0, @@ -194,41 +164,66 @@ %% API %%---------------------------------------------------------------------------- +-spec start_link(rabbit_types:proc_name()) -> + rabbit_types:ok_pid_or_error(). + start_link(ProcName) -> gen_server2:start_link(?MODULE, [ProcName], []). +-spec new(pid()) -> lstate(). + new(Pid) -> %% this a 'call' to ensure that it is invoked at most once. ok = gen_server:call(Pid, {new, self()}, infinity), #lstate{pid = Pid, prefetch_limited = false}. +-spec limit_prefetch(lstate(), non_neg_integer(), non_neg_integer()) -> + lstate(). + limit_prefetch(L, PrefetchCount, UnackedCount) when PrefetchCount > 0 -> ok = gen_server:call( L#lstate.pid, {limit_prefetch, PrefetchCount, UnackedCount}, infinity), L#lstate{prefetch_limited = true}. +-spec unlimit_prefetch(lstate()) -> lstate(). + unlimit_prefetch(L) -> ok = gen_server:call(L#lstate.pid, unlimit_prefetch, infinity), L#lstate{prefetch_limited = false}. +-spec is_active(lstate()) -> boolean(). + is_active(#lstate{prefetch_limited = Limited}) -> Limited. +-spec get_prefetch_limit(lstate()) -> non_neg_integer(). + get_prefetch_limit(#lstate{prefetch_limited = false}) -> 0; get_prefetch_limit(L) -> gen_server:call(L#lstate.pid, get_prefetch_limit, infinity). +-spec ack(lstate(), non_neg_integer()) -> 'ok'. + ack(#lstate{prefetch_limited = false}, _AckCount) -> ok; ack(L, AckCount) -> gen_server:cast(L#lstate.pid, {ack, AckCount}). +-spec pid(lstate()) -> pid(). + pid(#lstate{pid = Pid}) -> Pid. +-spec client(pid()) -> qstate(). + client(Pid) -> #qstate{pid = Pid, state = dormant, credits = gb_trees:empty()}. +-spec activate(qstate()) -> qstate(). + activate(L = #qstate{state = dormant}) -> ok = gen_server:cast(L#qstate.pid, {register, self()}), L#qstate{state = active}; activate(L) -> L. +-spec can_send(qstate(), boolean(), rabbit_types:ctag()) -> + {'continue' | 'suspend', qstate()}. + can_send(L = #qstate{pid = Pid, state = State, credits = Credits}, AckRequired, CTag) -> case is_consumer_blocked(L, CTag) of @@ -246,18 +241,26 @@ safe_call(Pid, Msg, ExitValue) -> fun () -> ExitValue end, fun () -> gen_server2:call(Pid, Msg, infinity) end). +-spec resume(qstate()) -> qstate(). + resume(L = #qstate{state = suspended}) -> L#qstate{state = active}; resume(L) -> L. +-spec deactivate(qstate()) -> qstate(). + deactivate(L = #qstate{state = dormant}) -> L; deactivate(L) -> ok = gen_server:cast(L#qstate.pid, {unregister, self()}), L#qstate{state = dormant}. +-spec is_suspended(qstate()) -> boolean(). + is_suspended(#qstate{state = suspended}) -> true; is_suspended(#qstate{}) -> false. +-spec is_consumer_blocked(qstate(), rabbit_types:ctag()) -> boolean(). + is_consumer_blocked(#qstate{credits = Credits}, CTag) -> case gb_trees:lookup(CTag, Credits) of none -> false; @@ -265,6 +268,11 @@ is_consumer_blocked(#qstate{credits = Credits}, CTag) -> {value, #credit{}} -> true end. +-spec credit + (qstate(), rabbit_types:ctag(), non_neg_integer(), credit_mode(), + boolean()) -> + {boolean(), qstate()}. + credit(Limiter = #qstate{credits = Credits}, CTag, Crd, Mode, IsEmpty) -> {Res, Cr} = case IsEmpty andalso Mode =:= drain of @@ -273,6 +281,9 @@ credit(Limiter = #qstate{credits = Credits}, CTag, Crd, Mode, IsEmpty) -> end, {Res, Limiter#qstate{credits = enter_credit(CTag, Cr, Credits)}}. +-spec ack_from_queue(qstate(), rabbit_types:ctag(), non_neg_integer()) -> + {boolean(), qstate()}. + ack_from_queue(Limiter = #qstate{credits = Credits}, CTag, Credit) -> {Credits1, Unblocked} = case gb_trees:lookup(CTag, Credits) of @@ -284,6 +295,9 @@ ack_from_queue(Limiter = #qstate{credits = Credits}, CTag, Credit) -> end, {Unblocked, Limiter#qstate{credits = Credits1}}. +-spec drained(qstate()) -> + {[{rabbit_types:ctag(), non_neg_integer()}], qstate()}. + drained(Limiter = #qstate{credits = Credits}) -> Drain = fun(C) -> C#credit{credit = 0, mode = manual} end, {CTagCredits, Credits2} = @@ -295,6 +309,8 @@ drained(Limiter = #qstate{credits = Credits}) -> end, {[], Credits}, Credits), {CTagCredits, Limiter#qstate{credits = Credits2}}. +-spec forget_consumer(qstate(), rabbit_types:ctag()) -> qstate(). + forget_consumer(Limiter = #qstate{credits = Credits}, CTag) -> Limiter#qstate{credits = gb_trees:delete_any(CTag, Credits)}. diff --git a/src/rabbit_memory_monitor.erl b/src/rabbit_memory_monitor.erl index 62cf87fb11..0f5c9fbe3c 100644 --- a/src/rabbit_memory_monitor.erl +++ b/src/rabbit_memory_monitor.erl @@ -54,31 +54,33 @@ -define(EPSILON, 0.000001). %% less than this and we clamp to 0 %%---------------------------------------------------------------------------- - --spec start_link() -> rabbit_types:ok_pid_or_error(). --spec register(pid(), {atom(),atom(),[any()]}) -> 'ok'. --spec deregister(pid()) -> 'ok'. --spec report_ram_duration - (pid(), float() | 'infinity') -> number() | 'infinity'. --spec stop() -> 'ok'. - -%%---------------------------------------------------------------------------- %% Public API %%---------------------------------------------------------------------------- +-spec start_link() -> rabbit_types:ok_pid_or_error(). + start_link() -> gen_server2:start_link({local, ?SERVER}, ?MODULE, [], []). +-spec register(pid(), {atom(),atom(),[any()]}) -> 'ok'. + register(Pid, MFA = {_M, _F, _A}) -> gen_server2:call(?SERVER, {register, Pid, MFA}, infinity). +-spec deregister(pid()) -> 'ok'. + deregister(Pid) -> gen_server2:cast(?SERVER, {deregister, Pid}). +-spec report_ram_duration + (pid(), float() | 'infinity') -> number() | 'infinity'. + report_ram_duration(Pid, QueueDuration) -> gen_server2:call(?SERVER, {report_ram_duration, Pid, QueueDuration}, infinity). +-spec stop() -> 'ok'. + stop() -> gen_server2:cast(?SERVER, stop). diff --git a/src/rabbit_metrics.erl b/src/rabbit_metrics.erl index 2a0a967e86..2d5f9c34e2 100644 --- a/src/rabbit_metrics.erl +++ b/src/rabbit_metrics.erl @@ -25,11 +25,12 @@ -define(SERVER, ?MODULE). --spec start_link() -> rabbit_types:ok_pid_or_error(). - %%---------------------------------------------------------------------------- %% Starts the raw metrics storage and owns the ETS tables. %%---------------------------------------------------------------------------- + +-spec start_link() -> rabbit_types:ok_pid_or_error(). + start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). diff --git a/src/rabbit_mirror_queue_coordinator.erl b/src/rabbit_mirror_queue_coordinator.erl index f2426e156f..96474b0d4e 100644 --- a/src/rabbit_mirror_queue_coordinator.erl +++ b/src/rabbit_mirror_queue_coordinator.erl @@ -26,7 +26,8 @@ -behaviour(gen_server2). -behaviour(gm). --include("rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include("amqqueue.hrl"). -include("gm_specs.hrl"). -record(state, { q, @@ -36,14 +37,6 @@ depth_fun }). --spec start_link - (rabbit_types:amqqueue(), pid() | 'undefined', - rabbit_mirror_queue_master:death_fun(), - rabbit_mirror_queue_master:depth_fun()) -> - rabbit_types:ok_pid_or_error(). --spec get_gm(pid()) -> pid(). --spec ensure_monitoring(pid(), [pid()]) -> 'ok'. - %%---------------------------------------------------------------------------- %% %% Mirror Queues @@ -306,12 +299,22 @@ %% %%---------------------------------------------------------------------------- +-spec start_link + (amqqueue:amqqueue(), pid() | 'undefined', + rabbit_mirror_queue_master:death_fun(), + rabbit_mirror_queue_master:depth_fun()) -> + rabbit_types:ok_pid_or_error(). + start_link(Queue, GM, DeathFun, DepthFun) -> gen_server2:start_link(?MODULE, [Queue, GM, DeathFun, DepthFun], []). +-spec get_gm(pid()) -> pid(). + get_gm(CPid) -> gen_server2:call(CPid, get_gm, infinity). +-spec ensure_monitoring(pid(), [pid()]) -> 'ok'. + ensure_monitoring(CPid, Pids) -> gen_server2:cast(CPid, {ensure_monitoring, Pids}). @@ -319,7 +322,8 @@ ensure_monitoring(CPid, Pids) -> %% gen_server %% --------------------------------------------------------------------------- -init([#amqqueue { name = QueueName } = Q, GM, DeathFun, DepthFun]) -> +init([Q, GM, DeathFun, DepthFun]) when ?is_amqqueue(Q) -> + QueueName = amqqueue:get_name(Q), ?store_proc_name(QueueName), GM1 = case GM of undefined -> @@ -345,9 +349,9 @@ init([#amqqueue { name = QueueName } = Q, GM, DeathFun, DepthFun]) -> handle_call(get_gm, _From, State = #state { gm = GM }) -> reply(GM, State). -handle_cast({gm_deaths, DeadGMPids}, - State = #state { q = #amqqueue { name = QueueName, pid = MPid } }) - when node(MPid) =:= node() -> +handle_cast({gm_deaths, DeadGMPids}, State = #state{q = Q}) when ?amqqueue_pid_runs_on_local_node(Q) -> + QueueName = amqqueue:get_name(Q), + MPid = amqqueue:get_pid(Q), case rabbit_mirror_queue_misc:remove_from_queue( QueueName, MPid, DeadGMPids) of {ok, MPid, DeadPids, ExtraNodes} -> @@ -373,14 +377,15 @@ handle_cast({gm_deaths, DeadGMPids}, error(unexpected_mirrored_state) end; -handle_cast(request_depth, State = #state { depth_fun = DepthFun, - q = #amqqueue { name = QName, pid = MPid }}) -> +handle_cast(request_depth, State = #state{depth_fun = DepthFun, q = QArg}) when ?is_amqqueue(QArg) -> + QName = amqqueue:get_name(QArg), + MPid = amqqueue:get_pid(QArg), case rabbit_amqqueue:lookup(QName) of - {ok, #amqqueue{ pid = MPid }} -> - ok = DepthFun(), - noreply(State); - _ -> - {stop, shutdown, State} + {ok, QFound} when ?amqqueue_pid_equals(QFound, MPid) -> + ok = DepthFun(), + noreply(State); + _ -> + {stop, shutdown, State} end; handle_cast({ensure_monitoring, Pids}, State = #state { monitors = Mons }) -> diff --git a/src/rabbit_mirror_queue_master.erl b/src/rabbit_mirror_queue_master.erl index 4cc6856442..6ab9a875c2 100644 --- a/src/rabbit_mirror_queue_master.erl +++ b/src/rabbit_mirror_queue_master.erl @@ -34,7 +34,8 @@ -behaviour(rabbit_backing_queue). --include("rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include("amqqueue.hrl"). -record(state, { name, gm, @@ -61,18 +62,6 @@ confirmed :: [rabbit_guid:guid()], known_senders :: sets:set() }. --spec promote_backing_queue_state - (rabbit_amqqueue:name(), pid(), atom(), any(), pid(), [any()], - map(), [pid()]) -> - master_state(). - --spec sender_death_fun() -> death_fun(). --spec depth_fun() -> depth_fun(). --spec init_with_existing_bq(rabbit_types:amqqueue(), atom(), any()) -> - master_state(). --spec stop_mirroring(master_state()) -> {atom(), any()}. --spec sync_mirrors(stats_fun(), stats_fun(), master_state()) -> - {'ok', master_state()} | {stop, any(), master_state()}. %% For general documentation of HA design, see %% rabbit_mirror_queue_coordinator @@ -100,45 +89,56 @@ init(Q, Recover, AsyncCallback) -> ok = gm:broadcast(GM, {depth, BQ:depth(BQS)}), State. -init_with_existing_bq(Q = #amqqueue{name = QName}, BQ, BQS) -> +-spec init_with_existing_bq(amqqueue:amqqueue(), atom(), any()) -> + master_state(). + +init_with_existing_bq(Q0, BQ, BQS) when ?is_amqqueue(Q0) -> + QName = amqqueue:get_name(Q0), case rabbit_mirror_queue_coordinator:start_link( - Q, undefined, sender_death_fun(), depth_fun()) of - {ok, CPid} -> - GM = rabbit_mirror_queue_coordinator:get_gm(CPid), - Self = self(), - ok = rabbit_misc:execute_mnesia_transaction( - fun () -> - [Q1 = #amqqueue{gm_pids = GMPids}] - = mnesia:read({rabbit_queue, QName}), - ok = rabbit_amqqueue:store_queue( - Q1#amqqueue{gm_pids = [{GM, Self} | GMPids], - state = live}) - end), - {_MNode, SNodes} = rabbit_mirror_queue_misc:suggested_queue_nodes(Q), - %% We need synchronous add here (i.e. do not return until the - %% slave is running) so that when queue declaration is finished - %% all slaves are up; we don't want to end up with unsynced slaves - %% just by declaring a new queue. But add can't be synchronous all - %% the time as it can be called by slaves and that's - %% deadlock-prone. - rabbit_mirror_queue_misc:add_mirrors(QName, SNodes, sync), - #state { name = QName, - gm = GM, - coordinator = CPid, - backing_queue = BQ, - backing_queue_state = BQS, - seen_status = #{}, - confirmed = [], - known_senders = sets:new(), - wait_timeout = rabbit_misc:get_env(rabbit, slave_wait_timeout, 15000) }; - {error, Reason} -> - %% The GM can shutdown before the coordinator has started up - %% (lost membership or missing group), thus the start_link of - %% the coordinator returns {error, shutdown} as rabbit_amqqueue_process - % is trapping exists - throw({coordinator_not_started, Reason}) + Q0, undefined, sender_death_fun(), depth_fun()) of + {ok, CPid} -> + GM = rabbit_mirror_queue_coordinator:get_gm(CPid), + Self = self(), + Fun = fun () -> + [Q1] = mnesia:read({rabbit_queue, QName}), + true = amqqueue:is_amqqueue(Q1), + GMPids0 = amqqueue:get_gm_pids(Q1), + GMPids1 = [{GM, Self} | GMPids0], + Q2 = amqqueue:set_gm_pids(Q1, GMPids1), + Q3 = amqqueue:set_state(Q2, live), + %% amqqueue migration: + %% The amqqueue was read from this transaction, no + %% need to handle migration. + ok = rabbit_amqqueue:store_queue(Q3) + end, + ok = rabbit_misc:execute_mnesia_transaction(Fun), + {_MNode, SNodes} = rabbit_mirror_queue_misc:suggested_queue_nodes(Q0), + %% We need synchronous add here (i.e. do not return until the + %% slave is running) so that when queue declaration is finished + %% all slaves are up; we don't want to end up with unsynced slaves + %% just by declaring a new queue. But add can't be synchronous all + %% the time as it can be called by slaves and that's + %% deadlock-prone. + rabbit_mirror_queue_misc:add_mirrors(QName, SNodes, sync), + #state{name = QName, + gm = GM, + coordinator = CPid, + backing_queue = BQ, + backing_queue_state = BQS, + seen_status = #{}, + confirmed = [], + known_senders = sets:new(), + wait_timeout = rabbit_misc:get_env(rabbit, slave_wait_timeout, 15000)}; + {error, Reason} -> + %% The GM can shutdown before the coordinator has started up + %% (lost membership or missing group), thus the start_link of + %% the coordinator returns {error, shutdown} as rabbit_amqqueue_process + % is trapping exists + throw({coordinator_not_started, Reason}) end. +-spec stop_mirroring(master_state()) -> {atom(), any()}. + stop_mirroring(State = #state { coordinator = CPid, backing_queue = BQ, backing_queue_state = BQS }) -> @@ -146,6 +146,9 @@ stop_mirroring(State = #state { coordinator = CPid, stop_all_slaves(shutdown, State), {BQ, BQS}. +-spec sync_mirrors(stats_fun(), stats_fun(), master_state()) -> + {'ok', master_state()} | {stop, any(), master_state()}. + sync_mirrors(HandleInfo, EmitStats, State = #state { name = QName, gm = GM, @@ -156,7 +159,8 @@ sync_mirrors(HandleInfo, EmitStats, QName, "Synchronising: " ++ Fmt ++ "~n", Params) end, Log("~p messages to synchronise", [BQ:len(BQS)]), - {ok, #amqqueue{slave_pids = SPids} = Q} = rabbit_amqqueue:lookup(QName), + {ok, Q} = rabbit_amqqueue:lookup(QName), + SPids = amqqueue:get_slave_pids(Q), SyncBatchSize = rabbit_mirror_queue_misc:sync_batch_size(Q), Log("batch size: ~p", [SyncBatchSize]), Ref = make_ref(), @@ -193,8 +197,8 @@ terminate(Reason, %% Backing queue termination. The queue is going down but %% shouldn't be deleted. Most likely safe shutdown of this %% node. - {ok, Q = #amqqueue{sync_slave_pids = SSPids}} = - rabbit_amqqueue:lookup(QName), + {ok, Q} = rabbit_amqqueue:lookup(QName), + SSPids = amqqueue:get_sync_slave_pids(Q), case SSPids =:= [] andalso rabbit_policy:get(<<"ha-promote-on-shutdown">>, Q) =/= <<"always">> of true -> %% Remove the whole queue to avoid data loss @@ -213,7 +217,8 @@ delete_and_terminate(Reason, State = #state { backing_queue = BQ, State#state{backing_queue_state = BQ:delete_and_terminate(Reason, BQS)}. stop_all_slaves(Reason, #state{name = QName, gm = GM, wait_timeout = WT}) -> - {ok, #amqqueue{slave_pids = SPids}} = rabbit_amqqueue:lookup(QName), + {ok, Q} = rabbit_amqqueue:lookup(QName), + SPids = amqqueue:get_slave_pids(Q), rabbit_mirror_queue_misc:stop_all_slaves(Reason, SPids, QName, GM, WT). purge(State = #state { gm = GM, @@ -497,6 +502,11 @@ zip_msgs_and_acks(Msgs, AckTags, Accumulator, %% Other exported functions %% --------------------------------------------------------------------------- +-spec promote_backing_queue_state + (rabbit_amqqueue:name(), pid(), atom(), any(), pid(), [any()], + map(), [pid()]) -> + master_state(). + promote_backing_queue_state(QName, CPid, BQ, BQS, GM, AckTags, Seen, KS) -> {_MsgIds, BQS1} = BQ:requeue(AckTags, BQS), Len = BQ:len(BQS1), @@ -514,6 +524,8 @@ promote_backing_queue_state(QName, CPid, BQ, BQS, GM, AckTags, Seen, KS) -> known_senders = sets:from_list(KS), wait_timeout = WaitTimeout }. +-spec sender_death_fun() -> death_fun(). + sender_death_fun() -> Self = self(), fun (DeadPid) -> @@ -526,6 +538,8 @@ sender_death_fun() -> end) end. +-spec depth_fun() -> depth_fun(). + depth_fun() -> Self = self(), fun () -> diff --git a/src/rabbit_mirror_queue_misc.erl b/src/rabbit_mirror_queue_misc.erl index ac933d4df4..88aef8fcdc 100644 --- a/src/rabbit_mirror_queue_misc.erl +++ b/src/rabbit_mirror_queue_misc.erl @@ -31,7 +31,8 @@ %% for testing only -export([module/1]). --include("rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include("amqqueue.hrl"). -define(HA_NODES_MODULE, rabbit_mirror_queue_mode_nodes). @@ -56,29 +57,12 @@ %%---------------------------------------------------------------------------- +%% Returns {ok, NewMPid, DeadPids, ExtraNodes} + -spec remove_from_queue (rabbit_amqqueue:name(), pid(), [pid()]) -> {'ok', pid(), [pid()], [node()]} | {'error', 'not_found'}. --spec add_mirrors(rabbit_amqqueue:name(), [node()], 'sync' | 'async') -> - 'ok'. --spec store_updated_slaves(rabbit_types:amqqueue()) -> - rabbit_types:amqqueue(). --spec initial_queue_node(rabbit_types:amqqueue(), node()) -> node(). --spec suggested_queue_nodes(rabbit_types:amqqueue()) -> - {node(), [node()]}. --spec is_mirrored(rabbit_types:amqqueue()) -> boolean(). --spec update_mirrors - (rabbit_types:amqqueue(), rabbit_types:amqqueue()) -> 'ok'. --spec update_mirrors - (rabbit_types:amqqueue()) -> 'ok'. --spec maybe_drop_master_after_sync(rabbit_types:amqqueue()) -> 'ok'. --spec maybe_auto_sync(rabbit_types:amqqueue()) -> 'ok'. --spec log_info(rabbit_amqqueue:name(), string(), [any()]) -> 'ok'. --spec log_warning(rabbit_amqqueue:name(), string(), [any()]) -> 'ok'. - -%%---------------------------------------------------------------------------- -%% Returns {ok, NewMPid, DeadPids, ExtraNodes} remove_from_queue(QueueName, Self, DeadGMPids) -> rabbit_misc:execute_mnesia_transaction( fun () -> @@ -86,10 +70,11 @@ remove_from_queue(QueueName, Self, DeadGMPids) -> %% get here. Or, gm group could've altered. see rabbitmq-server#914 case mnesia:read({rabbit_queue, QueueName}) of [] -> {error, not_found}; - [Q = #amqqueue { pid = QPid, - slave_pids = SPids, - sync_slave_pids = SyncSPids, - gm_pids = GMPids }] -> + [Q0] when ?is_amqqueue(Q0) -> + QPid = amqqueue:get_pid(Q0), + SPids = amqqueue:get_slave_pids(Q0), + SyncSPids = amqqueue:get_sync_slave_pids(Q0), + GMPids = amqqueue:get_gm_pids(Q0), {DeadGM, AliveGM} = lists:partition( fun ({GM, _}) -> lists:member(GM, DeadGMPids) @@ -109,7 +94,7 @@ remove_from_queue(QueueName, Self, DeadGMPids) -> _ -> promote_slave(Alive) end, DoNotPromote = SyncSPids =:= [] andalso - rabbit_policy:get(<<"ha-promote-on-failure">>, Q) =:= <<"when-synced">>, + rabbit_policy:get(<<"ha-promote-on-failure">>, Q0) =:= <<"when-synced">>, case {{QPid, SPids}, {QPid1, SPids1}} of {Same, Same} -> {ok, QPid1, DeadPids, []}; @@ -124,23 +109,23 @@ remove_from_queue(QueueName, Self, DeadGMPids) -> %% we're ok to update mnesia; or we have %% become the master. If gm altered, %% we have no choice but to proceed. - Q1 = Q#amqqueue{pid = QPid1, - slave_pids = SPids1, - gm_pids = AliveGM}, - store_updated_slaves(Q1), + Q1 = amqqueue:set_pid(Q0, QPid1), + Q2 = amqqueue:set_slave_pids(Q1, SPids1), + Q3 = amqqueue:set_gm_pids(Q2, AliveGM), + store_updated_slaves(Q3), %% If we add and remove nodes at the %% same time we might tell the old %% master we need to sync and then %% shut it down. So let's check if %% the new master needs to sync. - maybe_auto_sync(Q1), - {ok, QPid1, DeadPids, slaves_to_start_on_failure(Q1, DeadGMPids)}; + maybe_auto_sync(Q3), + {ok, QPid1, DeadPids, slaves_to_start_on_failure(Q3, DeadGMPids)}; _ -> %% Master has changed, and we're not it. %% [1]. - Q1 = Q#amqqueue{slave_pids = Alive, - gm_pids = AliveGM}, - store_updated_slaves(Q1), + Q1 = amqqueue:set_slave_pids(Q0, Alive), + Q2 = amqqueue:set_gm_pids(Q1, AliveGM), + store_updated_slaves(Q2), {ok, QPid1, DeadPids, []} end end @@ -185,13 +170,12 @@ on_vhost_up(VHost) -> fun () -> mnesia:foldl( fun - (#amqqueue{name = #resource{virtual_host = OtherVhost}}, - QNames0) when OtherVhost =/= VHost -> + (Q, QNames0) when not ?amqqueue_vhost_equals(Q, VHost) -> QNames0; - (Q = #amqqueue{name = QName, - pid = Pid, - slave_pids = SPids, - type = classic}, QNames0) -> + (Q, QNames0) when ?amqqueue_is_classic(Q) -> + QName = amqqueue:get_name(Q), + Pid = amqqueue:get_pid(Q), + SPids = amqqueue:get_slave_pids(Q), %% We don't want to pass in the whole %% cluster - we don't want a situation %% where starting one node causes us to @@ -221,7 +205,10 @@ drop_mirrors(QName, Nodes) -> drop_mirror(QName, MirrorNode) -> case rabbit_amqqueue:lookup(QName) of - {ok, #amqqueue { name = Name, pid = QPid, slave_pids = SPids }} -> + {ok, Q} when ?is_amqqueue(Q) -> + Name = amqqueue:get_name(Q), + QPid = amqqueue:get_pid(Q), + SPids = amqqueue:get_slave_pids(Q), case [Pid || Pid <- [QPid | SPids], node(Pid) =:= MirrorNode] of [] -> {error, {queue_not_mirrored_on_node, MirrorNode}}; @@ -237,6 +224,9 @@ drop_mirror(QName, MirrorNode) -> E end. +-spec add_mirrors(rabbit_amqqueue:name(), [node()], 'sync' | 'async') -> + 'ok'. + add_mirrors(QName, Nodes, SyncMode) -> [add_mirror(QName, Node, SyncMode) || Node <- Nodes], ok. @@ -247,7 +237,7 @@ add_mirror(QName, MirrorNode, SyncMode) -> rabbit_misc:with_exit_handler( rabbit_misc:const(ok), fun () -> - #amqqueue{name = #resource{virtual_host = VHost}} = Q, + #resource{virtual_host = VHost} = amqqueue:get_name(Q), case rabbit_vhost_sup_sup:get_vhost_sup(VHost, MirrorNode) of {ok, _} -> SPid = rabbit_amqqueue_sup_sup:start_queue_process( @@ -278,26 +268,39 @@ report_deaths(MirrorPid, IsMaster, QueueName, DeadPids) -> rabbit_misc:pid_to_string(MirrorPid), [[$ , rabbit_misc:pid_to_string(P)] || P <- DeadPids]]). +-spec log_info(rabbit_amqqueue:name(), string(), [any()]) -> 'ok'. + log_info (QName, Fmt, Args) -> rabbit_log_mirroring:info("Mirrored ~s: " ++ Fmt, [rabbit_misc:rs(QName) | Args]). + +-spec log_warning(rabbit_amqqueue:name(), string(), [any()]) -> 'ok'. + log_warning(QName, Fmt, Args) -> rabbit_log_mirroring:warning("Mirrored ~s: " ++ Fmt, [rabbit_misc:rs(QName) | Args]). -store_updated_slaves(Q = #amqqueue{slave_pids = SPids, - sync_slave_pids = SSPids, - recoverable_slaves = RS}) -> +-spec store_updated_slaves(amqqueue:amqqueue()) -> + amqqueue:amqqueue(). + +store_updated_slaves(Q0) when ?is_amqqueue(Q0) -> + SPids = amqqueue:get_slave_pids(Q0), + SSPids = amqqueue:get_sync_slave_pids(Q0), + RS0 = amqqueue:get_recoverable_slaves(Q0), %% TODO now that we clear sync_slave_pids in rabbit_durable_queue, %% do we still need this filtering? SSPids1 = [SSPid || SSPid <- SSPids, lists:member(SSPid, SPids)], - Q1 = Q#amqqueue{sync_slave_pids = SSPids1, - recoverable_slaves = update_recoverable(SPids, RS), - state = live}, - ok = rabbit_amqqueue:store_queue(Q1), + Q1 = amqqueue:set_sync_slave_pids(Q0, SSPids1), + RS1 = update_recoverable(SPids, RS0), + Q2 = amqqueue:set_recoverable_slaves(Q1, RS1), + Q3 = amqqueue:set_state(Q2, live), + %% amqqueue migration: + %% The amqqueue was read from this transaction, no need to handle + %% migration. + ok = rabbit_amqqueue:store_queue(Q3), %% Wake it up so that we emit a stats event - rabbit_amqqueue:notify_policy_changed(Q1), - Q1. + rabbit_amqqueue:notify_policy_changed(Q3), + Q3. %% Recoverable nodes are those which we could promote if the whole %% cluster were to suddenly stop and we then lose the master; i.e. all @@ -346,13 +349,14 @@ stop_all_slaves(Reason, SPids, QName, GM, WaitTimeout) -> %% notice and update Mnesia. But we just removed them all, and %% have stopped listening ourselves. So manually clean up. rabbit_misc:execute_mnesia_transaction(fun () -> - [Q] = mnesia:read({rabbit_queue, QName}), - rabbit_mirror_queue_misc:store_updated_slaves( - Q #amqqueue { gm_pids = [], slave_pids = [], - %% Restarted slaves on running nodes can - %% ensure old incarnations are stopped using - %% the pending slave pids. - slave_pids_pending_shutdown = PendingSlavePids}) + [Q0] = mnesia:read({rabbit_queue, QName}), + Q1 = amqqueue:set_gm_pids(Q0, []), + Q2 = amqqueue:set_slave_pids(Q1, []), + %% Restarted slaves on running nodes can + %% ensure old incarnations are stopped using + %% the pending slave pids. + Q3 = amqqueue:set_slave_pids_pending_shutdown(Q2, PendingSlavePids), + rabbit_mirror_queue_misc:store_updated_slaves(Q3) end), ok = gm:forget_group(QName). @@ -363,17 +367,23 @@ promote_slave([SPid | SPids]) -> %% the one to promote is the oldest. {SPid, SPids}. +-spec initial_queue_node(amqqueue:amqqueue(), node()) -> node(). + initial_queue_node(Q, DefNode) -> {MNode, _SNodes} = suggested_queue_nodes(Q, DefNode, rabbit_nodes:all_running()), MNode. +-spec suggested_queue_nodes(amqqueue:amqqueue()) -> + {node(), [node()]}. + suggested_queue_nodes(Q) -> suggested_queue_nodes(Q, rabbit_nodes:all_running()). suggested_queue_nodes(Q, All) -> suggested_queue_nodes(Q, node(), All). %% The third argument exists so we can pull a call to %% rabbit_mnesia:cluster_nodes(running) out of a loop or transaction %% or both. -suggested_queue_nodes(Q = #amqqueue{exclusive_owner = Owner}, DefNode, All) -> +suggested_queue_nodes(Q, DefNode, All) when ?is_amqqueue(Q) -> + Owner = amqqueue:get_exclusive_owner(Q), {MNode0, SNodes, SSNodes} = actual_queue_nodes(Q), MNode = case MNode0 of none -> DefNode; @@ -395,7 +405,7 @@ policy(Policy, Q) -> P -> P end. -module(#amqqueue{} = Q) -> +module(Q) when ?is_amqqueue(Q) -> case rabbit_policy:get(<<"ha-mode">>, Q) of undefined -> not_mirrored; Mode -> module(Mode) @@ -418,6 +428,8 @@ validate_mode(Mode) -> {error, "~p is not a valid ha-mode value", [Mode]} end. +-spec is_mirrored(amqqueue:amqqueue()) -> boolean(). + is_mirrored(Q) -> case module(Q) of {ok, _} -> true; @@ -430,16 +442,20 @@ is_mirrored_ha_nodes(Q) -> _ -> false end. -actual_queue_nodes(#amqqueue{pid = MPid, - slave_pids = SPids, - sync_slave_pids = SSPids}) -> +actual_queue_nodes(Q) when ?is_amqqueue(Q) -> + MPid = amqqueue:get_pid(Q), + SPids = amqqueue:get_slave_pids(Q), + SSPids = amqqueue:get_sync_slave_pids(Q), Nodes = fun (L) -> [node(Pid) || Pid <- L] end, {case MPid of none -> none; _ -> node(MPid) end, Nodes(SPids), Nodes(SSPids)}. -maybe_auto_sync(Q = #amqqueue{pid = QPid}) -> +-spec maybe_auto_sync(amqqueue:amqqueue()) -> 'ok'. + +maybe_auto_sync(Q) when ?is_amqqueue(Q) -> + QPid = amqqueue:get_pid(Q), case policy(<<"ha-sync-mode">>, Q) of <<"automatic">> -> spawn(fun() -> rabbit_amqqueue:sync_mirrors(QPid) end); @@ -447,23 +463,27 @@ maybe_auto_sync(Q = #amqqueue{pid = QPid}) -> ok end. -sync_queue(Q) -> - rabbit_amqqueue:with( - Q, fun(#amqqueue{pid = QPid, type = classic}) -> - rabbit_amqqueue:sync_mirrors(QPid); - (#amqqueue{type = quorum}) -> - {error, quorum_queue_not_supported} - end). - -cancel_sync_queue(Q) -> - rabbit_amqqueue:with( - Q, fun(#amqqueue{pid = QPid, type = classic}) -> - rabbit_amqqueue:cancel_sync_mirrors(QPid); - (#amqqueue{type = quorum}) -> - {error, quorum_queue_not_supported} - end). - -sync_batch_size(#amqqueue{} = Q) -> +sync_queue(Q0) -> + F = fun + (Q) when ?amqqueue_is_classic(Q) -> + QPid = amqqueue:get_pid(Q), + rabbit_amqqueue:sync_mirrors(QPid); + (Q) when ?amqqueue_is_quorum(Q) -> + {error, quorum_queue_not_supported} + end, + rabbit_amqqueue:with(Q0, F). + +cancel_sync_queue(Q0) -> + F = fun + (Q) when ?amqqueue_is_classic(Q) -> + QPid = amqqueue:get_pid(Q), + rabbit_amqqueue:cancel_sync_mirrors(QPid); + (Q) when ?amqqueue_is_quorum(Q) -> + {error, quorum_queue_not_supported} + end, + rabbit_amqqueue:with(Q0, F). + +sync_batch_size(Q) when ?is_amqqueue(Q) -> case policy(<<"ha-sync-batch-size">>, Q) of none -> %% we need this case because none > 1 == true default_batch_size(); @@ -479,14 +499,23 @@ default_batch_size() -> rabbit_misc:get_env(rabbit, mirroring_sync_batch_size, ?DEFAULT_BATCH_SIZE). -update_mirrors(OldQ = #amqqueue{pid = QPid}, - NewQ = #amqqueue{pid = QPid}) -> +-spec update_mirrors + (amqqueue:amqqueue(), amqqueue:amqqueue()) -> 'ok'. + +update_mirrors(OldQ, NewQ) when ?amqqueue_pids_are_equal(OldQ, NewQ) -> + % Note: we do want to ensure both queues have same pid + QPid = amqqueue:get_pid(OldQ), + QPid = amqqueue:get_pid(NewQ), case {is_mirrored(OldQ), is_mirrored(NewQ)} of {false, false} -> ok; _ -> rabbit_amqqueue:update_mirroring(QPid) end. -update_mirrors(Q = #amqqueue{name = QName}) -> +-spec update_mirrors + (amqqueue:amqqueue()) -> 'ok'. + +update_mirrors(Q) when ?is_amqqueue(Q) -> + QName = amqqueue:get_name(Q), {OldMNode, OldSNodes, _} = actual_queue_nodes(Q), {NewMNode, NewSNodes} = suggested_queue_nodes(Q), OldNodes = [OldMNode | OldSNodes], @@ -512,8 +541,12 @@ update_mirrors(Q = #amqqueue{name = QName}) -> %% We don't just call update_mirrors/2 here since that could decide to %% start a slave for some other reason, and since we are the slave ATM %% that allows complicated deadlocks. -maybe_drop_master_after_sync(Q = #amqqueue{name = QName, - pid = MPid}) -> + +-spec maybe_drop_master_after_sync(amqqueue:amqqueue()) -> 'ok'. + +maybe_drop_master_after_sync(Q) when ?is_amqqueue(Q) -> + QName = amqqueue:get_name(Q), + MPid = amqqueue:get_pid(Q), {DesiredMNode, DesiredSNodes} = suggested_queue_nodes(Q), case node(MPid) of DesiredMNode -> ok; diff --git a/src/rabbit_mirror_queue_slave.erl b/src/rabbit_mirror_queue_slave.erl index bf0a5a7ed0..3697051960 100644 --- a/src/rabbit_mirror_queue_slave.erl +++ b/src/rabbit_mirror_queue_slave.erl @@ -35,8 +35,9 @@ -behaviour(gen_server2). -behaviour(gm). --include("rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include("amqqueue.hrl"). -include("gm_specs.hrl"). %%---------------------------------------------------------------------------- @@ -76,8 +77,9 @@ set_maximum_since_use(QPid, Age) -> info(QPid) -> gen_server2:call(QPid, info, infinity). -init(Q) -> - ?store_proc_name(Q#amqqueue.name), +init(Q) when ?is_amqqueue(Q) -> + QName = amqqueue:get_name(Q), + ?store_proc_name(QName), {ok, {not_started, Q}, hibernate, {backoff, ?HIBERNATE_AFTER_MIN, ?HIBERNATE_AFTER_MIN, ?DESIRED_HIBERNATE}, ?MODULE}. @@ -85,7 +87,8 @@ init(Q) -> go(SPid, sync) -> gen_server2:call(SPid, go, infinity); go(SPid, async) -> gen_server2:cast(SPid, go). -handle_go(Q = #amqqueue{name = QName}) -> +handle_go(Q) when ?is_amqqueue(Q) -> + QName = amqqueue:get_name(Q), %% We join the GM group before we add ourselves to the amqqueue %% record. As a result: %% 1. We can receive msgs from GM that correspond to messages we will @@ -119,10 +122,10 @@ handle_go(Q = #amqqueue{name = QName}) -> ok = rabbit_memory_monitor:register( Self, {rabbit_amqqueue, set_ram_duration_target, [Self]}), {ok, BQ} = application:get_env(backing_queue_module), - Q1 = Q #amqqueue { pid = QPid }, + QPid = amqqueue:get_pid(Q), _ = BQ:delete_crashed(Q), %% For crash recovery - BQS = bq_init(BQ, Q1, new), - State = #state { q = Q1, + BQS = bq_init(BQ, Q, new), + State = #state { q = Q, gm = GM, backing_queue = BQ, backing_queue_state = BQS, @@ -139,7 +142,7 @@ handle_go(Q = #amqqueue{name = QName}) -> }, ok = gm:broadcast(GM, request_depth), ok = gm:validate_members(GM, [GM | [G || {G, _} <- GMPids]]), - rabbit_mirror_queue_misc:maybe_auto_sync(Q1), + rabbit_mirror_queue_misc:maybe_auto_sync(Q), {ok, State}; {stale, StalePid} -> rabbit_mirror_queue_misc:log_warning( @@ -163,8 +166,11 @@ handle_go(Q = #amqqueue{name = QName}) -> init_it(Self, GM, Node, QName) -> case mnesia:read({rabbit_queue, QName}) of - [Q = #amqqueue { pid = QPid, slave_pids = SPids, gm_pids = GMPids, - slave_pids_pending_shutdown = PSPids}] -> + [Q] when ?is_amqqueue(Q) -> + QPid = amqqueue:get_pid(Q), + SPids = amqqueue:get_slave_pids(Q), + GMPids = amqqueue:get_gm_pids(Q), + PSPids = amqqueue:get_slave_pids_pending_shutdown(Q), case [Pid || Pid <- [QPid | SPids], node(Pid) =:= Node] of [] -> stop_pending_slaves(QName, PSPids), add_slave(Q, Self, GM), @@ -175,12 +181,11 @@ init_it(Self, GM, Node, QName) -> end; [SPid] -> case rabbit_mnesia:is_process_alive(SPid) of true -> existing; - false -> GMPids1 = [T || T = {_, S} <- GMPids, - S =/= SPid], - Q1 = Q#amqqueue{ - slave_pids = SPids -- [SPid], - gm_pids = GMPids1}, - add_slave(Q1, Self, GM), + false -> GMPids1 = [T || T = {_, S} <- GMPids, S =/= SPid], + SPids1 = SPids -- [SPid], + Q1 = amqqueue:set_slave_pids(Q, SPids1), + Q2 = amqqueue:set_gm_pids(Q1, GMPids1), + add_slave(Q2, Self, GM), {new, QPid, GMPids1} end end; @@ -212,9 +217,14 @@ stop_pending_slaves(QName, Pids) -> %% Add to the end, so they are in descending order of age, see %% rabbit_mirror_queue_misc:promote_slave/1 -add_slave(Q = #amqqueue { slave_pids = SPids, gm_pids = GMPids }, New, GM) -> - rabbit_mirror_queue_misc:store_updated_slaves( - Q#amqqueue{slave_pids = SPids ++ [New], gm_pids = [{GM, New} | GMPids]}). +add_slave(Q0, New, GM) when ?is_amqqueue(Q0) -> + SPids = amqqueue:get_slave_pids(Q0), + GMPids = amqqueue:get_gm_pids(Q0), + SPids1 = SPids ++ [New], + GMPids1 = [{GM, New} | GMPids], + Q1 = amqqueue:set_slave_pids(Q0, SPids1), + Q2 = amqqueue:set_gm_pids(Q1, GMPids1), + rabbit_mirror_queue_misc:store_updated_slaves(Q2). handle_call(go, _From, {not_started, Q} = NotStarted) -> case handle_go(Q) of @@ -223,10 +233,11 @@ handle_call(go, _From, {not_started, Q} = NotStarted) -> end; handle_call({gm_deaths, DeadGMPids}, From, - State = #state{ gm = GM, - q = Q = #amqqueue{ name = QName, pid = MPid }, - backing_queue = BQ, - backing_queue_state = BQS}) -> + State = #state{ gm = GM, q = Q, + backing_queue = BQ, + backing_queue_state = BQS}) when ?is_amqqueue(Q) -> + QName = amqqueue:get_name(Q), + MPid = amqqueue:get_pid(Q), Self = self(), case rabbit_mirror_queue_misc:remove_from_queue(QName, Self, DeadGMPids) of {error, not_found} -> @@ -269,7 +280,9 @@ handle_call({gm_deaths, DeadGMPids}, From, %% death. That is all process_death does, create %% some traffic. ok = gm:broadcast(GM, process_death), - noreply(State #state { q = Q #amqqueue { pid = Pid } }) + Q1 = amqqueue:set_pid(Q, Pid), + State1 = State#state{q = Q1}, + noreply(State1) end end; @@ -285,20 +298,22 @@ handle_cast(go, {not_started, Q} = NotStarted) -> handle_cast({run_backing_queue, Mod, Fun}, State) -> noreply(run_backing_queue(Mod, Fun, State)); -handle_cast({gm, Instruction}, State = #state{q = #amqqueue { name = QName }}) -> +handle_cast({gm, Instruction}, State = #state{q = Q0}) when ?is_amqqueue(Q0) -> + QName = amqqueue:get_name(Q0), case rabbit_amqqueue:lookup(QName) of - {ok, #amqqueue{slave_pids = SPids}} -> - case lists:member(self(), SPids) of - true -> - handle_process_result(process_instruction(Instruction, State)); - false -> - %% Potentially a duplicated slave caused by a partial partition, - %% will stop as a new slave could start unaware of our presence - {stop, shutdown, State} - end; - {error, not_found} -> - %% Would not expect this to happen after fixing #953 - {stop, shutdown, State} + {ok, Q1} when ?is_amqqueue(Q1) -> + SPids = amqqueue:get_slave_pids(Q1), + case lists:member(self(), SPids) of + true -> + handle_process_result(process_instruction(Instruction, State)); + false -> + %% Potentially a duplicated slave caused by a partial partition, + %% will stop as a new slave could start unaware of our presence + {stop, shutdown, State} + end; + {error, not_found} -> + %% Would not expect this to happen after fixing #953 + {stop, shutdown, State} end; handle_cast({deliver, Delivery = #delivery{sender = Sender, flow = Flow}, true}, @@ -521,11 +536,16 @@ handle_terminate([_SPid], _Reason) -> infos(Items, State) -> [{Item, i(Item, State)} || Item <- Items]. -i(pid, _State) -> self(); -i(name, #state { q = #amqqueue { name = Name } }) -> Name; -i(master_pid, #state { q = #amqqueue { pid = MPid } }) -> MPid; -i(is_synchronised, #state { depth_delta = DD }) -> DD =:= 0; -i(Item, _State) -> throw({bad_argument, Item}). +i(pid, _State) -> + self(); +i(name, #state{q = Q}) when ?is_amqqueue(Q) -> + amqqueue:get_name(Q); +i(master_pid, #state{q = Q}) when ?is_amqqueue(Q) -> + amqqueue:get_pid(Q); +i(is_synchronised, #state{depth_delta = DD}) -> + DD =:= 0; +i(Item, _State) -> + throw({bad_argument, Item}). bq_init(BQ, Q, Recover) -> Self = self(), @@ -542,6 +562,18 @@ run_backing_queue(Mod, Fun, State = #state { backing_queue = BQ, backing_queue_state = BQS }) -> State #state { backing_queue_state = BQ:invoke(Mod, Fun, BQS) }. +%% This feature was used by `rabbit_amqqueue_process` and +%% `rabbit_mirror_queue_slave` up-to and including RabbitMQ 3.7.x. It is +%% unused in 3.8.x and thus deprecated. We keep it to support in-place +%% upgrades to 3.8.x (i.e. mixed-version clusters), but it is a no-op +%% starting with that version. +send_mandatory(#delivery{mandatory = false}) -> + ok; +send_mandatory(#delivery{mandatory = true, + sender = SenderPid, + msg_seq_no = MsgSeqNo}) -> + gen_server2:cast(SenderPid, {mandatory_received, MsgSeqNo}). + send_or_record_confirm(_, #delivery{ confirm = false }, MS, _State) -> MS; send_or_record_confirm(published, #delivery { sender = ChPid, @@ -550,7 +582,7 @@ send_or_record_confirm(published, #delivery { sender = ChPid, message = #basic_message { id = MsgId, is_persistent = true } }, - MS, #state { q = #amqqueue { durable = true } }) -> + MS, #state{q = Q}) when ?amqqueue_is_durable(Q) -> maps:put(MsgId, {published, ChPid, MsgSeqNo} , MS); send_or_record_confirm(_Status, #delivery { sender = ChPid, confirm = true, @@ -595,7 +627,7 @@ handle_process_result({stop, State}) -> {stop, normal, State}. -spec promote_me({pid(), term()}, #state{}) -> no_return(). -promote_me(From, #state { q = Q = #amqqueue { name = QName }, +promote_me(From, #state { q = Q0, gm = GM, backing_queue = BQ, backing_queue_state = BQS, @@ -603,13 +635,14 @@ promote_me(From, #state { q = Q = #amqqueue { name = QName }, sender_queues = SQ, msg_id_ack = MA, msg_id_status = MS, - known_senders = KS }) -> + known_senders = KS}) when ?is_amqqueue(Q0) -> + QName = amqqueue:get_name(Q0), rabbit_mirror_queue_misc:log_info(QName, "Promoting slave ~s to master~n", [rabbit_misc:pid_to_string(self())]), - Q1 = Q #amqqueue { pid = self() }, - {ok, CPid} = rabbit_mirror_queue_coordinator:start_link( - Q1, GM, rabbit_mirror_queue_master:sender_death_fun(), - rabbit_mirror_queue_master:depth_fun()), + Q1 = amqqueue:set_pid(Q0, self()), + DeathFun = rabbit_mirror_queue_master:sender_death_fun(), + DepthFun = rabbit_mirror_queue_master:depth_fun(), + {ok, CPid} = rabbit_mirror_queue_coordinator:start_link(Q1, GM, DeathFun, DepthFun), true = unlink(GM), gen_server2:reply(From, {promote, CPid}), @@ -700,11 +733,13 @@ promote_me(From, #state { q = Q = #amqqueue { name = QName }, Q1, rabbit_mirror_queue_master, MasterState, RateTRef, Deliveries, KS1, MTC). -%% We need to send an ack for these messages since the channel is waiting +%% We reset mandatory to false here because we will have sent the +%% mandatory_received already as soon as we got the message. We also +%% need to send an ack for these messages since the channel is waiting %% for one for the via-GM case and we will not now receive one. promote_delivery(Delivery = #delivery{sender = Sender, flow = Flow}) -> maybe_flow_ack(Sender, Flow), - Delivery. + Delivery#delivery{mandatory = false}. noreply(State) -> {NewState, Timeout} = next_state(State), @@ -823,6 +858,7 @@ maybe_enqueue_message( Delivery = #delivery { message = #basic_message { id = MsgId }, sender = ChPid }, State = #state { sender_queues = SQ, msg_id_status = MS }) -> + send_mandatory(Delivery), %% must do this before confirms State1 = ensure_monitoring(ChPid, State), %% We will never see {published, ChPid, MsgSeqNo} here. case maps:find(MsgId, MS) of @@ -1040,19 +1076,22 @@ update_ram_duration(BQ, BQS) -> rabbit_memory_monitor:report_ram_duration(self(), RamDuration), BQ:set_ram_duration_target(DesiredDuration, BQS1). -record_synchronised(#amqqueue { name = QName }) -> +record_synchronised(Q0) when ?is_amqqueue(Q0) -> + QName = amqqueue:get_name(Q0), Self = self(), - case rabbit_misc:execute_mnesia_transaction( - fun () -> - case mnesia:read({rabbit_queue, QName}) of - [] -> - ok; - [Q1 = #amqqueue { sync_slave_pids = SSPids }] -> - Q2 = Q1#amqqueue{sync_slave_pids = [Self | SSPids]}, - rabbit_mirror_queue_misc:store_updated_slaves(Q2), - {ok, Q2} - end - end) of - ok -> ok; - {ok, Q} -> rabbit_mirror_queue_misc:maybe_drop_master_after_sync(Q) + F = fun () -> + case mnesia:read({rabbit_queue, QName}) of + [] -> + ok; + [Q1] when ?is_amqqueue(Q1) -> + SSPids = amqqueue:get_sync_slave_pids(Q1), + SSPids1 = [Self | SSPids], + Q2 = amqqueue:set_sync_slave_pids(Q1, SSPids1), + rabbit_mirror_queue_misc:store_updated_slaves(Q2), + {ok, Q2} + end + end, + case rabbit_misc:execute_mnesia_transaction(F) of + ok -> ok; + {ok, Q2} -> rabbit_mirror_queue_misc:maybe_drop_master_after_sync(Q2) end. diff --git a/src/rabbit_mirror_queue_sync.erl b/src/rabbit_mirror_queue_sync.erl index d206650d0f..2d7ce7edb2 100644 --- a/src/rabbit_mirror_queue_sync.erl +++ b/src/rabbit_mirror_queue_sync.erl @@ -59,8 +59,19 @@ -type slave_sync_state() :: {[{rabbit_types:msg_id(), ack()}], timer:tref(), bqs()}. +%% --------------------------------------------------------------------------- +%% Master + -spec master_prepare(reference(), rabbit_amqqueue:name(), log_fun(), [pid()]) -> pid(). + +master_prepare(Ref, QName, Log, SPids) -> + MPid = self(), + spawn_link(fun () -> + ?store_proc_name(QName), + syncer(Ref, Log, MPid, SPids) + end). + -spec master_go(pid(), reference(), log_fun(), rabbit_mirror_queue_master:stats_fun(), rabbit_mirror_queue_master:stats_fun(), @@ -69,21 +80,6 @@ {'already_synced', bqs()} | {'ok', bqs()} | {'shutdown', any(), bqs()} | {'sync_died', any(), bqs()}. --spec slave(non_neg_integer(), reference(), timer:tref(), pid(), - bq(), bqs(), fun((bq(), bqs()) -> {timer:tref(), bqs()})) -> - 'denied' | - {'ok' | 'failed', slave_sync_state()} | - {'stop', any(), slave_sync_state()}. - -%% --------------------------------------------------------------------------- -%% Master - -master_prepare(Ref, QName, Log, SPids) -> - MPid = self(), - spawn_link(fun () -> - ?store_proc_name(QName), - syncer(Ref, Log, MPid, SPids) - end). master_go(Syncer, Ref, Log, HandleInfo, EmitStats, SyncBatchSize, BQ, BQS) -> Args = {Syncer, Ref, Log, HandleInfo, EmitStats, rabbit_misc:get_parent()}, @@ -321,6 +317,12 @@ wait_for_resources(Ref, SPids) -> %% --------------------------------------------------------------------------- %% Slave +-spec slave(non_neg_integer(), reference(), timer:tref(), pid(), + bq(), bqs(), fun((bq(), bqs()) -> {timer:tref(), bqs()})) -> + 'denied' | + {'ok' | 'failed', slave_sync_state()} | + {'stop', any(), slave_sync_state()}. + slave(0, Ref, _TRef, Syncer, _BQ, _BQS, _UpdateRamDuration) -> Syncer ! {sync_deny, Ref, self()}, denied; diff --git a/src/rabbit_mnesia.erl b/src/rabbit_mnesia.erl index 4760a9432e..61b9e10e70 100644 --- a/src/rabbit_mnesia.erl +++ b/src/rabbit_mnesia.erl @@ -16,7 +16,8 @@ -module(rabbit_mnesia). --export([init/0, +-export([%% Main interface + init/0, join_cluster/2, reset/0, force_reset/0, @@ -25,6 +26,7 @@ forget_cluster_node/2, force_load_next_boot/0, + %% Various queries to get the status of the db status/0, is_clustered/0, on_running_node/1, @@ -35,11 +37,13 @@ dir/0, cluster_status_from_mnesia/0, + %% Operations on the db and utils, mainly used in `rabbit_upgrade' and `rabbit' init_db_unchecked/2, copy_db/1, check_cluster_consistency/0, ensure_mnesia_dir/0, + %% Hooks used in `rabbit_node_monitor' on_node_up/1, on_node_down/1 ]). @@ -61,45 +65,12 @@ -type node_type() :: disc | ram. -type cluster_status() :: {[node()], [node()], [node()]}. -%% Main interface --spec init() -> 'ok'. --spec join_cluster(node(), node_type()) - -> ok | {ok, already_member} | {error, {inconsistent_cluster, string()}}. --spec reset() -> 'ok'. --spec force_reset() -> 'ok'. --spec update_cluster_nodes(node()) -> 'ok'. --spec change_cluster_node_type(node_type()) -> 'ok'. --spec forget_cluster_node(node(), boolean()) -> 'ok'. --spec force_load_next_boot() -> 'ok'. - -%% Various queries to get the status of the db --spec status() -> [{'nodes', [{node_type(), [node()]}]} | - {'running_nodes', [node()]} | - {'partitions', [{node(), [node()]}]}]. --spec is_clustered() -> boolean(). --spec on_running_node(pid()) -> boolean(). --spec is_process_alive(pid() | {atom(), node()}) -> boolean(). --spec is_registered_process_alive(atom()) -> boolean(). --spec cluster_nodes('all' | 'disc' | 'ram' | 'running') -> [node()]. --spec node_type() -> node_type(). --spec dir() -> file:filename(). --spec cluster_status_from_mnesia() -> rabbit_types:ok_or_error2( - cluster_status(), any()). - -%% Operations on the db and utils, mainly used in `rabbit_upgrade' and `rabbit' --spec init_db_unchecked([node()], node_type()) -> 'ok'. --spec copy_db(file:filename()) -> rabbit_types:ok_or_error(any()). --spec check_cluster_consistency() -> 'ok'. --spec ensure_mnesia_dir() -> 'ok'. - -%% Hooks used in `rabbit_node_monitor' --spec on_node_up(node()) -> 'ok'. --spec on_node_down(node()) -> 'ok'. - %%---------------------------------------------------------------------------- %% Main interface %%---------------------------------------------------------------------------- +-spec init() -> 'ok'. + init() -> ensure_mnesia_running(), ensure_mnesia_dir(), @@ -165,20 +136,14 @@ init_from_config() -> {DiscoveredNodes, NodeType} = case rabbit_peer_discovery:discover_cluster_nodes() of {ok, {Nodes, Type} = Config} - when is_list(Nodes) andalso (Type == disc orelse Type == disk orelse Type == ram) -> + when is_list(Nodes) andalso + (Type == disc orelse Type == disk orelse Type == ram) -> case lists:foldr(FindBadNodeNames, [], Nodes) of [] -> Config; BadNames -> e({invalid_cluster_node_names, BadNames}) end; {ok, {_, BadType}} when BadType /= disc andalso BadType /= ram -> e({invalid_cluster_node_type, BadType}); - {ok, Nodes} when is_list(Nodes) -> - %% The legacy syntax (a nodes list without the node - %% type) is unsupported. - case lists:foldr(FindBadNodeNames, [], Nodes) of - [] -> e(cluster_node_type_mandatory); - _ -> e(invalid_cluster_nodes_conf) - end; {ok, _} -> e(invalid_cluster_nodes_conf) end, @@ -228,6 +193,10 @@ join_discovered_peers(TryNodes, NodeType) -> %% Note that we make no attempt to verify that the nodes provided are %% all in the same cluster, we simply pick the first online node and %% we cluster to its cluster. + +-spec join_cluster(node(), node_type()) + -> ok | {ok, already_member} | {error, {inconsistent_cluster, string()}}. + join_cluster(DiscoveryNode, NodeType) -> ensure_mnesia_not_running(), ensure_mnesia_dir(), @@ -275,11 +244,16 @@ join_cluster(DiscoveryNode, NodeType) -> %% return node to its virgin state, where it is not member of any %% cluster, has no cluster configuration, no local database, and no %% persisted messages + +-spec reset() -> 'ok'. + reset() -> ensure_mnesia_not_running(), rabbit_log:info("Resetting Rabbit~n", []), reset_gracefully(). +-spec force_reset() -> 'ok'. + force_reset() -> ensure_mnesia_not_running(), rabbit_log:info("Resetting Rabbit forcefully~n", []), @@ -310,6 +284,8 @@ wipe() -> ok = rabbit_node_monitor:reset_cluster_status(), ok. +-spec change_cluster_node_type(node_type()) -> 'ok'. + change_cluster_node_type(Type) -> ensure_mnesia_not_running(), ensure_mnesia_dir(), @@ -327,6 +303,8 @@ change_cluster_node_type(Type) -> ok = reset(), ok = join_cluster(Node, Type). +-spec update_cluster_nodes(node()) -> 'ok'. + update_cluster_nodes(DiscoveryNode) -> ensure_mnesia_not_running(), ensure_mnesia_dir(), @@ -353,6 +331,9 @@ update_cluster_nodes(DiscoveryNode) -> %% * This node was, at the best of our knowledge (see comment below) %% the last or second to last after the node we're removing to go %% down + +-spec forget_cluster_node(node(), boolean()) -> 'ok'. + forget_cluster_node(Node, RemoveWhenOffline) -> forget_cluster_node(Node, RemoveWhenOffline, true). @@ -407,6 +388,10 @@ remove_node_offline_node(Node) -> %% Queries %%---------------------------------------------------------------------------- +-spec status() -> [{'nodes', [{node_type(), [node()]}]} | + {'running_nodes', [node()]} | + {'partitions', [{node(), [node()]}]}]. + status() -> IfNonEmpty = fun (_, []) -> []; (Type, Nodes) -> [{Type, Nodes}] @@ -427,15 +412,22 @@ mnesia_partitions(Nodes) -> is_running() -> mnesia:system_info(is_running) =:= yes. +-spec is_clustered() -> boolean(). + is_clustered() -> AllNodes = cluster_nodes(all), AllNodes =/= [] andalso AllNodes =/= [node()]. +-spec on_running_node(pid()) -> boolean(). + on_running_node(Pid) -> lists:member(node(Pid), cluster_nodes(running)). %% This requires the process be in the same running cluster as us %% (i.e. not partitioned or some random node). %% %% See also rabbit_misc:is_process_alive/1 which does not. + +-spec is_process_alive(pid() | {atom(), node()}) -> boolean(). + is_process_alive(Pid) when is_pid(Pid) -> on_running_node(Pid) andalso rpc:call(node(Pid), erlang, is_process_alive, [Pid]) =:= true; @@ -443,14 +435,22 @@ is_process_alive({Name, Node}) -> lists:member(Node, cluster_nodes(running)) andalso rpc:call(Node, rabbit_mnesia, is_registered_process_alive, [Name]) =:= true. +-spec is_registered_process_alive(atom()) -> boolean(). + is_registered_process_alive(Name) -> is_pid(whereis(Name)). +-spec cluster_nodes('all' | 'disc' | 'ram' | 'running') -> [node()]. + cluster_nodes(WhichNodes) -> cluster_status(WhichNodes). %% This function is the actual source of information, since it gets %% the data from mnesia. Obviously it'll work only when mnesia is %% running. + +-spec cluster_status_from_mnesia() -> rabbit_types:ok_or_error2( + cluster_status(), any()). + cluster_status_from_mnesia() -> case is_running() of false -> @@ -505,6 +505,8 @@ node_info() -> mnesia:system_info(protocol_version), cluster_status_from_mnesia()}. +-spec node_type() -> node_type(). + node_type() -> {_AllNodes, DiscNodes, _RunningNodes} = rabbit_node_monitor:read_cluster_status(), @@ -513,6 +515,8 @@ node_type() -> false -> ram end. +-spec dir() -> file:filename(). + dir() -> mnesia:system_info(directory). %%---------------------------------------------------------------------------- @@ -547,10 +551,13 @@ init_db(ClusterNodes, NodeType, CheckOtherNodes) -> ok = rabbit_table:wait_for_replicated(_Retry = true), ok = rabbit_table:create_local_copy(NodeType) end, + ensure_feature_flags_are_in_sync(Nodes), ensure_schema_integrity(), rabbit_node_monitor:update_cluster_status(), ok. +-spec init_db_unchecked([node()], node_type()) -> 'ok'. + init_db_unchecked(ClusterNodes, NodeType) -> init_db(ClusterNodes, NodeType, false). @@ -581,6 +588,8 @@ init_db_with_mnesia(ClusterNodes, NodeType, stop_mnesia() end. +-spec ensure_mnesia_dir() -> 'ok'. + ensure_mnesia_dir() -> MnesiaDir = dir() ++ "/", case filelib:ensure_dir(MnesiaDir) of @@ -612,6 +621,14 @@ ensure_mnesia_not_running() -> throw({error, mnesia_unexpectedly_running}) end. +ensure_feature_flags_are_in_sync(Nodes) -> + case rabbit_feature_flags:sync_feature_flags_with_cluster(Nodes) of + ok -> + ok; + {error, Reason} -> + throw({error, {incompatible_feature_flags, Reason}}) + end. + ensure_schema_integrity() -> case rabbit_table:check_schema_integrity(_Retry = true) of ok -> @@ -620,6 +637,8 @@ ensure_schema_integrity() -> throw({error, {schema_integrity_check_failed, Reason}}) end. +-spec copy_db(file:filename()) -> rabbit_types:ok_or_error(any()). + copy_db(Destination) -> ok = ensure_mnesia_not_running(), rabbit_file:recursive_copy(dir(), Destination). @@ -627,6 +646,8 @@ copy_db(Destination) -> force_load_filename() -> filename:join(dir(), "force_load"). +-spec force_load_next_boot() -> 'ok'. + force_load_next_boot() -> rabbit_file:write_file(force_load_filename(), <<"">>). @@ -639,6 +660,9 @@ maybe_force_load() -> %% This does not guarantee us much, but it avoids some situations that %% will definitely end up badly + +-spec check_cluster_consistency() -> 'ok'. + check_cluster_consistency() -> %% We want to find 0 or 1 consistent nodes. case lists:foldl( @@ -708,12 +732,16 @@ remote_node_info(Node) -> %% Hooks for `rabbit_node_monitor' %%-------------------------------------------------------------------- +-spec on_node_up(node()) -> 'ok'. + on_node_up(Node) -> case running_disc_nodes() of [Node] -> rabbit_log:info("cluster contains disc nodes again~n"); _ -> ok end. +-spec on_node_down(node()) -> 'ok'. + on_node_down(_Node) -> case running_disc_nodes() of [] -> rabbit_log:info("only running disc node went down~n"); @@ -857,12 +885,12 @@ change_extra_db_nodes(ClusterNodes0, CheckOtherNodes) -> check_consistency(Node, OTP, Rabbit, ProtocolVersion) -> rabbit_misc:sequence_error( [check_mnesia_or_otp_consistency(Node, ProtocolVersion, OTP), - check_rabbit_consistency(Rabbit)]). + check_rabbit_consistency(Node, Rabbit)]). check_consistency(Node, OTP, Rabbit, ProtocolVersion, Status) -> rabbit_misc:sequence_error( [check_mnesia_or_otp_consistency(Node, ProtocolVersion, OTP), - check_rabbit_consistency(Rabbit), + check_rabbit_consistency(Node, Rabbit), check_nodes_consistency(Node, Status)]). check_nodes_consistency(Node, RemoteStatus = {RemoteAllNodes, _, _}) -> @@ -923,10 +951,12 @@ with_running_or_clean_mnesia(Fun) -> Result end. -check_rabbit_consistency(Remote) -> - rabbit_version:check_version_consistency( - rabbit_misc:version(), Remote, "Rabbit", - fun rabbit_misc:version_minor_equivalent/2). +check_rabbit_consistency(RemoteNode, RemoteVersion) -> + rabbit_misc:sequence_error( + [rabbit_version:check_version_consistency( + rabbit_misc:version(), RemoteVersion, "Rabbit", + fun rabbit_misc:version_minor_equivalent/2), + rabbit_feature_flags:check_node_compatibility(RemoteNode)]). %% This is fairly tricky. We want to know if the node is in the state %% that a `reset' would leave it in. We cannot simply check if the @@ -989,6 +1019,8 @@ nodes_incl_me(Nodes) -> lists:usort([node()|Nodes]). nodes_excl_me(Nodes) -> Nodes -- [node()]. +-spec e(any()) -> no_return(). + e(Tag) -> throw({error, {Tag, error_description(Tag)}}). error_description({invalid_cluster_node_names, BadNames}) -> @@ -998,9 +1030,6 @@ error_description({invalid_cluster_node_type, BadType}) -> "In the 'cluster_nodes' configuration key, the node type is invalid " "(expected 'disc' or 'ram'): " ++ lists:flatten(io_lib:format("~p", [BadType])); -error_description(cluster_node_type_mandatory) -> - "The 'cluster_nodes' configuration key must indicate the node type: " - "either {[...], disc} or {[...], ram}"; error_description(invalid_cluster_nodes_conf) -> "The 'cluster_nodes' configuration key is invalid, it must be of the " "form {[Nodes], Type}, where Nodes is a list of node names and " diff --git a/src/rabbit_mnesia_rename.erl b/src/rabbit_mnesia_rename.erl index c6648ac802..76e120f292 100644 --- a/src/rabbit_mnesia_rename.erl +++ b/src/rabbit_mnesia_rename.erl @@ -44,9 +44,6 @@ %%---------------------------------------------------------------------------- -spec rename(node(), [{node(), node()}]) -> 'ok'. --spec maybe_finish([node()]) -> 'ok'. - -%%---------------------------------------------------------------------------- rename(Node, NodeMapList) -> try @@ -139,6 +136,8 @@ restore_backup(Backup) -> stop_mnesia(), rabbit_mnesia:force_load_next_boot(). +-spec maybe_finish([node()]) -> 'ok'. + maybe_finish(AllNodes) -> case rabbit_file:read_term_file(rename_config_name()) of {ok, [{FromNode, ToNode}]} -> finish(FromNode, ToNode, AllNodes); diff --git a/src/rabbit_msg_file.erl b/src/rabbit_msg_file.erl index f2496cd71f..f3ec7b78cc 100644 --- a/src/rabbit_msg_file.erl +++ b/src/rabbit_msg_file.erl @@ -41,15 +41,10 @@ fun (({rabbit_types:msg_id(), msg_size(), position(), binary()}, A) -> A). +%%---------------------------------------------------------------------------- + -spec append(io_device(), rabbit_types:msg_id(), msg()) -> rabbit_types:ok_or_error2(msg_size(), any()). --spec read(io_device(), msg_size()) -> - rabbit_types:ok_or_error2({rabbit_types:msg_id(), msg()}, - any()). --spec scan(io_device(), file_size(), message_accumulator(A), A) -> - {'ok', A, position()}. - -%%---------------------------------------------------------------------------- append(FileHdl, MsgId, MsgBody) when is_binary(MsgId) andalso size(MsgId) =:= ?MSG_ID_SIZE_BYTES -> @@ -65,6 +60,10 @@ append(FileHdl, MsgId, MsgBody) KO -> KO end. +-spec read(io_device(), msg_size()) -> + rabbit_types:ok_or_error2({rabbit_types:msg_id(), msg()}, + any()). + read(FileHdl, TotalSize) -> Size = TotalSize - ?FILE_PACKING_ADJUSTMENT, BodyBinSize = Size - ?MSG_ID_SIZE_BYTES, @@ -77,6 +76,9 @@ read(FileHdl, TotalSize) -> KO -> KO end. +-spec scan(io_device(), file_size(), message_accumulator(A), A) -> + {'ok', A, position()}. + scan(FileHdl, FileSize, Fun, Acc) when FileSize >= 0 -> scan(FileHdl, FileSize, <<>>, 0, 0, Fun, Acc). diff --git a/src/rabbit_msg_store.erl b/src/rabbit_msg_store.erl index bae8364614..337064ad39 100644 --- a/src/rabbit_msg_store.erl +++ b/src/rabbit_msg_store.erl @@ -173,33 +173,6 @@ -type maybe_close_fds_fun() :: 'undefined' | fun (() -> 'ok'). -type deletion_thunk() :: fun (() -> boolean()). --spec start_link - (atom(), file:filename(), [binary()] | 'undefined', - {msg_ref_delta_gen(A), A}) -> rabbit_types:ok_pid_or_error(). --spec successfully_recovered_state(server()) -> boolean(). --spec client_init(server(), client_ref(), maybe_msg_id_fun(), - maybe_close_fds_fun()) -> client_msstate(). --spec client_terminate(client_msstate()) -> 'ok'. --spec client_delete_and_terminate(client_msstate()) -> 'ok'. --spec client_ref(client_msstate()) -> client_ref(). --spec close_all_indicated - (client_msstate()) -> rabbit_types:ok(client_msstate()). --spec write(rabbit_types:msg_id(), msg(), client_msstate()) -> 'ok'. --spec write_flow(rabbit_types:msg_id(), msg(), client_msstate()) -> 'ok'. --spec read(rabbit_types:msg_id(), client_msstate()) -> - {rabbit_types:ok(msg()) | 'not_found', client_msstate()}. --spec contains(rabbit_types:msg_id(), client_msstate()) -> boolean(). --spec remove([rabbit_types:msg_id()], client_msstate()) -> 'ok'. - --spec set_maximum_since_use(server(), non_neg_integer()) -> 'ok'. --spec has_readers(non_neg_integer(), gc_state()) -> boolean(). --spec combine_files(non_neg_integer(), non_neg_integer(), gc_state()) -> - deletion_thunk(). --spec delete_file(non_neg_integer(), gc_state()) -> deletion_thunk(). --spec force_recovery(file:filename(), server()) -> 'ok'. --spec transform_dir(file:filename(), server(), - fun ((any()) -> (rabbit_types:ok_or_error2(msg(), any())))) -> 'ok'. - %%---------------------------------------------------------------------------- %% We run GC whenever (garbage / sum_file_size) > ?GARBAGE_FRACTION @@ -472,6 +445,10 @@ %% public API %%---------------------------------------------------------------------------- +-spec start_link + (atom(), file:filename(), [binary()] | 'undefined', + {msg_ref_delta_gen(A), A}) -> rabbit_types:ok_pid_or_error(). + start_link(Type, Dir, ClientRefs, StartupFunState) when is_atom(Type) -> gen_server2:start_link(?MODULE, [Type, Dir, ClientRefs, StartupFunState], @@ -482,9 +459,14 @@ start_global_store_link(Type, Dir, ClientRefs, StartupFunState) when is_atom(Typ [Type, Dir, ClientRefs, StartupFunState], [{timeout, infinity}]). +-spec successfully_recovered_state(server()) -> boolean(). + successfully_recovered_state(Server) -> gen_server2:call(Server, successfully_recovered_state, infinity). +-spec client_init(server(), client_ref(), maybe_msg_id_fun(), + maybe_close_fds_fun()) -> client_msstate(). + client_init(Server, Ref, MsgOnDiskFun, CloseFDsFun) when is_pid(Server); is_atom(Server) -> {IState, IModule, Dir, GCPid, FileHandlesEts, FileSummaryEts, CurFileCacheEts, FlyingEts} = @@ -506,17 +488,25 @@ client_init(Server, Ref, MsgOnDiskFun, CloseFDsFun) when is_pid(Server); is_atom flying_ets = FlyingEts, credit_disc_bound = CreditDiscBound }. +-spec client_terminate(client_msstate()) -> 'ok'. + client_terminate(CState = #client_msstate { client_ref = Ref }) -> close_all_handles(CState), ok = server_call(CState, {client_terminate, Ref}). +-spec client_delete_and_terminate(client_msstate()) -> 'ok'. + client_delete_and_terminate(CState = #client_msstate { client_ref = Ref }) -> close_all_handles(CState), ok = server_cast(CState, {client_dying, Ref}), ok = server_cast(CState, {client_delete, Ref}). +-spec client_ref(client_msstate()) -> client_ref(). + client_ref(#client_msstate { client_ref = Ref }) -> Ref. +-spec write_flow(rabbit_types:msg_id(), msg(), client_msstate()) -> 'ok'. + write_flow(MsgId, Msg, CState = #client_msstate { server = Server, @@ -528,8 +518,13 @@ write_flow(MsgId, Msg, credit_flow:send(Server, CreditDiscBound), client_write(MsgId, Msg, flow, CState). +-spec write(rabbit_types:msg_id(), msg(), client_msstate()) -> 'ok'. + write(MsgId, Msg, CState) -> client_write(MsgId, Msg, noflow, CState). +-spec read(rabbit_types:msg_id(), client_msstate()) -> + {rabbit_types:ok(msg()) | 'not_found', client_msstate()}. + read(MsgId, CState = #client_msstate { cur_file_cache_ets = CurFileCacheEts }) -> file_handle_cache_stats:update(msg_store_read), @@ -545,12 +540,19 @@ read(MsgId, {{ok, Msg}, CState} end. +-spec contains(rabbit_types:msg_id(), client_msstate()) -> boolean(). + contains(MsgId, CState) -> server_call(CState, {contains, MsgId}). + +-spec remove([rabbit_types:msg_id()], client_msstate()) -> 'ok'. + remove([], _CState) -> ok; remove(MsgIds, CState = #client_msstate { client_ref = CRef }) -> [client_update_flying(-1, MsgId, CState) || MsgId <- MsgIds], server_cast(CState, {remove, CRef, MsgIds}). +-spec set_maximum_since_use(server(), non_neg_integer()) -> 'ok'. + set_maximum_since_use(Server, Age) when is_pid(Server); is_atom(Server) -> gen_server2:cast(Server, {set_maximum_since_use, Age}). @@ -1447,6 +1449,9 @@ safe_file_delete(File, Dir, FileHandlesEts) -> true end. +-spec close_all_indicated + (client_msstate()) -> rabbit_types:ok(client_msstate()). + close_all_indicated(#client_msstate { file_handles_ets = FileHandlesEts, client_ref = Ref } = CState) -> @@ -1965,11 +1970,16 @@ cleanup_after_file_deletion(File, %% garbage collection / compaction / aggregation -- external %%---------------------------------------------------------------------------- +-spec has_readers(non_neg_integer(), gc_state()) -> boolean(). + has_readers(File, #gc_state { file_summary_ets = FileSummaryEts }) -> [#file_summary { locked = true, readers = Count }] = ets:lookup(FileSummaryEts, File), Count /= 0. +-spec combine_files(non_neg_integer(), non_neg_integer(), gc_state()) -> + deletion_thunk(). + combine_files(Source, Destination, State = #gc_state { file_summary_ets = FileSummaryEts, file_handles_ets = FileHandlesEts, @@ -2046,6 +2056,8 @@ combine_files(Source, Destination, gen_server2:cast(Server, {combine_files, Source, Destination, Reclaimed}), safe_file_delete_fun(Source, Dir, FileHandlesEts). +-spec delete_file(non_neg_integer(), gc_state()) -> deletion_thunk(). + delete_file(File, State = #gc_state { file_summary_ets = FileSummaryEts, file_handles_ets = FileHandlesEts, dir = Dir, @@ -2127,6 +2139,8 @@ copy_messages(WorkList, InitOffset, FinalOffset, SourceHdl, DestinationHdl, {destination, Destination}]} end. +-spec force_recovery(file:filename(), server()) -> 'ok'. + force_recovery(BaseDir, Store) -> Dir = filename:join(BaseDir, atom_to_list(Store)), case file:delete(filename:join(Dir, ?CLEAN_FILENAME)) of @@ -2142,6 +2156,9 @@ foreach_file(D, Fun, Files) -> foreach_file(D1, D2, Fun, Files) -> [ok = Fun(filename:join(D1, File), filename:join(D2, File)) || File <- Files]. +-spec transform_dir(file:filename(), server(), + fun ((any()) -> (rabbit_types:ok_or_error2(msg(), any())))) -> 'ok'. + transform_dir(BaseDir, Store, TransformFun) -> Dir = filename:join(BaseDir, atom_to_list(Store)), TmpDir = filename:join(Dir, ?TRANSFORM_TMP), diff --git a/src/rabbit_msg_store_gc.erl b/src/rabbit_msg_store_gc.erl index d11d110abf..4b8d95b535 100644 --- a/src/rabbit_msg_store_gc.erl +++ b/src/rabbit_msg_store_gc.erl @@ -37,31 +37,34 @@ -spec start_link(rabbit_msg_store:gc_state()) -> rabbit_types:ok_pid_or_error(). --spec combine(pid(), rabbit_msg_store:file_num(), - rabbit_msg_store:file_num()) -> 'ok'. --spec delete(pid(), rabbit_msg_store:file_num()) -> 'ok'. --spec no_readers(pid(), rabbit_msg_store:file_num()) -> 'ok'. --spec stop(pid()) -> 'ok'. --spec set_maximum_since_use(pid(), non_neg_integer()) -> 'ok'. - -%%---------------------------------------------------------------------------- start_link(MsgStoreState) -> gen_server2:start_link(?MODULE, [MsgStoreState], [{timeout, infinity}]). +-spec combine(pid(), rabbit_msg_store:file_num(), + rabbit_msg_store:file_num()) -> 'ok'. + combine(Server, Source, Destination) -> gen_server2:cast(Server, {combine, Source, Destination}). +-spec delete(pid(), rabbit_msg_store:file_num()) -> 'ok'. + delete(Server, File) -> gen_server2:cast(Server, {delete, File}). +-spec no_readers(pid(), rabbit_msg_store:file_num()) -> 'ok'. + no_readers(Server, File) -> gen_server2:cast(Server, {no_readers, File}). +-spec stop(pid()) -> 'ok'. + stop(Server) -> gen_server2:call(Server, stop, infinity). +-spec set_maximum_since_use(pid(), non_neg_integer()) -> 'ok'. + set_maximum_since_use(Pid, Age) -> gen_server2:cast(Pid, {set_maximum_since_use, Age}). diff --git a/src/rabbit_networking.erl b/src/rabbit_networking.erl index 6131e2f294..809b999b98 100644 --- a/src/rabbit_networking.erl +++ b/src/rabbit_networking.erl @@ -35,7 +35,8 @@ connection_info/1, connection_info/2, connection_info_all/0, connection_info_all/1, emit_connection_info_all/4, emit_connection_info_local/3, - close_connection/2, handshake/2, tcp_host/1]). + close_connection/2, force_connection_event_refresh/1, + handshake/2, tcp_host/1]). %% Used by TCP-based transports, e.g. STOMP adapter -export([tcp_listener_addresses/1, tcp_listener_spec/9, @@ -43,6 +44,8 @@ -export([tcp_listener_started/4, tcp_listener_stopped/4]). +-deprecated([{force_connection_event_refresh, 1, eventually}]). + %% Internal -export([connections_local/0]). @@ -67,51 +70,7 @@ -type protocol() :: atom(). -type label() :: string(). --spec start_tcp_listener( - listener_config(), integer()) -> 'ok' | {'error', term()}. --spec start_ssl_listener( - listener_config(), rabbit_types:infos(), integer()) -> 'ok' | {'error', term()}. --spec stop_tcp_listener(listener_config()) -> 'ok'. --spec active_listeners() -> [rabbit_types:listener()]. --spec node_listeners(node()) -> [rabbit_types:listener()]. --spec register_connection(pid()) -> ok. --spec unregister_connection(pid()) -> ok. --spec connections() -> [rabbit_types:connection()]. --spec connections_local() -> [rabbit_types:connection()]. --spec connection_info_keys() -> rabbit_types:info_keys(). --spec connection_info(rabbit_types:connection()) -> rabbit_types:infos(). --spec connection_info(rabbit_types:connection(), rabbit_types:info_keys()) -> - rabbit_types:infos(). --spec connection_info_all() -> [rabbit_types:infos()]. --spec connection_info_all(rabbit_types:info_keys()) -> - [rabbit_types:infos()]. --spec close_connection(pid(), string()) -> 'ok'. - --spec on_node_down(node()) -> 'ok'. --spec tcp_listener_addresses(listener_config()) -> [address()]. --spec tcp_listener_spec - (name_prefix(), address(), [gen_tcp:listen_option()], module(), module(), - protocol(), any(), non_neg_integer(), label()) -> - supervisor:child_spec(). --spec ensure_ssl() -> rabbit_types:infos(). --spec poodle_check(atom()) -> 'ok' | 'danger'. - -spec boot() -> 'ok'. --spec tcp_listener_started - (_, _, - string() | - {byte(),byte(),byte(),byte()} | - {char(),char(),char(),char(),char(),char(),char(),char()}, _) -> - 'ok'. --spec tcp_listener_stopped - (_, _, - string() | - {byte(),byte(),byte(),byte()} | - {char(),char(),char(),char(),char(),char(),char(),char()}, - _) -> - 'ok'. - -%%---------------------------------------------------------------------------- boot() -> ok = record_distribution_listener(), @@ -156,12 +115,16 @@ boot_tls(NumAcceptors) -> ok end. +-spec ensure_ssl() -> rabbit_types:infos(). + ensure_ssl() -> {ok, SslAppsConfig} = application:get_env(rabbit, ssl_apps), ok = app_utils:start_applications(SslAppsConfig), {ok, SslOptsConfig0} = application:get_env(rabbit, ssl_options), rabbit_ssl_options:fix(SslOptsConfig0). +-spec poodle_check(atom()) -> 'ok' | 'danger'. + poodle_check(Context) -> {ok, Vsn} = application:get_key(ssl, vsn), case rabbit_misc:version_compare(Vsn, "5.3", gte) of %% R16B01 @@ -190,6 +153,8 @@ log_poodle_fail(Context) -> fix_ssl_options(Config) -> rabbit_ssl_options:fix(Config). +-spec tcp_listener_addresses(listener_config()) -> [address()]. + tcp_listener_addresses(Port) when is_integer(Port) -> tcp_listener_addresses_auto(Port); tcp_listener_addresses({"auto", Port}) -> @@ -210,6 +175,11 @@ tcp_listener_addresses_auto(Port) -> lists:append([tcp_listener_addresses(Listener) || Listener <- port_to_listeners(Port)]). +-spec tcp_listener_spec + (name_prefix(), address(), [gen_tcp:listen_option()], module(), module(), + protocol(), any(), non_neg_integer(), label()) -> + supervisor:child_spec(). + tcp_listener_spec(NamePrefix, {IPAddress, Port, Family}, SocketOpts, Transport, ProtoSup, ProtoOpts, Protocol, NumAcceptors, Label) -> {rabbit_misc:tcp_name(NamePrefix, IPAddress, Port), @@ -220,9 +190,15 @@ tcp_listener_spec(NamePrefix, {IPAddress, Port, Family}, SocketOpts, NumAcceptors, Label]}, transient, infinity, supervisor, [tcp_listener_sup]}. +-spec start_tcp_listener( + listener_config(), integer()) -> 'ok' | {'error', term()}. + start_tcp_listener(Listener, NumAcceptors) -> start_listener(Listener, NumAcceptors, amqp, "TCP listener", tcp_opts()). +-spec start_ssl_listener( + listener_config(), rabbit_types:infos(), integer()) -> 'ok' | {'error', term()}. + start_ssl_listener(Listener, SslOpts, NumAcceptors) -> start_listener(Listener, NumAcceptors, 'amqp/ssl', "TLS (SSL) listener", tcp_opts() ++ SslOpts). @@ -259,6 +235,7 @@ transport(Protocol) -> 'amqp/ssl' -> ranch_ssl end. +-spec stop_tcp_listener(listener_config()) -> 'ok'. stop_tcp_listener(Listener) -> [stop_tcp_listener0(Address) || @@ -270,6 +247,13 @@ stop_tcp_listener0({IPAddress, Port, _Family}) -> ok = supervisor:terminate_child(rabbit_sup, Name), ok = supervisor:delete_child(rabbit_sup, Name). +-spec tcp_listener_started + (_, _, + string() | + {byte(),byte(),byte(),byte()} | + {char(),char(),char(),char(),char(),char(),char(),char()}, _) -> + 'ok'. + tcp_listener_started(Protocol, Opts, IPAddress, Port) -> %% We need the ip to distinguish e.g. 0.0.0.0 and 127.0.0.1 %% We need the host so we can distinguish multiple instances of the above @@ -283,6 +267,14 @@ tcp_listener_started(Protocol, Opts, IPAddress, Port) -> port = Port, opts = Opts}). +-spec tcp_listener_stopped + (_, _, + string() | + {byte(),byte(),byte(),byte()} | + {char(),char(),char(),char(),char(),char(),char(),char()}, + _) -> + 'ok'. + tcp_listener_stopped(Protocol, Opts, IPAddress, Port) -> ok = mnesia:dirty_delete_object( rabbit_listener, @@ -298,12 +290,18 @@ record_distribution_listener() -> {port, Port, _Version} = erl_epmd:port_please(Name, Host), tcp_listener_started(clustering, [], {0,0,0,0,0,0,0,0}, Port). +-spec active_listeners() -> [rabbit_types:listener()]. + active_listeners() -> rabbit_misc:dirty_read_all(rabbit_listener). +-spec node_listeners(node()) -> [rabbit_types:listener()]. + node_listeners(Node) -> mnesia:dirty_read(rabbit_listener, Node). +-spec on_node_down(node()) -> 'ok'. + on_node_down(Node) -> case lists:member(Node, nodes()) of false -> @@ -315,22 +313,44 @@ on_node_down(Node) -> "Keeping ~s listeners: the node is already back~n", [Node]) end. +-spec register_connection(pid()) -> ok. + register_connection(Pid) -> pg_local:join(rabbit_connections, Pid). +-spec unregister_connection(pid()) -> ok. + unregister_connection(Pid) -> pg_local:leave(rabbit_connections, Pid). +-spec connections() -> [rabbit_types:connection()]. + connections() -> rabbit_misc:append_rpc_all_nodes(rabbit_mnesia:cluster_nodes(running), rabbit_networking, connections_local, []). +-spec connections_local() -> [rabbit_types:connection()]. + connections_local() -> pg_local:get_members(rabbit_connections). +-spec connection_info_keys() -> rabbit_types:info_keys(). + connection_info_keys() -> rabbit_reader:info_keys(). +-spec connection_info(rabbit_types:connection()) -> rabbit_types:infos(). + connection_info(Pid) -> rabbit_reader:info(Pid). + +-spec connection_info(rabbit_types:connection(), rabbit_types:info_keys()) -> + rabbit_types:infos(). + connection_info(Pid, Items) -> rabbit_reader:info(Pid, Items). +-spec connection_info_all() -> [rabbit_types:infos()]. + connection_info_all() -> cmap(fun (Q) -> connection_info(Q) end). + +-spec connection_info_all(rabbit_types:info_keys()) -> + [rabbit_types:infos()]. + connection_info_all(Items) -> cmap(fun (Q) -> connection_info(Q, Items) end). emit_connection_info_all(Nodes, Items, Ref, AggregatorPid) -> @@ -343,6 +363,8 @@ emit_connection_info_local(Items, Ref, AggregatorPid) -> AggregatorPid, Ref, fun(Q) -> connection_info(Q, Items) end, connections_local()). +-spec close_connection(pid(), string()) -> 'ok'. + close_connection(Pid, Explanation) -> case lists:member(Pid, connections()) of true -> @@ -356,6 +378,12 @@ close_connection(Pid, Explanation) -> ok end. +-spec force_connection_event_refresh(reference()) -> 'ok'. + +force_connection_event_refresh(Ref) -> + [rabbit_reader:force_event_refresh(C, Ref) || C <- connections()], + ok. + handshake(Ref, ProxyProtocol) -> case ProxyProtocol of true -> diff --git a/src/rabbit_node_monitor.erl b/src/rabbit_node_monitor.erl index 4a5dea6073..cd46ade0e2 100644 --- a/src/rabbit_node_monitor.erl +++ b/src/rabbit_node_monitor.erl @@ -50,37 +50,11 @@ keepalive_timer, autoheal, guid, node_guids}). %%---------------------------------------------------------------------------- - --spec start_link() -> rabbit_types:ok_pid_or_error(). - --spec running_nodes_filename() -> string(). --spec cluster_status_filename() -> string(). --spec prepare_cluster_status_files() -> 'ok'. --spec write_cluster_status(rabbit_mnesia:cluster_status()) -> 'ok'. --spec read_cluster_status() -> rabbit_mnesia:cluster_status(). --spec update_cluster_status() -> 'ok'. --spec reset_cluster_status() -> 'ok'. - --spec notify_node_up() -> 'ok'. --spec notify_joined_cluster() -> 'ok'. --spec notify_left_cluster(node()) -> 'ok'. - --spec partitions() -> [node()]. --spec partitions([node()]) -> [{node(), [node()]}]. --spec status([node()]) -> {[{node(), [node()]}], [node()]}. --spec subscribe(pid()) -> 'ok'. --spec pause_partition_guard() -> 'ok' | 'pausing'. - --spec all_rabbit_nodes_up() -> boolean(). --spec run_outside_applications(fun (() -> any()), boolean()) -> pid(). --spec ping_all() -> 'ok'. --spec alive_nodes([node()]) -> [node()]. --spec alive_rabbit_nodes([node()]) -> [node()]. - -%%---------------------------------------------------------------------------- %% Start %%---------------------------------------------------------------------------- +-spec start_link() -> rabbit_types:ok_pid_or_error(). + start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). %%---------------------------------------------------------------------------- @@ -97,21 +71,26 @@ start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). %% the information we have will be outdated, but it cannot be %% otherwise. +-spec running_nodes_filename() -> string(). + running_nodes_filename() -> filename:join(rabbit_mnesia:dir(), "nodes_running_at_shutdown"). +-spec cluster_status_filename() -> string(). + cluster_status_filename() -> filename:join(rabbit_mnesia:dir(), "cluster_nodes.config"). quorum_filename() -> filename:join(rabbit_mnesia:dir(), "quorum"). +-spec prepare_cluster_status_files() -> 'ok' | no_return(). + prepare_cluster_status_files() -> rabbit_mnesia:ensure_mnesia_dir(), - Corrupt = fun(F) -> throw({error, corrupt_cluster_status_files, F}) end, RunningNodes1 = case try_read_file(running_nodes_filename()) of {ok, [Nodes]} when is_list(Nodes) -> Nodes; - {ok, Other} -> Corrupt(Other); + {ok, Other} -> corrupt_cluster_status_files(Other); {error, enoent} -> [] end, ThisNode = [node()], @@ -125,7 +104,7 @@ prepare_cluster_status_files() -> {ok, [AllNodes0]} when is_list(AllNodes0) -> {legacy_cluster_nodes(AllNodes0), legacy_disc_nodes(AllNodes0)}; {ok, Files} -> - Corrupt(Files); + corrupt_cluster_status_files(Files); {error, enoent} -> LegacyNodes = legacy_cluster_nodes([]), {LegacyNodes, LegacyNodes} @@ -133,6 +112,13 @@ prepare_cluster_status_files() -> AllNodes2 = lists:usort(AllNodes1 ++ RunningNodes2), ok = write_cluster_status({AllNodes2, DiscNodes, RunningNodes2}). +-spec corrupt_cluster_status_files(any()) -> no_return(). + +corrupt_cluster_status_files(F) -> + throw({error, corrupt_cluster_status_files, F}). + +-spec write_cluster_status(rabbit_mnesia:cluster_status()) -> 'ok'. + write_cluster_status({All, Disc, Running}) -> ClusterStatusFN = cluster_status_filename(), Res = case rabbit_file:write_term_file(ClusterStatusFN, [{All, Disc}]) of @@ -148,6 +134,8 @@ write_cluster_status({All, Disc, Running}) -> {FN, {error, E2}} -> throw({error, {could_not_write_file, FN, E2}}) end. +-spec read_cluster_status() -> rabbit_mnesia:cluster_status(). + read_cluster_status() -> case {try_read_file(cluster_status_filename()), try_read_file(running_nodes_filename())} of @@ -157,10 +145,14 @@ read_cluster_status() -> throw({error, {corrupt_or_missing_cluster_files, Stat, Run}}) end. +-spec update_cluster_status() -> 'ok'. + update_cluster_status() -> {ok, Status} = rabbit_mnesia:cluster_status_from_mnesia(), write_cluster_status(Status). +-spec reset_cluster_status() -> 'ok'. + reset_cluster_status() -> write_cluster_status({[node()], [node()], [node()]}). @@ -168,15 +160,21 @@ reset_cluster_status() -> %% Cluster notifications %%---------------------------------------------------------------------------- +-spec notify_node_up() -> 'ok'. + notify_node_up() -> gen_server:cast(?SERVER, notify_node_up). +-spec notify_joined_cluster() -> 'ok'. + notify_joined_cluster() -> Nodes = rabbit_mnesia:cluster_nodes(running) -- [node()], gen_server:abcast(Nodes, ?SERVER, {joined_cluster, node(), rabbit_mnesia:node_type()}), ok. +-spec notify_left_cluster(node()) -> 'ok'. + notify_left_cluster(Node) -> Nodes = rabbit_mnesia:cluster_nodes(running), gen_server:abcast(Nodes, ?SERVER, {left_cluster, Node}), @@ -186,16 +184,24 @@ notify_left_cluster(Node) -> %% Server calls %%---------------------------------------------------------------------------- +-spec partitions() -> [node()]. + partitions() -> gen_server:call(?SERVER, partitions, infinity). +-spec partitions([node()]) -> [{node(), [node()]}]. + partitions(Nodes) -> {Replies, _} = gen_server:multi_call(Nodes, ?SERVER, partitions, ?NODE_REPLY_TIMEOUT), Replies. +-spec status([node()]) -> {[{node(), [node()]}], [node()]}. + status(Nodes) -> gen_server:multi_call(Nodes, ?SERVER, status, infinity). +-spec subscribe(pid()) -> 'ok'. + subscribe(Pid) -> gen_server:cast(?SERVER, {subscribe, Pid}). @@ -218,6 +224,8 @@ subscribe(Pid) -> %% So we have channels call in here before issuing confirms, to do a %% lightweight check that we have not entered a pausing state. +-spec pause_partition_guard() -> 'ok' | 'pausing'. + pause_partition_guard() -> case get(pause_partition_guard) of not_pause_mode -> @@ -888,19 +896,28 @@ all_nodes_up() -> Nodes = rabbit_mnesia:cluster_nodes(all), length(alive_nodes(Nodes)) =:= length(Nodes). +-spec all_rabbit_nodes_up() -> boolean(). + all_rabbit_nodes_up() -> Nodes = rabbit_mnesia:cluster_nodes(all), length(alive_rabbit_nodes(Nodes)) =:= length(Nodes). +-spec alive_nodes([node()]) -> [node()]. + alive_nodes() -> alive_nodes(rabbit_mnesia:cluster_nodes(all)). alive_nodes(Nodes) -> [N || N <- Nodes, lists:member(N, [node()|nodes()])]. +-spec alive_rabbit_nodes([node()]) -> [node()]. + alive_rabbit_nodes() -> alive_rabbit_nodes(rabbit_mnesia:cluster_nodes(all)). alive_rabbit_nodes(Nodes) -> [N || N <- alive_nodes(Nodes), rabbit:is_running(N)]. %% This one is allowed to connect! + +-spec ping_all() -> 'ok'. + ping_all() -> [net_adm:ping(N) || N <- rabbit_mnesia:cluster_nodes(all)], ok. diff --git a/src/rabbit_nodes.erl b/src/rabbit_nodes.erl index 3a27ce8dc9..1e4abad8c9 100644 --- a/src/rabbit_nodes.erl +++ b/src/rabbit_nodes.erl @@ -31,28 +31,20 @@ %% Specs %%---------------------------------------------------------------------------- --spec names(string()) -> - rabbit_types:ok_or_error2([{string(), integer()}], term()). --spec diagnostics([node()]) -> string(). --spec cookie_hash() -> string(). --spec is_running(node(), atom()) -> boolean(). --spec is_process_running(node(), atom()) -> boolean(). --spec cluster_name() -> binary(). --spec set_cluster_name(binary(), rabbit_types:username()) -> 'ok'. --spec all_running() -> [node()]. --spec running_count() -> integer(). - -%%---------------------------------------------------------------------------- - name_type() -> case os:getenv("RABBITMQ_USE_LONGNAME") of "true" -> longnames; _ -> shortnames end. +-spec names(string()) -> + rabbit_types:ok_or_error2([{string(), integer()}], term()). + names(Hostname) -> rabbit_nodes_common:names(Hostname). +-spec diagnostics([node()]) -> string(). + diagnostics(Nodes) -> rabbit_nodes_common:diagnostics(Nodes). @@ -62,15 +54,23 @@ make(NodeStr) -> parts(NodeStr) -> rabbit_nodes_common:parts(NodeStr). +-spec cookie_hash() -> string(). + cookie_hash() -> rabbit_nodes_common:cookie_hash(). +-spec is_running(node(), atom()) -> boolean(). + is_running(Node, Application) -> rabbit_nodes_common:is_running(Node, Application). +-spec is_process_running(node(), atom()) -> boolean(). + is_process_running(Node, Process) -> rabbit_nodes_common:is_process_running(Node, Process). +-spec cluster_name() -> binary(). + cluster_name() -> rabbit_runtime_parameters:value_global( cluster_name, cluster_name_default()). @@ -80,6 +80,8 @@ cluster_name_default() -> FQDN = rabbit_net:hostname(), list_to_binary(atom_to_list(make({ID, FQDN}))). +-spec set_cluster_name(binary(), rabbit_types:username()) -> 'ok'. + set_cluster_name(Name, Username) -> %% Cluster name should be binary BinaryName = rabbit_data_coercion:to_binary(Name), @@ -88,8 +90,12 @@ set_cluster_name(Name, Username) -> ensure_epmd() -> rabbit_nodes_common:ensure_epmd(). +-spec all_running() -> [node()]. + all_running() -> rabbit_mnesia:cluster_nodes(running). +-spec running_count() -> integer(). + running_count() -> length(all_running()). -spec await_running_count(integer(), integer()) -> 'ok' | {'error', atom()}. diff --git a/src/rabbit_peer_discovery.erl b/src/rabbit_peer_discovery.erl index 2f4acee49e..2ed36d32ec 100644 --- a/src/rabbit_peer_discovery.erl +++ b/src/rabbit_peer_discovery.erl @@ -105,9 +105,15 @@ maybe_init() -> end. --spec discover_cluster_nodes() -> {ok, Nodes :: list()} | - {ok, {Nodes :: list(), NodeType :: rabbit_types:node_type()}} | - {error, Reason :: string()}. +%% This module doesn't currently sanity-check the return value of +%% `Backend:list_nodes()`. Therefore, it could return something invalid: +%% thus the `{œk, any()} in the spec. +%% +%% `rabbit_mnesia:init_from_config()` does some verifications. + +-spec discover_cluster_nodes() -> + {ok, {Nodes :: [node()], NodeType :: rabbit_types:node_type()} | any()} | + {error, Reason :: string()}. discover_cluster_nodes() -> Backend = backend(), @@ -156,10 +162,7 @@ maybe_inject_randomized_delay() -> -spec inject_randomized_delay() -> ok. inject_randomized_delay() -> - {Min, Max} = case randomized_delay_range_in_ms() of - {A, B} -> {A, B}; - [A, B] -> {A, B} - end, + {Min, Max} = randomized_delay_range_in_ms(), case {Min, Max} of %% When the max value is set to 0, consider the delay to be disabled. %% In addition, `rand:uniform/1` will fail with a "no function clause" @@ -258,12 +261,15 @@ unlock(Data) -> %% Implementation %% --spec normalize(Nodes :: list() | - {Nodes :: list(), NodeType :: rabbit_types:node_type()} | - {ok, Nodes :: list()} | - {ok, {Nodes :: list(), NodeType :: rabbit_types:node_type()}} | - {error, Reason :: string()}) -> {ok, {Nodes :: list(), NodeType :: rabbit_types:node_type()}} | - {error, Reason :: string()}. +-spec normalize(Nodes :: [node()] | + {Nodes :: [node()], + NodeType :: rabbit_types:node_type()} | + {ok, Nodes :: [node()]} | + {ok, {Nodes :: [node()], + NodeType :: rabbit_types:node_type()}} | + {error, Reason :: string()}) -> + {ok, {Nodes :: [node()], NodeType :: rabbit_types:node_type()}} | + {error, Reason :: string()}. normalize(Nodes) when is_list(Nodes) -> {ok, {Nodes, disc}}; @@ -296,14 +302,12 @@ node_prefix() -> --spec append_node_prefix(Value :: binary() | list()) -> atom(). +-spec append_node_prefix(Value :: binary() | string()) -> string(). -append_node_prefix(Value) -> +append_node_prefix(Value) when is_binary(Value) orelse is_list(Value) -> Val = rabbit_data_coercion:to_list(Value), Hostname = case string:tokens(Val, ?NODENAME_PART_SEPARATOR) of - [_ExistingPrefix, Val] -> - Val; - [Val] -> - Val + [_ExistingPrefix, HN] -> HN; + [HN] -> HN end, string:join([node_prefix(), Hostname], ?NODENAME_PART_SEPARATOR). diff --git a/src/rabbit_peer_discovery_classic_config.erl b/src/rabbit_peer_discovery_classic_config.erl index 6597d77da4..16b7861f5f 100644 --- a/src/rabbit_peer_discovery_classic_config.erl +++ b/src/rabbit_peer_discovery_classic_config.erl @@ -26,13 +26,12 @@ %% API %% --spec list_nodes() -> {ok, Nodes :: list()} | {error, Reason :: string()}. +-spec list_nodes() -> {ok, {Nodes :: [node()], rabbit_types:node_type()}}. list_nodes() -> - case application:get_env(rabbit, cluster_nodes) of - {_Nodes, _NodeType} = Pair -> Pair; - Nodes when is_list(Nodes) -> {Nodes, disc}; - undefined -> {[], disc} + case application:get_env(rabbit, cluster_nodes, {[], disc}) of + {_Nodes, _NodeType} = Pair -> {ok, Pair}; + Nodes when is_list(Nodes) -> {ok, {Nodes, disc}} end. -spec supports_registration() -> boolean(). diff --git a/src/rabbit_peer_discovery_dns.erl b/src/rabbit_peer_discovery_dns.erl index ad277e08b0..031d1290b3 100644 --- a/src/rabbit_peer_discovery_dns.erl +++ b/src/rabbit_peer_discovery_dns.erl @@ -28,12 +28,13 @@ %% API %% --spec list_nodes() -> {ok, Nodes :: list()} | {error, Reason :: string()}. +-spec list_nodes() -> + {ok, {Nodes :: [node()], rabbit_types:node_type()}}. list_nodes() -> case application:get_env(rabbit, cluster_formation) of undefined -> - {[], disc}; + {ok, {[], disc}}; {ok, ClusterFormation} -> case proplists:get_value(peer_discovery_dns, ClusterFormation) of undefined -> @@ -41,11 +42,11 @@ list_nodes() -> "but final config does not contain rabbit.cluster_formation.peer_discovery_dns. " "Cannot discover any nodes because seed hostname is not configured!", [?MODULE]), - {[], disc}; + {ok, {[], disc}}; Proplist -> Hostname = rabbit_data_coercion:to_list(proplists:get_value(hostname, Proplist)), - {discover_nodes(Hostname, net_kernel:longnames()), rabbit_peer_discovery:node_type()} + {ok, {discover_nodes(Hostname, net_kernel:longnames()), rabbit_peer_discovery:node_type()}} end end. diff --git a/src/rabbit_plugins.erl b/src/rabbit_plugins.erl index 80f70a1b7f..6adce8a24e 100644 --- a/src/rabbit_plugins.erl +++ b/src/rabbit_plugins.erl @@ -31,20 +31,10 @@ -type plugin_name() :: atom(). --spec setup() -> [plugin_name()]. --spec active() -> [plugin_name()]. --spec list(string()) -> [#plugin{}]. --spec list(string(), boolean()) -> [#plugin{}]. --spec read_enabled(file:filename()) -> [plugin_name()]. --spec dependencies(boolean(), [plugin_name()], [#plugin{}]) -> - [plugin_name()]. --spec ensure(string()) -> {'ok', [atom()], [atom()]} | {error, any()}. --spec strictly_plugins([plugin_name()], [#plugin{}]) -> [plugin_name()]. --spec strictly_plugins([plugin_name()]) -> [plugin_name()]. --spec is_strictly_plugin(#plugin{}) -> boolean(). - %%---------------------------------------------------------------------------- +-spec ensure(string()) -> {'ok', [atom()], [atom()]} | {error, any()}. + ensure(FileJustChanged) -> case rabbit:is_running() of true -> ensure1(FileJustChanged); @@ -128,6 +118,9 @@ enabled_plugins() -> end. %% @doc Prepares the file system and installs all enabled plugins. + +-spec setup() -> [plugin_name()]. + setup() -> ExpandDir = plugins_expand_dir(), %% Eliminate the contents of the destination directory @@ -184,15 +177,23 @@ extract_schema(#plugin{type = dir, location = Location}, SchemaDir) -> %% @doc Lists the plugins which are currently running. + +-spec active() -> [plugin_name()]. + active() -> InstalledPlugins = plugin_names(list(plugins_dir())), [App || {App, _, _} <- rabbit_misc:which_applications(), lists:member(App, InstalledPlugins)]. %% @doc Get the list of plugins which are ready to be enabled. + +-spec list(string()) -> [#plugin{}]. + list(PluginsPath) -> list(PluginsPath, false). +-spec list(string(), boolean()) -> [#plugin{}]. + list(PluginsPath, IncludeRequiredDeps) -> {AllPlugins, LoadingProblems} = discover_plugins(split_path(PluginsPath)), {UniquePlugins, DuplicateProblems} = remove_duplicate_plugins(AllPlugins), @@ -202,6 +203,9 @@ list(PluginsPath, IncludeRequiredDeps) -> ensure_dependencies(Plugins2). %% @doc Read the list of enabled plugins from the supplied term file. + +-spec read_enabled(file:filename()) -> [plugin_name()]. + read_enabled(PluginsFile) -> case rabbit_file:read_term_file(PluginsFile) of {ok, [Plugins]} -> Plugins; @@ -216,6 +220,10 @@ read_enabled(PluginsFile) -> %% @doc Calculate the dependency graph from <i>Sources</i>. %% When Reverse =:= true the bottom/leaf level applications are returned in %% the resulting list, otherwise they're skipped. + +-spec dependencies(boolean(), [plugin_name()], [#plugin{}]) -> + [plugin_name()]. + dependencies(Reverse, Sources, AllPlugins) -> {ok, G} = rabbit_misc:build_acyclic_graph( fun ({App, _Deps}) -> [{App, App}] end, @@ -231,15 +239,22 @@ dependencies(Reverse, Sources, AllPlugins) -> OrderedDests. %% Filter real plugins from application dependencies + +-spec is_strictly_plugin(#plugin{}) -> boolean(). + is_strictly_plugin(#plugin{extra_dependencies = ExtraDeps}) -> lists:member(rabbit, ExtraDeps). +-spec strictly_plugins([plugin_name()], [#plugin{}]) -> [plugin_name()]. + strictly_plugins(Plugins, AllPlugins) -> lists:filter( fun(Name) -> is_strictly_plugin(lists:keyfind(Name, #plugin.name, AllPlugins)) end, Plugins). +-spec strictly_plugins([plugin_name()]) -> [plugin_name()]. + strictly_plugins(Plugins) -> AllPlugins = list(plugins_dir()), lists:filter( @@ -433,7 +448,8 @@ is_version_supported(VersionFull, ExpectedVersions) -> %% therefore preview part should be removed Version = remove_version_preview_part(VersionFull), case lists:any(fun(ExpectedVersion) -> - rabbit_misc:version_minor_equivalent(ExpectedVersion, Version) + rabbit_misc:strict_version_minor_equivalent(ExpectedVersion, + Version) andalso rabbit_misc:version_compare(ExpectedVersion, Version, lte) end, @@ -453,6 +469,7 @@ clean_plugins(Plugins) -> clean_plugin(Plugin, ExpandDir) -> {ok, Mods} = application:get_key(Plugin, modules), application:unload(Plugin), + rabbit_feature_flags:initialize_registry(), [begin code:soft_purge(Mod), code:delete(Mod), diff --git a/src/rabbit_policy.erl b/src/rabbit_policy.erl index 7f07b205ff..e38e23a2f1 100644 --- a/src/rabbit_policy.erl +++ b/src/rabbit_policy.erl @@ -36,7 +36,8 @@ -behaviour(rabbit_runtime_parameter). --include("rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include("amqqueue.hrl"). -import(rabbit_misc, [pget/2, pget/3]). @@ -59,16 +60,22 @@ register() -> rabbit_registry:register(runtime_parameter, <<"policy">>, ?MODULE), rabbit_registry:register(runtime_parameter, <<"operator_policy">>, ?MODULE). -name(#amqqueue{policy = Policy}) -> name0(Policy); +name(Q) when ?is_amqqueue(Q) -> + Policy = amqqueue:get_policy(Q), + name0(Policy); name(#exchange{policy = Policy}) -> name0(Policy). -name_op(#amqqueue{operator_policy = Policy}) -> name0(Policy); +name_op(Q) when ?is_amqqueue(Q) -> + OpPolicy = amqqueue:get_operator_policy(Q), + name0(OpPolicy); name_op(#exchange{operator_policy = Policy}) -> name0(Policy). name0(undefined) -> none; name0(Policy) -> pget(name, Policy). -effective_definition(#amqqueue{policy = Policy, operator_policy = OpPolicy}) -> +effective_definition(Q) when ?is_amqqueue(Q) -> + Policy = amqqueue:get_policy(Q), + OpPolicy = amqqueue:get_operator_policy(Q), effective_definition0(Policy, OpPolicy); effective_definition(#exchange{policy = Policy, operator_policy = OpPolicy}) -> effective_definition0(Policy, OpPolicy). @@ -90,10 +97,15 @@ effective_definition0(Policy, OpPolicy) -> end, lists:umerge(Keys, OpKeys)). -set(Q = #amqqueue{name = Name}) -> Q#amqqueue{policy = match(Name), - operator_policy = match_op(Name)}; -set(X = #exchange{name = Name}) -> X#exchange{policy = match(Name), - operator_policy = match_op(Name)}. +set(Q0) when ?is_amqqueue(Q0) -> + Name = amqqueue:get_name(Q0), + Policy = match(Name), + OpPolicy = match_op(Name), + Q1 = amqqueue:set_policy(Q0, Policy), + Q2 = amqqueue:set_operator_policy(Q1, OpPolicy), + Q2; +set(X = #exchange{name = Name}) -> + X#exchange{policy = match(Name), operator_policy = match_op(Name)}. match(Name = #resource{virtual_host = VHost}) -> match(Name, list(VHost)). @@ -101,7 +113,9 @@ match(Name = #resource{virtual_host = VHost}) -> match_op(Name = #resource{virtual_host = VHost}) -> match(Name, list_op(VHost)). -get(Name, #amqqueue{policy = Policy, operator_policy = OpPolicy}) -> +get(Name, Q) when ?is_amqqueue(Q) -> + Policy = amqqueue:get_policy(Q), + OpPolicy = amqqueue:get_operator_policy(Q), get0(Name, Policy, OpPolicy); get(Name, #exchange{policy = Policy, operator_policy = OpPolicy}) -> get0(Name, Policy, OpPolicy); @@ -170,7 +184,11 @@ recover() -> %% variants. recover0() -> Xs = mnesia:dirty_match_object(rabbit_durable_exchange, #exchange{_ = '_'}), - Qs = mnesia:dirty_match_object(rabbit_durable_queue, #amqqueue{_ = '_'}), + Qs = rabbit_amqqueue:list_with_possible_retry( + fun() -> + mnesia:dirty_match_object( + rabbit_durable_queue, amqqueue:pattern_match_all()) + end), Policies = list(), OpPolicies = list_op(), [rabbit_misc:execute_mnesia_transaction( @@ -182,15 +200,26 @@ recover0() -> operator_policy = match(Name, OpPolicies)}), write) end) || X = #exchange{name = Name} <- Xs], - [rabbit_misc:execute_mnesia_transaction( - fun () -> - mnesia:write( - rabbit_durable_queue, - rabbit_queue_decorator:set( - Q#amqqueue{policy = match(Name, Policies), - operator_policy = match(Name, OpPolicies)}), - write) - end) || Q = #amqqueue{name = Name} <- Qs], + [begin + QName = amqqueue:get_name(Q0), + Policy1 = match(QName, Policies), + Q1 = amqqueue:set_policy(Q0, Policy1), + OpPolicy1 = match(QName, OpPolicies), + Q2 = amqqueue:set_operator_policy(Q1, OpPolicy1), + Q3 = rabbit_queue_decorator:set(Q2), + ?try_mnesia_tx_or_upgrade_amqqueue_and_retry( + rabbit_misc:execute_mnesia_transaction( + fun () -> + mnesia:write(rabbit_durable_queue, Q3, write) + end), + begin + Q4 = amqqueue:upgrade(Q3), + rabbit_misc:execute_mnesia_transaction( + fun () -> + mnesia:write(rabbit_durable_queue, Q4, write) + end) + end) + end || Q0 <- Qs], ok. invalid_file() -> @@ -395,25 +424,26 @@ update_exchange(X = #exchange{name = XName, end end. -update_queue(Q = #amqqueue{name = QName, - policy = OldPolicy, - operator_policy = OldOpPolicy}, - Policies, OpPolicies) -> +update_queue(Q0, Policies, OpPolicies) when ?is_amqqueue(Q0) -> + QName = amqqueue:get_name(Q0), + OldPolicy = amqqueue:get_policy(Q0), + OldOpPolicy = amqqueue:get_operator_policy(Q0), case {match(QName, Policies), match(QName, OpPolicies)} of {OldPolicy, OldOpPolicy} -> no_change; {NewPolicy, NewOpPolicy} -> - NewQueue = rabbit_amqqueue:update( - QName, - fun(Q1) -> - rabbit_queue_decorator:set( - Q1#amqqueue{policy = NewPolicy, - operator_policy = NewOpPolicy, - policy_version = - Q1#amqqueue.policy_version + 1 }) - end), + F = fun (QFun0) -> + QFun1 = amqqueue:set_policy(QFun0, NewPolicy), + QFun2 = amqqueue:set_operator_policy(QFun1, NewOpPolicy), + NewPolicyVersion = amqqueue:get_policy_version(QFun2) + 1, + QFun3 = amqqueue:set_policy_version(QFun2, NewPolicyVersion), + rabbit_queue_decorator:set(QFun3) + end, + NewQueue = rabbit_amqqueue:update(QName, F), case NewQueue of - #amqqueue{} = Q1 -> {Q, Q1}; - not_found -> {Q, Q } + Q1 when ?is_amqqueue(Q1) -> + {Q0, Q1}; + not_found -> + {Q0, Q0} end end. @@ -421,7 +451,7 @@ notify(no_change)-> ok; notify({X1 = #exchange{}, X2 = #exchange{}}) -> rabbit_exchange:policy_changed(X1, X2); -notify({Q1 = #amqqueue{}, Q2 = #amqqueue{}}) -> +notify({Q1, Q2}) when ?is_amqqueue(Q1), ?is_amqqueue(Q2) -> rabbit_amqqueue:policy_changed(Q1, Q2). match(Name, Policies) -> diff --git a/src/rabbit_prelaunch.erl b/src/rabbit_prelaunch.erl index 8d13a16e0b..b0fe6d2b04 100644 --- a/src/rabbit_prelaunch.erl +++ b/src/rabbit_prelaunch.erl @@ -29,13 +29,8 @@ -define(EX_USAGE, 64). %%---------------------------------------------------------------------------- -%% Specs -%%---------------------------------------------------------------------------- -spec start() -> no_return(). --spec stop() -> 'ok'. - -%%---------------------------------------------------------------------------- start() -> case init:get_plain_arguments() of @@ -55,6 +50,8 @@ start() -> rabbit_misc:quit(?SET_DIST_PORT), ok. +-spec stop() -> 'ok'. + stop() -> ok. diff --git a/src/rabbit_prequeue.erl b/src/rabbit_prequeue.erl index 63dbec545b..5e92013424 100644 --- a/src/rabbit_prequeue.erl +++ b/src/rabbit_prequeue.erl @@ -29,7 +29,8 @@ -behaviour(gen_server2). --include("rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include("amqqueue.hrl"). %%---------------------------------------------------------------------------- @@ -37,11 +38,11 @@ -type start_mode() :: 'declare' | 'recovery' | 'slave'. --spec start_link(rabbit_types:amqqueue(), start_mode(), pid()) - -> rabbit_types:ok_pid_or_error(). - %%---------------------------------------------------------------------------- +-spec start_link(amqqueue:amqqueue(), start_mode(), pid()) + -> rabbit_types:ok_pid_or_error(). + start_link(Q, StartMode, Marker) -> gen_server2:start_link(?MODULE, {Q, StartMode, Marker}, []). @@ -57,20 +58,22 @@ init({Q, StartMode, Marker}) -> init(Q, master) -> rabbit_amqqueue_process:init(Q); init(Q, slave) -> rabbit_mirror_queue_slave:init(Q); -init(#amqqueue{name = QueueName}, restart) -> - {ok, Q = #amqqueue{pid = QPid, - slave_pids = SPids}} = rabbit_amqqueue:lookup(QueueName), +init(Q0, restart) when ?is_amqqueue(Q0) -> + QueueName = amqqueue:get_name(Q0), + {ok, Q1} = rabbit_amqqueue:lookup(QueueName), + QPid = amqqueue:get_pid(Q1), + SPids = amqqueue:get_slave_pids(Q1), LocalOrMasterDown = node(QPid) =:= node() orelse not rabbit_mnesia:on_running_node(QPid), Slaves = [SPid || SPid <- SPids, rabbit_mnesia:is_process_alive(SPid)], case rabbit_mnesia:is_process_alive(QPid) of true -> false = LocalOrMasterDown, %% assertion rabbit_mirror_queue_slave:go(self(), async), - rabbit_mirror_queue_slave:init(Q); %% [1] + rabbit_mirror_queue_slave:init(Q1); %% [1] false -> case LocalOrMasterDown andalso Slaves =:= [] of - true -> crash_restart(Q); %% [2] + true -> crash_restart(Q1); %% [2] false -> timer:sleep(25), - init(Q, restart) %% [3] + init(Q1, restart) %% [3] end end. %% [1] There is a master on another node. Regardless of whether we @@ -83,10 +86,12 @@ init(#amqqueue{name = QueueName}, restart) -> %% not a stable situation. Sleep and wait for somebody else to make a %% move. -crash_restart(Q = #amqqueue{name = QueueName}) -> +crash_restart(Q0) when ?is_amqqueue(Q0) -> + QueueName = amqqueue:get_name(Q0), rabbit_log:error("Restarting crashed ~s.~n", [rabbit_misc:rs(QueueName)]), gen_server2:cast(self(), init), - rabbit_amqqueue_process:init(Q#amqqueue{pid = self()}). + Q1 = amqqueue:set_pid(Q0, self()), + rabbit_amqqueue_process:init(Q1). %%---------------------------------------------------------------------------- diff --git a/src/rabbit_priority_queue.erl b/src/rabbit_priority_queue.erl index b41511f874..621f42dafb 100644 --- a/src/rabbit_priority_queue.erl +++ b/src/rabbit_priority_queue.erl @@ -16,8 +16,10 @@ -module(rabbit_priority_queue). --include_lib("rabbit.hrl"). --include_lib("rabbit_framing.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit_framing.hrl"). +-include("amqqueue.hrl"). + -behaviour(rabbit_backing_queue). %% enabled unconditionally. Disabling priority queueing after @@ -102,10 +104,14 @@ stop(VHost) -> %%---------------------------------------------------------------------------- -mutate_name(P, Q = #amqqueue{name = QName = #resource{name = QNameBin}}) -> - Q#amqqueue{name = QName#resource{name = mutate_name_bin(P, QNameBin)}}. +mutate_name(P, Q) when ?is_amqqueue(Q) -> + Res0 = #resource{name = QNameBin0} = amqqueue:get_name(Q), + QNameBin1 = mutate_name_bin(P, QNameBin0), + Res1 = Res0#resource{name = QNameBin1}, + amqqueue:set_name(Q, Res1). -mutate_name_bin(P, NameBin) -> <<NameBin/binary, 0, P:8>>. +mutate_name_bin(P, NameBin) -> + <<NameBin/binary, 0, P:8>>. expand_queues(QNames) -> lists:unzip( @@ -125,7 +131,8 @@ collapse_recovery(QNames, DupNames, Recovery) -> end, dict:new(), lists:zip(DupNames, Recovery)), [dict:fetch(Name, NameToTerms) || Name <- QNames]. -priorities(#amqqueue{arguments = Args}) -> +priorities(Q) when ?is_amqqueue(Q) -> + Args = amqqueue:get_arguments(Q), Ints = [long, short, signedint, byte, unsignedbyte, unsignedshort, unsignedint], case rabbit_misc:table_lookup(Args, <<"x-max-priority">>) of {Type, RequestedMax} -> diff --git a/src/rabbit_queue_collector.erl b/src/rabbit_queue_collector.erl index e52156f60d..732415b82e 100644 --- a/src/rabbit_queue_collector.erl +++ b/src/rabbit_queue_collector.erl @@ -33,13 +33,12 @@ %%---------------------------------------------------------------------------- -spec start_link(rabbit_types:proc_name()) -> rabbit_types:ok_pid_or_error(). --spec register(pid(), pid()) -> 'ok'. - -%%---------------------------------------------------------------------------- start_link(ProcName) -> gen_server:start_link(?MODULE, [ProcName], []). +-spec register(pid(), pid()) -> 'ok'. + register(CollectorPid, Q) -> gen_server:call(CollectorPid, {register, Q}, infinity). diff --git a/src/rabbit_queue_consumers.erl b/src/rabbit_queue_consumers.erl index 7a0c0f98e3..2ede7b7b8e 100644 --- a/src/rabbit_queue_consumers.erl +++ b/src/rabbit_queue_consumers.erl @@ -66,57 +66,29 @@ -type cr_fun() :: fun ((#cr{}) -> #cr{}). -type fetch_result() :: {rabbit_types:basic_message(), boolean(), ack()}. --spec new() -> state(). --spec max_active_priority(state()) -> integer() | 'infinity' | 'empty'. --spec inactive(state()) -> boolean(). --spec all(state()) -> [{ch(), rabbit_types:ctag(), boolean(), - non_neg_integer(), rabbit_framing:amqp_table(), - rabbit_types:username()}]. --spec count() -> non_neg_integer(). --spec unacknowledged_message_count() -> non_neg_integer(). --spec add(ch(), rabbit_types:ctag(), boolean(), pid(), boolean(), - non_neg_integer(), rabbit_framing:amqp_table(), boolean(), - rabbit_types:username(), state()) - -> state(). --spec remove(ch(), rabbit_types:ctag(), state()) -> - 'not_found' | state(). --spec erase_ch(ch(), state()) -> - 'not_found' | {[ack()], [rabbit_types:ctag()], - state()}. --spec send_drained() -> 'ok'. --spec deliver(fun ((boolean()) -> {fetch_result(), T}), - rabbit_amqqueue:name(), state(), boolean(), - none | {ch(), rabbit_types:ctag()} | {ch(), consumer()}) -> - {'delivered', boolean(), T, state()} | - {'undelivered', boolean(), state()}. --spec record_ack(ch(), pid(), ack()) -> 'ok'. --spec subtract_acks(ch(), [ack()], state()) -> - 'not_found' | 'unchanged' | {'unblocked', state()}. --spec possibly_unblock(cr_fun(), ch(), state()) -> - 'unchanged' | {'unblocked', state()}. --spec resume_fun() -> cr_fun(). --spec notify_sent_fun(non_neg_integer()) -> cr_fun(). --spec activate_limit_fun() -> cr_fun(). --spec credit(boolean(), integer(), boolean(), ch(), rabbit_types:ctag(), - state()) -> 'unchanged' | {'unblocked', state()}. --spec utilisation(state()) -> ratio(). --spec get(ch(), rabbit_types:ctag(), state()) -> undefined | consumer(). --spec consumer_tag(consumer()) -> rabbit_types:ctag(). --spec get_infos(consumer()) -> term(). - %%---------------------------------------------------------------------------- +-spec new() -> state(). + new() -> #state{consumers = priority_queue:new(), use = {active, erlang:monotonic_time(micro_seconds), 1.0}}. +-spec max_active_priority(state()) -> integer() | 'infinity' | 'empty'. + max_active_priority(#state{consumers = Consumers}) -> priority_queue:highest(Consumers). +-spec inactive(state()) -> boolean(). + inactive(#state{consumers = Consumers}) -> priority_queue:is_empty(Consumers). +-spec all(state()) -> [{ch(), rabbit_types:ctag(), boolean(), + non_neg_integer(), rabbit_framing:amqp_table(), + rabbit_types:username()}]. + all(State) -> all(State, none, false). @@ -146,11 +118,20 @@ consumers(Consumers, SingleActiveConsumer, SingleActiveConsumerOn, Acc) -> [{ChPid, CTag, Ack, Prefetch, Active, ActivityStatus, Args, Username} | Acc1] end, Acc, Consumers). +-spec count() -> non_neg_integer(). + count() -> lists:sum([Count || #cr{consumer_count = Count} <- all_ch_record()]). +-spec unacknowledged_message_count() -> non_neg_integer(). + unacknowledged_message_count() -> lists:sum([?QUEUE:len(C#cr.acktags) || C <- all_ch_record()]). +-spec add(ch(), rabbit_types:ctag(), boolean(), pid(), boolean(), + non_neg_integer(), rabbit_framing:amqp_table(), boolean(), + rabbit_types:username(), state()) + -> state(). + add(ChPid, CTag, NoAck, LimiterPid, LimiterActive, Prefetch, Args, IsEmpty, Username, State = #state{consumers = Consumers, use = CUInfo}) -> @@ -176,6 +157,9 @@ add(ChPid, CTag, NoAck, LimiterPid, LimiterActive, Prefetch, Args, IsEmpty, State#state{consumers = add_consumer({ChPid, Consumer}, Consumers), use = update_use(CUInfo, active)}. +-spec remove(ch(), rabbit_types:ctag(), state()) -> + 'not_found' | state(). + remove(ChPid, CTag, State = #state{consumers = Consumers}) -> case lookup_ch(ChPid) of not_found -> @@ -196,6 +180,10 @@ remove(ChPid, CTag, State = #state{consumers = Consumers}) -> remove_consumer(ChPid, CTag, Consumers)} end. +-spec erase_ch(ch(), state()) -> + 'not_found' | {[ack()], [rabbit_types:ctag()], + state()}. + erase_ch(ChPid, State = #state{consumers = Consumers}) -> case lookup_ch(ChPid) of not_found -> @@ -211,9 +199,17 @@ erase_ch(ChPid, State = #state{consumers = Consumers}) -> State#state{consumers = remove_consumers(ChPid, Consumers)}} end. +-spec send_drained() -> 'ok'. + send_drained() -> [update_ch_record(send_drained(C)) || C <- all_ch_record()], ok. +-spec deliver(fun ((boolean()) -> {fetch_result(), T}), + rabbit_amqqueue:name(), state(), boolean(), + none | {ch(), rabbit_types:ctag()} | {ch(), consumer()}) -> + {'delivered', boolean(), T, state()} | + {'undelivered', boolean(), state()}. + deliver(FetchFun, QName, State, SingleActiveConsumerIsOn, ActiveConsumer) -> deliver(FetchFun, QName, false, State, SingleActiveConsumerIsOn, ActiveConsumer). @@ -299,11 +295,16 @@ is_blocked(Consumer = {ChPid, _C}) -> #cr{blocked_consumers = BlockedConsumers} = lookup_ch(ChPid), priority_queue:member(Consumer, BlockedConsumers). +-spec record_ack(ch(), pid(), ack()) -> 'ok'. + record_ack(ChPid, LimiterPid, AckTag) -> C = #cr{acktags = ChAckTags} = ch_record(ChPid, LimiterPid), update_ch_record(C#cr{acktags = ?QUEUE:in({AckTag, none}, ChAckTags)}), ok. +-spec subtract_acks(ch(), [ack()], state()) -> + 'not_found' | 'unchanged' | {'unblocked', state()}. + subtract_acks(ChPid, AckTags, State) -> case lookup_ch(ChPid) of not_found -> @@ -341,6 +342,9 @@ subtract_acks([T | TL] = AckTags, Prefix, CTagCounts, AckQ) -> subtract_acks([], Prefix, CTagCounts, AckQ) end. +-spec possibly_unblock(cr_fun(), ch(), state()) -> + 'unchanged' | {'unblocked', state()}. + possibly_unblock(Update, ChPid, State) -> case lookup_ch(ChPid) of not_found -> unchanged; @@ -370,23 +374,31 @@ unblock(C = #cr{blocked_consumers = BlockedQ, limiter = Limiter}, use = update_use(Use, active)}} end. +-spec resume_fun() -> cr_fun(). + resume_fun() -> fun (C = #cr{limiter = Limiter}) -> C#cr{limiter = rabbit_limiter:resume(Limiter)} end. +-spec notify_sent_fun(non_neg_integer()) -> cr_fun(). + notify_sent_fun(Credit) -> fun (C = #cr{unsent_message_count = Count}) -> C#cr{unsent_message_count = Count - Credit} end. +-spec activate_limit_fun() -> cr_fun(). + activate_limit_fun() -> fun (C = #cr{limiter = Limiter}) -> C#cr{limiter = rabbit_limiter:activate(Limiter)} end. -credit(IsEmpty, Credit, Drain, ChPid, CTag, State) -> +-spec credit(boolean(), integer(), boolean(), ch(), rabbit_types:ctag(), + state()) -> 'unchanged' | {'unblocked', state()}. +credit(IsEmpty, Credit, Drain, ChPid, CTag, State) -> case lookup_ch(ChPid) of not_found -> unchanged; @@ -405,6 +417,8 @@ credit(IsEmpty, Credit, Drain, ChPid, CTag, State) -> drain_mode(true) -> drain; drain_mode(false) -> manual. +-spec utilisation(state()) -> ratio(). + utilisation(#state{use = {active, Since, Avg}}) -> use_avg(erlang:monotonic_time(micro_seconds) - Since, 0, Avg); utilisation(#state{use = {inactive, Since, Active, Avg}}) -> @@ -421,6 +435,8 @@ get_consumer(#state{consumers = Consumers}) -> {empty, _} -> undefined end. +-spec get(ch(), rabbit_types:ctag(), state()) -> undefined | consumer(). + get(ChPid, ConsumerTag, #state{consumers = Consumers}) -> Consumers1 = priority_queue:filter(fun ({CP, #consumer{tag = CT}}) -> (CP == ChPid) and (CT == ConsumerTag) @@ -430,10 +446,14 @@ get(ChPid, ConsumerTag, #state{consumers = Consumers}) -> {{value, Consumer, _Priority}, _Tail} -> Consumer end. +-spec get_infos(consumer()) -> term(). + get_infos(Consumer) -> {Consumer#consumer.tag,Consumer#consumer.ack_required, Consumer#consumer.prefetch, Consumer#consumer.args}. +-spec consumer_tag(consumer()) -> rabbit_types:ctag(). + consumer_tag(#consumer{tag = CTag}) -> CTag. diff --git a/src/rabbit_queue_decorator.erl b/src/rabbit_queue_decorator.erl index 0c234e1072..6f40637b93 100644 --- a/src/rabbit_queue_decorator.erl +++ b/src/rabbit_queue_decorator.erl @@ -16,7 +16,8 @@ -module(rabbit_queue_decorator). --include("rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include("amqqueue.hrl"). -export([select/1, set/1, register/2, unregister/1]). @@ -26,18 +27,18 @@ %%---------------------------------------------------------------------------- --callback startup(rabbit_types:amqqueue()) -> 'ok'. +-callback startup(amqqueue:amqqueue()) -> 'ok'. --callback shutdown(rabbit_types:amqqueue()) -> 'ok'. +-callback shutdown(amqqueue:amqqueue()) -> 'ok'. --callback policy_changed(rabbit_types:amqqueue(), rabbit_types:amqqueue()) -> +-callback policy_changed(amqqueue:amqqueue(), amqqueue:amqqueue()) -> 'ok'. --callback active_for(rabbit_types:amqqueue()) -> boolean(). +-callback active_for(amqqueue:amqqueue()) -> boolean(). %% called with Queue, MaxActivePriority, IsEmpty -callback consumer_state_changed( - rabbit_types:amqqueue(), integer(), boolean()) -> 'ok'. + amqqueue:amqqueue(), integer(), boolean()) -> 'ok'. %%---------------------------------------------------------------------------- @@ -47,7 +48,9 @@ removed_from_rabbit_registry(_Type) -> ok. select(Modules) -> [M || M <- Modules, code:which(M) =/= non_existing]. -set(Q) -> Q#amqqueue{decorators = [D || D <- list(), D:active_for(Q)]}. +set(Q) when ?is_amqqueue(Q) -> + Decorators = [D || D <- list(), D:active_for(Q)], + amqqueue:set_decorators(Q, Decorators). list() -> [M || {_, M} <- rabbit_registry:lookup_all(queue_decorator)]. @@ -61,13 +64,18 @@ unregister(TypeName) -> [maybe_recover(Q) || Q <- rabbit_amqqueue:list()], ok. -maybe_recover(Q = #amqqueue{name = Name, - decorators = Decs}) -> - #amqqueue{decorators = Decs1} = set(Q), - Old = lists:sort(select(Decs)), +maybe_recover(Q0) when ?is_amqqueue(Q0) -> + Name = amqqueue:get_name(Q0), + Decs0 = amqqueue:get_decorators(Q0), + Q1 = set(Q0), + Decs1 = amqqueue:get_decorators(Q1), + Old = lists:sort(select(Decs0)), New = lists:sort(select(Decs1)), case New of - Old -> ok; - _ -> [M:startup(Q) || M <- New -- Old], - rabbit_amqqueue:update_decorators(Name) + Old -> + ok; + _ -> + %% TODO LRB JSP 160169569 should startup be passed Q1 here? + [M:startup(Q0) || M <- New -- Old], + rabbit_amqqueue:update_decorators(Name) end. diff --git a/src/rabbit_queue_index.erl b/src/rabbit_queue_index.erl index 9785ae170d..e4047a9902 100644 --- a/src/rabbit_queue_index.erl +++ b/src/rabbit_queue_index.erl @@ -258,46 +258,20 @@ {rabbit_types:msg_id(), non_neg_integer(), A}). -type shutdown_terms() :: [term()] | 'non_clean_shutdown'. --spec erase(rabbit_amqqueue:name()) -> 'ok'. --spec reset_state(qistate()) -> qistate(). --spec init(rabbit_amqqueue:name(), - on_sync_fun(), on_sync_fun()) -> qistate(). --spec recover(rabbit_amqqueue:name(), shutdown_terms(), boolean(), - contains_predicate(), - on_sync_fun(), on_sync_fun()) -> - {'undefined' | non_neg_integer(), - 'undefined' | non_neg_integer(), qistate()}. --spec terminate(rabbit_types:vhost(), [any()], qistate()) -> qistate(). --spec delete_and_terminate(qistate()) -> qistate(). --spec publish(rabbit_types:msg_id(), seq_id(), - rabbit_types:message_properties(), boolean(), - non_neg_integer(), qistate()) -> qistate(). --spec deliver([seq_id()], qistate()) -> qistate(). --spec ack([seq_id()], qistate()) -> qistate(). --spec sync(qistate()) -> qistate(). --spec needs_sync(qistate()) -> 'confirms' | 'other' | 'false'. --spec flush(qistate()) -> qistate(). --spec read(seq_id(), seq_id(), qistate()) -> - {[{rabbit_types:msg_id(), seq_id(), - rabbit_types:message_properties(), - boolean(), boolean()}], qistate()}. --spec next_segment_boundary(seq_id()) -> seq_id(). --spec bounds(qistate()) -> - {non_neg_integer(), non_neg_integer(), qistate()}. --spec start(rabbit_types:vhost(), [rabbit_amqqueue:name()]) -> {[[any()]], {walker(A), A}}. - --spec add_queue_ttl() -> 'ok'. - - %%---------------------------------------------------------------------------- %% public API %%---------------------------------------------------------------------------- +-spec erase(rabbit_amqqueue:name()) -> 'ok'. + erase(Name) -> #qistate { dir = Dir } = blank_state(Name), erase_index_dir(Dir). %% used during variable queue purge when there are no pending acks + +-spec reset_state(qistate()) -> qistate(). + reset_state(#qistate{ queue_name = Name, dir = Dir, on_sync = OnSyncFun, @@ -310,12 +284,21 @@ reset_state(#qistate{ queue_name = Name, ok = erase_index_dir(Dir), blank_state_name_dir_funs(Name, Dir, OnSyncFun, OnSyncMsgFun). +-spec init(rabbit_amqqueue:name(), + on_sync_fun(), on_sync_fun()) -> qistate(). + init(Name, OnSyncFun, OnSyncMsgFun) -> State = #qistate { dir = Dir } = blank_state(Name), false = rabbit_file:is_file(Dir), %% is_file == is file or dir State#qistate{on_sync = OnSyncFun, on_sync_msg = OnSyncMsgFun}. +-spec recover(rabbit_amqqueue:name(), shutdown_terms(), boolean(), + contains_predicate(), + on_sync_fun(), on_sync_fun()) -> + {'undefined' | non_neg_integer(), + 'undefined' | non_neg_integer(), qistate()}. + recover(Name, Terms, MsgStoreRecovered, ContainsCheckFun, OnSyncFun, OnSyncMsgFun) -> State = blank_state(Name), @@ -328,12 +311,16 @@ recover(Name, Terms, MsgStoreRecovered, ContainsCheckFun, false -> init_dirty(CleanShutdown, ContainsCheckFun, State1) end. +-spec terminate(rabbit_types:vhost(), [any()], qistate()) -> qistate(). + terminate(VHost, Terms, State = #qistate { dir = Dir }) -> {SegmentCounts, State1} = terminate(State), rabbit_recovery_terms:store(VHost, filename:basename(Dir), [{segments, SegmentCounts} | Terms]), State1. +-spec delete_and_terminate(qistate()) -> qistate(). + delete_and_terminate(State) -> {_SegmentCounts, State1 = #qistate { dir = Dir }} = terminate(State), ok = rabbit_file:recursive_delete([Dir]), @@ -396,6 +383,10 @@ flush_delivered_cache(State = #qistate{delivered_cache = DC}) -> State1 = deliver(lists:reverse(DC), State), State1#qistate{delivered_cache = []}. +-spec publish(rabbit_types:msg_id(), seq_id(), + rabbit_types:message_properties(), boolean(), + non_neg_integer(), qistate()) -> qistate(). + publish(MsgOrId, SeqId, MsgProps, IsPersistent, JournalSizeHint, State) -> {JournalHdl, State1} = get_journal_handle( @@ -429,20 +420,29 @@ maybe_needs_confirming(MsgProps, MsgOrId, {false, _} -> State end. +-spec deliver([seq_id()], qistate()) -> qistate(). + deliver(SeqIds, State) -> deliver_or_ack(del, SeqIds, State). +-spec ack([seq_id()], qistate()) -> qistate(). + ack(SeqIds, State) -> deliver_or_ack(ack, SeqIds, State). %% This is called when there are outstanding confirms or when the %% queue is idle and the journal needs syncing (see needs_sync/1). + +-spec sync(qistate()) -> qistate(). + sync(State = #qistate { journal_handle = undefined }) -> State; sync(State = #qistate { journal_handle = JournalHdl }) -> ok = file_handle_cache:sync(JournalHdl), notify_sync(State). +-spec needs_sync(qistate()) -> 'confirms' | 'other' | 'false'. + needs_sync(#qistate{journal_handle = undefined}) -> false; needs_sync(#qistate{journal_handle = JournalHdl, @@ -456,9 +456,16 @@ needs_sync(#qistate{journal_handle = JournalHdl, false -> confirms end. +-spec flush(qistate()) -> qistate(). + flush(State = #qistate { dirty_count = 0 }) -> State; flush(State) -> flush_journal(State). +-spec read(seq_id(), seq_id(), qistate()) -> + {[{rabbit_types:msg_id(), seq_id(), + rabbit_types:message_properties(), + boolean(), boolean()}], qistate()}. + read(StartEnd, StartEnd, State) -> {[], State}; read(Start, End, State = #qistate { segments = Segments, @@ -472,10 +479,15 @@ read(Start, End, State = #qistate { segments = Segments, end, {[], Segments}, lists:seq(StartSeg, EndSeg)), {Messages, State #qistate { segments = Segments1 }}. +-spec next_segment_boundary(seq_id()) -> seq_id(). + next_segment_boundary(SeqId) -> {Seg, _RelSeq} = seq_id_to_seg_and_rel_seq_id(SeqId), reconstruct_seq_id(Seg + 1, 0). +-spec bounds(qistate()) -> + {non_neg_integer(), non_neg_integer(), qistate()}. + bounds(State = #qistate { segments = Segments }) -> %% This is not particularly efficient, but only gets invoked on %% queue initialisation. @@ -498,6 +510,8 @@ bounds(State = #qistate { segments = Segments }) -> end, {LowSeqId, NextSeqId, State}. +-spec start(rabbit_types:vhost(), [rabbit_amqqueue:name()]) -> {[[any()]], {walker(A), A}}. + start(VHost, DurableQueueNames) -> ok = rabbit_recovery_terms:start(VHost), {DurableTerms, DurableDirectories} = @@ -1286,6 +1300,8 @@ journal_minus_segment1({no_pub, del, ack}, undefined) -> %% upgrade %%---------------------------------------------------------------------------- +-spec add_queue_ttl() -> 'ok'. + add_queue_ttl() -> foreach_queue_index({fun add_queue_ttl_journal/1, fun add_queue_ttl_segment/1}). diff --git a/src/rabbit_queue_location_client_local.erl b/src/rabbit_queue_location_client_local.erl index aa07637ab1..4c51bf52d2 100644 --- a/src/rabbit_queue_location_client_local.erl +++ b/src/rabbit_queue_location_client_local.erl @@ -17,7 +17,8 @@ -module(rabbit_queue_location_client_local). -behaviour(rabbit_queue_master_locator). --include("rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include("amqqueue.hrl"). -export([description/0, queue_master_location/1]). @@ -37,4 +38,5 @@ description() -> [{description, <<"Locate queue master node as the client local node">>}]. -queue_master_location(#amqqueue{}) -> {ok, node()}. +queue_master_location(Q) when ?is_amqqueue(Q) -> + {ok, node()}. diff --git a/src/rabbit_queue_location_min_masters.erl b/src/rabbit_queue_location_min_masters.erl index a3a3021229..9462e15db9 100644 --- a/src/rabbit_queue_location_min_masters.erl +++ b/src/rabbit_queue_location_min_masters.erl @@ -17,7 +17,8 @@ -module(rabbit_queue_location_min_masters). -behaviour(rabbit_queue_master_locator). --include("rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include("amqqueue.hrl"). -export([description/0, queue_master_location/1]). @@ -37,7 +38,7 @@ description() -> [{description, <<"Locate queue master node from cluster node with least bound queues">>}]. -queue_master_location(#amqqueue{} = Q) -> +queue_master_location(Q) when ?is_amqqueue(Q) -> Cluster = rabbit_queue_master_location_misc:all_nodes(Q), QueueNames = rabbit_amqqueue:list_names(), MastersPerNode = lists:foldl( diff --git a/src/rabbit_queue_location_random.erl b/src/rabbit_queue_location_random.erl index a92c178dfe..e166fa350d 100644 --- a/src/rabbit_queue_location_random.erl +++ b/src/rabbit_queue_location_random.erl @@ -17,7 +17,8 @@ -module(rabbit_queue_location_random). -behaviour(rabbit_queue_master_locator). --include("rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include("amqqueue.hrl"). -export([description/0, queue_master_location/1]). @@ -37,7 +38,7 @@ description() -> [{description, <<"Locate queue master node from cluster in a random manner">>}]. -queue_master_location(#amqqueue{} = Q) -> +queue_master_location(Q) when ?is_amqqueue(Q) -> Cluster = rabbit_queue_master_location_misc:all_nodes(Q), RandomPos = erlang:phash2(erlang:monotonic_time(), length(Cluster)), MasterNode = lists:nth(RandomPos + 1, Cluster), diff --git a/src/rabbit_queue_location_validator.erl b/src/rabbit_queue_location_validator.erl index 68a4bc3e06..2c7bb41b7e 100644 --- a/src/rabbit_queue_location_validator.erl +++ b/src/rabbit_queue_location_validator.erl @@ -17,7 +17,8 @@ -module(rabbit_queue_location_validator). -behaviour(rabbit_policy_validator). --include("rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include("amqqueue.hrl"). -export([validate_policy/1, validate_strategy/1]). @@ -49,7 +50,7 @@ policy(Policy, Q) -> P -> P end. -module(#amqqueue{} = Q) -> +module(Q) when ?is_amqqueue(Q) -> case policy(<<"queue-master-locator">>, Q) of undefined -> no_location_strategy; Mode -> module(Mode) diff --git a/src/rabbit_queue_master_location_misc.erl b/src/rabbit_queue_master_location_misc.erl index 6df7e5db6f..b31fd50192 100644 --- a/src/rabbit_queue_master_location_misc.erl +++ b/src/rabbit_queue_master_location_misc.erl @@ -16,7 +16,8 @@ -module(rabbit_queue_master_location_misc). --include("rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include("amqqueue.hrl"). -export([lookup_master/2, lookup_queue/2, @@ -28,22 +29,25 @@ lookup_master(QueueNameBin, VHostPath) when is_binary(QueueNameBin), is_binary(VHostPath) -> - Queue = rabbit_misc:r(VHostPath, queue, QueueNameBin), - case rabbit_amqqueue:lookup(Queue) of - {ok, #amqqueue{pid = Pid}} when is_pid(Pid) -> + QueueR = rabbit_misc:r(VHostPath, queue, QueueNameBin), + case rabbit_amqqueue:lookup(QueueR) of + {ok, Queue} when ?amqqueue_has_valid_pid(Queue) -> + Pid = amqqueue:get_pid(Queue), {ok, node(Pid)}; Error -> Error end. lookup_queue(QueueNameBin, VHostPath) when is_binary(QueueNameBin), is_binary(VHostPath) -> - Queue = rabbit_misc:r(VHostPath, queue, QueueNameBin), - case rabbit_amqqueue:lookup(Queue) of - Reply = {ok, #amqqueue{}} -> Reply; - Error -> Error + QueueR = rabbit_misc:r(VHostPath, queue, QueueNameBin), + case rabbit_amqqueue:lookup(QueueR) of + Reply = {ok, Queue} when ?is_amqqueue(Queue) -> + Reply; + Error -> + Error end. -get_location(Queue=#amqqueue{})-> +get_location(Queue) when ?is_amqqueue(Queue) -> Reply1 = case get_location_mod_by_args(Queue) of _Err1 = {error, _} -> case get_location_mod_by_policy(Queue) of @@ -62,7 +66,8 @@ get_location(Queue=#amqqueue{})-> Error -> Error end. -get_location_mod_by_args(#amqqueue{arguments=Args}) -> +get_location_mod_by_args(Queue) when ?is_amqqueue(Queue) -> + Args = amqqueue:get_arguments(Queue), case rabbit_misc:table_lookup(Args, <<"x-queue-master-locator">>) of {_Type, Strategy} -> case rabbit_queue_location_validator:validate_strategy(Strategy) of @@ -72,7 +77,7 @@ get_location_mod_by_args(#amqqueue{arguments=Args}) -> _ -> {error, "x-queue-master-locator undefined"} end. -get_location_mod_by_policy(Queue=#amqqueue{}) -> +get_location_mod_by_policy(Queue) when ?is_amqqueue(Queue) -> case rabbit_policy:get(<<"queue-master-locator">> , Queue) of undefined -> {error, "queue-master-locator policy undefined"}; Strategy -> @@ -82,7 +87,7 @@ get_location_mod_by_policy(Queue=#amqqueue{}) -> end end. -get_location_mod_by_config(#amqqueue{}) -> +get_location_mod_by_config(Queue) when ?is_amqqueue(Queue) -> case application:get_env(rabbit, queue_master_locator) of {ok, Strategy} -> case rabbit_queue_location_validator:validate_strategy(Strategy) of @@ -92,7 +97,7 @@ get_location_mod_by_config(#amqqueue{}) -> _ -> {error, "queue_master_locator undefined"} end. -all_nodes(Queue = #amqqueue{}) -> +all_nodes(Queue) when ?is_amqqueue(Queue) -> handle_is_mirrored_ha_nodes(rabbit_mirror_queue_misc:is_mirrored_ha_nodes(Queue), Queue). handle_is_mirrored_ha_nodes(false, _Queue) -> diff --git a/src/rabbit_queue_master_locator.erl b/src/rabbit_queue_master_locator.erl new file mode 100644 index 0000000000..eba1e2aefa --- /dev/null +++ b/src/rabbit_queue_master_locator.erl @@ -0,0 +1,28 @@ +%% The contents of this file are subject to the Mozilla Public License +%% Version 1.1 (the "License"); you may not use this file except in +%% compliance with the License. You may obtain a copy of the License at +%% http://www.mozilla.org/MPL/ +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the +%% License for the specific language governing rights and limitations +%% under the License. +%% +%% The Original Code is RabbitMQ. +%% +%% The Initial Developer of the Original Code is GoPivotal, Inc. +%% Copyright (c) 2007-2017 Pivotal Software, Inc. All rights reserved. +%% + +-module(rabbit_queue_master_locator). + +-behaviour(rabbit_registry_class). + +-export([added_to_rabbit_registry/2, removed_from_rabbit_registry/1]). + +-callback description() -> [proplists:property()]. +-callback queue_master_location(amqqueue:amqqueue()) -> + {'ok', node()} | {'error', term()}. + +added_to_rabbit_registry(_Type, _ModuleName) -> ok. +removed_from_rabbit_registry(_Type) -> ok. diff --git a/src/rabbit_quorum_queue.erl b/src/rabbit_quorum_queue.erl index b4edca7937..725d8086d9 100644 --- a/src/rabbit_quorum_queue.erl +++ b/src/rabbit_quorum_queue.erl @@ -41,31 +41,11 @@ %%-include_lib("rabbit_common/include/rabbit.hrl"). -include_lib("rabbit.hrl"). -include_lib("stdlib/include/qlc.hrl"). +-include("amqqueue.hrl"). --type ra_server_id() :: {Name :: atom(), Node :: node()}. -type msg_id() :: non_neg_integer(). -type qmsg() :: {rabbit_types:r('queue'), pid(), msg_id(), boolean(), rabbit_types:message()}. --spec handle_event({'ra_event', ra_server_id(), any()}, rabbit_fifo_client:state()) -> - {'internal', Correlators :: [term()], rabbit_fifo_client:state()} | - {rabbit_fifo:client_msg(), rabbit_fifo_client:state()}. --spec recover([rabbit_types:amqqueue()]) -> [rabbit_types:amqqueue() | - {'absent', rabbit_types:amqqueue(), atom()}]. --spec stop(rabbit_types:vhost()) -> 'ok'. --spec ack(rabbit_types:ctag(), [msg_id()], rabbit_fifo_client:state()) -> - {'ok', rabbit_fifo_client:state()}. --spec reject(Confirm :: boolean(), rabbit_types:ctag(), [msg_id()], rabbit_fifo_client:state()) -> - {'ok', rabbit_fifo_client:state()}. --spec basic_cancel(rabbit_types:ctag(), ChPid :: pid(), any(), rabbit_fifo_client:state()) -> - {'ok', rabbit_fifo_client:state()}. --spec stateless_deliver(ra_server_id(), rabbit_types:delivery()) -> 'ok'. --spec info(rabbit_types:amqqueue()) -> rabbit_types:infos(). --spec info(rabbit_types:amqqueue(), rabbit_types:info_keys()) -> rabbit_types:infos(). --spec infos(rabbit_types:r('queue')) -> rabbit_types:infos(). --spec stat(rabbit_types:amqqueue()) -> {'ok', non_neg_integer(), non_neg_integer()}. --spec cluster_state(Name :: atom()) -> 'down' | 'recovering' | 'running'. --spec status(rabbit_types:vhost(), Name :: rabbit_misc:resource_name()) -> rabbit_types:infos() | {error, term()}. - -define(STATISTICS_KEYS, [policy, operator_policy, @@ -87,14 +67,15 @@ %%---------------------------------------------------------------------------- --spec init_state(ra_server_id(), rabbit_types:r('queue')) -> - rabbit_fifo_client:state(). +-spec init_state(amqqueue:ra_server_id(), rabbit_amqqueue:name()) -> + rabbit_fifo_client:state(). init_state({Name, _}, QName = #resource{}) -> {ok, SoftLimit} = application:get_env(rabbit, quorum_commands_soft_limit), %% This lookup could potentially return an {error, not_found}, but we do not %% know what to do if the queue has `disappeared`. Let it crash. - {ok, #amqqueue{pid = Leader, quorum_nodes = Nodes}} = - rabbit_amqqueue:lookup(QName), + {ok, Q} = rabbit_amqqueue:lookup(QName), + Leader = amqqueue:get_pid(Q), + Nodes = amqqueue:get_quorum_nodes(Q), %% Ensure the leader is listed first Servers0 = [{Name, N} || N <- Nodes], Servers = [Leader | lists:delete(Leader, Servers0)], @@ -102,17 +83,22 @@ init_state({Name, _}, QName = #resource{}) -> fun() -> credit_flow:block(Name), ok end, fun() -> credit_flow:unblock(Name), ok end). +-spec handle_event({'ra_event', amqqueue:ra_server_id(), any()}, rabbit_fifo_client:state()) -> + {'internal', Correlators :: [term()], rabbit_fifo_client:state()} | + {rabbit_fifo:client_msg(), rabbit_fifo_client:state()}. + handle_event({ra_event, From, Evt}, QState) -> rabbit_fifo_client:handle_ra_event(From, Evt, QState). --spec declare(rabbit_types:amqqueue()) -> - {'new', rabbit_types:amqqueue()} | - {existing, rabbit_types:amqqueue()}. -declare(#amqqueue{name = QName, - durable = Durable, - auto_delete = AutoDelete, - arguments = Arguments, - options = Opts} = Q) -> +-spec declare(amqqueue:amqqueue()) -> + {new | existing, amqqueue:amqqueue()} | rabbit_types:channel_exit(). + +declare(Q) when ?amqqueue_is_quorum(Q) -> + QName = amqqueue:get_name(Q), + Durable = amqqueue:is_durable(Q), + AutoDelete = amqqueue:is_auto_delete(Q), + Arguments = amqqueue:get_arguments(Q), + Opts = amqqueue:get_options(Q), ActingUser = maps:get(user, Opts, ?UNKNOWN_USER), check_invalid_arguments(QName, Arguments), check_auto_delete(Q), @@ -122,9 +108,9 @@ declare(#amqqueue{name = QName, RaName = qname_to_rname(QName), Id = {RaName, node()}, Nodes = select_quorum_nodes(QuorumSize, rabbit_mnesia:cluster_nodes(all)), - NewQ0 = Q#amqqueue{pid = Id, - quorum_nodes = Nodes}, - case rabbit_amqqueue:internal_declare(NewQ0, false) of + NewQ0 = amqqueue:set_pid(Q, Id), + NewQ1 = amqqueue:set_quorum_nodes(NewQ0, Nodes), + case rabbit_amqqueue:internal_declare(NewQ1, false) of {created, NewQ} -> RaMachine = ra_machine(NewQ), case ra:start_cluster(RaName, RaMachine, @@ -150,8 +136,9 @@ declare(#amqqueue{name = QName, ra_machine(Q) -> {module, rabbit_fifo, ra_machine_config(Q)}. -ra_machine_config(Q = #amqqueue{name = QName, - pid = {Name, _}}) -> +ra_machine_config(Q) when ?is_amqqueue(Q) -> + QName = amqqueue:get_name(Q), + {Name, _} = amqqueue:get_pid(Q), %% take the minimum value of the policy and the queue arg if present MaxLength = args_policy_lookup(<<"max-length">>, fun min/2, Q), MaxBytes = args_policy_lookup(<<"max-length-bytes">>, fun min/2, Q), @@ -163,7 +150,8 @@ ra_machine_config(Q = #amqqueue{name = QName, max_bytes => MaxBytes, single_active_consumer_on => single_active_consumer_on(Q)}. -single_active_consumer_on(#amqqueue{arguments = QArguments}) -> +single_active_consumer_on(Q) -> + QArguments = amqqueue:get_arguments(Q), case rabbit_misc:table_lookup(QArguments, <<"x-single-active-consumer">>) of {bool, true} -> true; _ -> false @@ -200,9 +188,10 @@ local_or_remote_handler(ChPid, Module, Function, Args) -> end. become_leader(QName, Name) -> - Fun = fun(Q1) -> - Q1#amqqueue{pid = {Name, node()}, - state = live} + Fun = fun (Q1) -> + amqqueue:set_state( + amqqueue:set_pid(Q1, {Name, node()}), + live) end, %% as this function is called synchronously when a ra node becomes leader %% we need to ensure there is no chance of blocking as else the ra node @@ -213,7 +202,8 @@ become_leader(QName, Name) -> rabbit_amqqueue:update(QName, Fun) end), case rabbit_amqqueue:lookup(QName) of - {ok, #amqqueue{quorum_nodes = Nodes}} -> + {ok, Q0} when ?is_amqqueue(Q0) -> + Nodes = amqqueue:get_quorum_nodes(Q0), [rpc:call(Node, ?MODULE, rpc_delete_metrics, [QName], ?TICK_TIME) || Node <- Nodes, Node =/= node()]; @@ -261,16 +251,20 @@ reductions(Name) -> 0 end. +-spec recover([amqqueue:amqqueue()]) -> [amqqueue:amqqueue() | + {'absent', amqqueue:amqqueue(), atom()}]. + recover(Queues) -> [begin + {Name, _} = amqqueue:get_pid(Q0), + Nodes = amqqueue:get_quorum_nodes(Q0), case ra:restart_server({Name, node()}) of ok -> - % queue was restarted, good ok; - {error, Err} - when Err == not_started orelse - Err == name_not_registered -> + {error, Err1} + when Err1 == not_started orelse + Err1 == name_not_registered -> % queue was never started on this node % so needs to be started from scratch. Machine = ra_machine(Q0), @@ -298,20 +292,27 @@ recover(Queues) -> %% So many code paths are dependent on this. {ok, Q} = rabbit_amqqueue:ensure_rabbit_queue_record_is_initialized(Q0), Q - end || #amqqueue{pid = {Name, _}, - quorum_nodes = Nodes} = Q0 <- Queues]. + end || Q0 <- Queues]. + +-spec stop(rabbit_types:vhost()) -> 'ok'. stop(VHost) -> - _ = [ra:stop_server(Pid) || #amqqueue{pid = Pid} <- find_quorum_queues(VHost)], + _ = [begin + Pid = amqqueue:get_pid(Q), + ra:stop_server(Pid) + end || Q <- find_quorum_queues(VHost)], ok. --spec delete(#amqqueue{}, +-spec delete(amqqueue:amqqueue(), boolean(), boolean(), rabbit_types:username()) -> {ok, QLen :: non_neg_integer()}. -delete(#amqqueue{type = quorum, pid = {Name, _}, - name = QName, quorum_nodes = QNodes} = Q, - _IfUnused, _IfEmpty, ActingUser) -> + +delete(Q, + _IfUnused, _IfEmpty, ActingUser) when ?amqqueue_is_quorum(Q) -> + {Name, _} = amqqueue:get_pid(Q), + QName = amqqueue:get_name(Q), + QNodes = amqqueue:get_quorum_nodes(Q), %% TODO Quorum queue needs to support consumer tracking for IfUnused Timeout = ?DELETE_TIMEOUT, {ok, ReadyMsgs, _} = stat(Q), @@ -377,9 +378,15 @@ delete_immediately(Resource, {_Name, _} = QPid) -> rabbit_core_metrics:queue_deleted(Resource), ok. +-spec ack(rabbit_types:ctag(), [msg_id()], rabbit_fifo_client:state()) -> + {'ok', rabbit_fifo_client:state()}. + ack(CTag, MsgIds, QState) -> rabbit_fifo_client:settle(quorum_ctag(CTag), MsgIds, QState). +-spec reject(Confirm :: boolean(), rabbit_types:ctag(), [msg_id()], rabbit_fifo_client:state()) -> + {'ok', rabbit_fifo_client:state()}. + reject(true, CTag, MsgIds, QState) -> rabbit_fifo_client:return(quorum_ctag(CTag), MsgIds, QState); reject(false, CTag, MsgIds, QState) -> @@ -388,12 +395,15 @@ reject(false, CTag, MsgIds, QState) -> credit(CTag, Credit, Drain, QState) -> rabbit_fifo_client:credit(quorum_ctag(CTag), Credit, Drain, QState). --spec basic_get(#amqqueue{}, NoAck :: boolean(), rabbit_types:ctag(), +-spec basic_get(amqqueue:amqqueue(), NoAck :: boolean(), rabbit_types:ctag(), rabbit_fifo_client:state()) -> {'ok', 'empty', rabbit_fifo_client:state()} | - {'ok', QLen :: non_neg_integer(), qmsg(), rabbit_fifo_client:state()}. -basic_get(#amqqueue{name = QName, pid = Id, type = quorum}, NoAck, - CTag0, QState0) -> + {'ok', QLen :: non_neg_integer(), qmsg(), rabbit_fifo_client:state()} | + {error, timeout | term()}. + +basic_get(Q, NoAck, CTag0, QState0) when ?amqqueue_is_quorum(Q) -> + QName = amqqueue:get_name(Q), + Id = amqqueue:get_pid(Q), CTag = quorum_ctag(CTag0), Settlement = case NoAck of true -> @@ -409,21 +419,26 @@ basic_get(#amqqueue{name = QName, pid = Id, type = quorum}, NoAck, IsDelivered = Count > 0, Msg = rabbit_basic:add_header(<<"x-delivery-count">>, long, Count, Msg0), {ok, MsgsReady, {QName, Id, MsgId, IsDelivered, Msg}, QState}; + {error, _} = Err -> + Err; {timeout, _} -> {error, timeout} end. --spec basic_consume(rabbit_types:amqqueue(), NoAck :: boolean(), ChPid :: pid(), +-spec basic_consume(amqqueue:amqqueue(), NoAck :: boolean(), ChPid :: pid(), ConsumerPrefetchCount :: non_neg_integer(), rabbit_types:ctag(), ExclusiveConsume :: boolean(), Args :: rabbit_framing:amqp_table(), ActingUser :: binary(), any(), rabbit_fifo_client:state()) -> {'ok', rabbit_fifo_client:state()}. -basic_consume(#amqqueue{name = QName, pid = QPid, type = quorum} = Q, NoAck, ChPid, + +basic_consume(Q, NoAck, ChPid, ConsumerPrefetchCount, ConsumerTag0, ExclusiveConsume, Args, - ActingUser, OkMsg, QState0) -> + ActingUser, OkMsg, QState0) when ?amqqueue_is_quorum(Q) -> %% TODO: validate consumer arguments %% currently quorum queues do not support any arguments + QName = amqqueue:get_name(Q), + QPid = amqqueue:get_pid(Q), maybe_send_reply(ChPid, OkMsg), ConsumerTag = quorum_ctag(ConsumerTag0), %% A prefetch count of 0 means no limitation, @@ -461,10 +476,15 @@ basic_consume(#amqqueue{name = QName, pid = QPid, type = quorum} = Q, NoAck, ChP ActivityStatus, Args), {ok, QState}. +-spec basic_cancel(rabbit_types:ctag(), ChPid :: pid(), any(), rabbit_fifo_client:state()) -> + {'ok', rabbit_fifo_client:state()}. + basic_cancel(ConsumerTag, ChPid, OkMsg, QState0) -> maybe_send_reply(ChPid, OkMsg), rabbit_fifo_client:cancel_checkout(quorum_ctag(ConsumerTag), QState0). +-spec stateless_deliver(amqqueue:ra_server_id(), rabbit_types:delivery()) -> 'ok'. + stateless_deliver(ServerId, Delivery) -> ok = rabbit_fifo_client:untracked_enqueue([ServerId], Delivery#delivery.message). @@ -472,16 +492,21 @@ stateless_deliver(ServerId, Delivery) -> -spec deliver(Confirm :: boolean(), rabbit_types:delivery(), rabbit_fifo_client:state()) -> {ok | slow, rabbit_fifo_client:state()}. + deliver(false, Delivery, QState0) -> rabbit_fifo_client:enqueue(Delivery#delivery.message, QState0); deliver(true, Delivery, QState0) -> rabbit_fifo_client:enqueue(Delivery#delivery.msg_seq_no, Delivery#delivery.message, QState0). +-spec info(amqqueue:amqqueue()) -> rabbit_types:infos(). + info(Q) -> info(Q, [name, durable, auto_delete, arguments, pid, state, messages, messages_ready, messages_unacknowledged]). +-spec infos(rabbit_types:r('queue')) -> rabbit_types:infos(). + infos(QName) -> case rabbit_amqqueue:lookup(QName) of {ok, Q} -> @@ -490,10 +515,15 @@ infos(QName) -> [] end. +-spec info(amqqueue:amqqueue(), rabbit_types:info_keys()) -> rabbit_types:infos(). + info(Q, Items) -> [{Item, i(Item, Q)} || Item <- Items]. -stat(#amqqueue{pid = Leader}) -> +-spec stat(amqqueue:amqqueue()) -> {'ok', non_neg_integer(), non_neg_integer()}. + +stat(Q) when ?is_amqqueue(Q) -> + Leader = amqqueue:get_pid(Q), try case rabbit_fifo_client:stat(Leader) of {ok, _, _} = Stat -> @@ -514,9 +544,12 @@ requeue(ConsumerTag, MsgIds, QState) -> rabbit_fifo_client:return(quorum_ctag(ConsumerTag), MsgIds, QState). cleanup_data_dir() -> - Names = [Name || #amqqueue{pid = {Name, _}, quorum_nodes = Nodes} - <- rabbit_amqqueue:list_by_type(quorum), - lists:member(node(), Nodes)], + Names = [begin + {Name, _} = amqqueue:get_pid(Q), + Name + end + || Q <- rabbit_amqqueue:list_by_type(quorum), + lists:member(node(), amqqueue:get_quorum_nodes(Q))], Registered = ra_directory:list_registered(), _ = [maybe_delete_data_dir(UId) || {Name, UId} <- Registered, not lists:member(Name, Names)], @@ -537,6 +570,8 @@ policy_changed(QName, Node) -> {ok, Q} = rabbit_amqqueue:lookup(QName), rabbit_fifo_client:update_machine_state(Node, ra_machine_config(Q)). +-spec cluster_state(Name :: atom()) -> 'down' | 'recovering' | 'running'. + cluster_state(Name) -> case whereis(Name) of undefined -> down; @@ -547,14 +582,18 @@ cluster_state(Name) -> end end. +-spec status(rabbit_types:vhost(), Name :: rabbit_misc:resource_name()) -> rabbit_types:infos() | {error, term()}. + status(Vhost, QueueName) -> %% Handle not found queues QName = #resource{virtual_host = Vhost, name = QueueName, kind = queue}, RName = qname_to_rname(QName), case rabbit_amqqueue:lookup(QName) of - {ok, #amqqueue{type = classic}} -> + {ok, Q} when ?amqqueue_is_classic(Q) -> {error, classic_queue_not_supported}; - {ok, #amqqueue{pid = {_, Leader}, quorum_nodes = Nodes}} -> + {ok, Q} when ?amqqueue_is_quorum(Q) -> + {_, Leader} = amqqueue:get_pid(Q), + Nodes = amqqueue:get_quorum_nodes(Q), Info = [{leader, Leader}, {members, Nodes}], case ets:lookup(ra_state, RName) of [{_, State}] -> @@ -569,9 +608,10 @@ status(Vhost, QueueName) -> add_member(VHost, Name, Node) -> QName = #resource{virtual_host = VHost, name = Name, kind = queue}, case rabbit_amqqueue:lookup(QName) of - {ok, #amqqueue{type = classic}} -> + {ok, Q} when ?amqqueue_is_classic(Q) -> {error, classic_queue_not_supported}; - {ok, #amqqueue{quorum_nodes = QNodes} = Q} -> + {ok, Q} when ?amqqueue_is_quorum(Q) -> + QNodes = amqqueue:get_quorum_nodes(Q), case lists:member(Node, rabbit_mnesia:cluster_nodes(running)) of false -> {error, node_not_running}; @@ -587,8 +627,10 @@ add_member(VHost, Name, Node) -> E end. -add_member(#amqqueue{pid = {RaName, _} = ServerRef, name = QName, - quorum_nodes = QNodes} = Q, Node) -> +add_member(Q, Node) when ?amqqueue_is_quorum(Q) -> + {RaName, _} = ServerRef = amqqueue:get_pid(Q), + QName = amqqueue:get_name(Q), + QNodes = amqqueue:get_quorum_nodes(Q), %% TODO parallel calls might crash this, or add a duplicate in quorum_nodes ServerId = {RaName, Node}, case ra:start_server(RaName, ServerId, ra_machine(Q), @@ -597,9 +639,10 @@ add_member(#amqqueue{pid = {RaName, _} = ServerRef, name = QName, case ra:add_member(ServerRef, ServerId) of {ok, _, Leader} -> Fun = fun(Q1) -> - Q1#amqqueue{quorum_nodes = - [Node | Q1#amqqueue.quorum_nodes], - pid = Leader} + Q2 = amqqueue:set_quorum_nodes( + Q1, + [Node | amqqueue:get_quorum_nodes(Q1)]), + amqqueue:set_pid(Q2, Leader) end, rabbit_misc:execute_mnesia_transaction( fun() -> rabbit_amqqueue:update(QName, Fun) end), @@ -615,9 +658,10 @@ add_member(#amqqueue{pid = {RaName, _} = ServerRef, name = QName, delete_member(VHost, Name, Node) -> QName = #resource{virtual_host = VHost, name = Name, kind = queue}, case rabbit_amqqueue:lookup(QName) of - {ok, #amqqueue{type = classic}} -> + {ok, Q} when ?amqqueue_is_classic(Q) -> {error, classic_queue_not_supported}; - {ok, #amqqueue{quorum_nodes = QNodes} = Q} -> + {ok, Q} when ?amqqueue_is_quorum(Q) -> + QNodes = amqqueue:get_quorum_nodes(Q), case lists:member(Node, rabbit_mnesia:cluster_nodes(running)) of false -> {error, node_not_running}; @@ -633,13 +677,16 @@ delete_member(VHost, Name, Node) -> E end. -delete_member(#amqqueue{pid = {RaName, _}, name = QName}, Node) -> +delete_member(Q, Node) when ?amqqueue_is_quorum(Q) -> + QName = amqqueue:get_name(Q), + {RaName, _} = amqqueue:get_pid(Q), ServerId = {RaName, Node}, case ra:leave_and_delete_server(ServerId) of ok -> Fun = fun(Q1) -> - Q1#amqqueue{quorum_nodes = - lists:delete(Node, Q1#amqqueue.quorum_nodes)} + amqqueue:set_quorum_nodes( + Q1, + lists:delete(Node, amqqueue:get_quorum_nodes(Q1))) end, rabbit_misc:execute_mnesia_transaction( fun() -> rabbit_amqqueue:update(QName, Fun) end), @@ -654,16 +701,18 @@ dlx_mfa(Q) -> fun res_arg/2, Q), Q), DLXRKey = args_policy_lookup(<<"dead-letter-routing-key">>, fun res_arg/2, Q), - {?MODULE, dead_letter_publish, [DLX, DLXRKey, Q#amqqueue.name]}. + {?MODULE, dead_letter_publish, [DLX, DLXRKey, amqqueue:get_name(Q)]}. init_dlx(undefined, _Q) -> undefined; -init_dlx(DLX, #amqqueue{name = QName}) -> +init_dlx(DLX, Q) when ?is_amqqueue(Q) -> + QName = amqqueue:get_name(Q), rabbit_misc:r(QName, exchange, DLX). res_arg(_PolVal, ArgVal) -> ArgVal. -args_policy_lookup(Name, Resolve, Q = #amqqueue{arguments = Args}) -> +args_policy_lookup(Name, Resolve, Q) when ?is_amqqueue(Q) -> + Args = amqqueue:get_arguments(Q), AName = <<"x-", Name/binary>>, case {rabbit_policy:get(Name, Q), rabbit_misc:table_lookup(Args, AName)} of {undefined, undefined} -> undefined; @@ -693,29 +742,32 @@ find_quorum_queues(VHost) -> Node = node(), mnesia:async_dirty( fun () -> - qlc:e(qlc:q([Q || Q = #amqqueue{vhost = VH, - pid = Pid, - type = quorum} - <- mnesia:table(rabbit_durable_queue), - VH =:= VHost, - qnode(Pid) == Node])) + qlc:e(qlc:q([Q || Q <- mnesia:table(rabbit_durable_queue), + ?amqqueue_is_quorum(Q), + amqqueue:get_vhost(Q) =:= VHost, + amqqueue:qnode(Q) == Node])) end). -i(name, #amqqueue{name = Name}) -> Name; -i(durable, #amqqueue{durable = Dur}) -> Dur; -i(auto_delete, #amqqueue{auto_delete = AD}) -> AD; -i(arguments, #amqqueue{arguments = Args}) -> Args; -i(pid, #amqqueue{pid = {Name, _}}) -> whereis(Name); -i(messages, #amqqueue{pid = {Name, _}}) -> +i(name, Q) when ?is_amqqueue(Q) -> amqqueue:get_name(Q); +i(durable, Q) when ?is_amqqueue(Q) -> amqqueue:is_durable(Q); +i(auto_delete, Q) when ?is_amqqueue(Q) -> amqqueue:is_auto_delete(Q); +i(arguments, Q) when ?is_amqqueue(Q) -> amqqueue:get_arguments(Q); +i(pid, Q) when ?is_amqqueue(Q) -> + {Name, _} = amqqueue:get_pid(Q), + whereis(Name); +i(messages, Q) when ?is_amqqueue(Q) -> + {Name, _} = amqqueue:get_pid(Q), quorum_messages(Name); -i(messages_ready, #amqqueue{name = QName}) -> +i(messages_ready, Q) when ?is_amqqueue(Q) -> + QName = amqqueue:get_name(Q), case ets:lookup(queue_coarse_metrics, QName) of [{_, MR, _, _, _}] -> MR; [] -> 0 end; -i(messages_unacknowledged, #amqqueue{name = QName}) -> +i(messages_unacknowledged, Q) when ?is_amqqueue(Q) -> + QName = amqqueue:get_name(Q), case ets:lookup(queue_coarse_metrics, QName) of [{_, _, MU, _, _}] -> MU; @@ -737,14 +789,16 @@ i(effective_policy_definition, Q) -> undefined -> []; Def -> Def end; -i(consumers, #amqqueue{name = QName}) -> +i(consumers, Q) when ?is_amqqueue(Q) -> + QName = amqqueue:get_name(Q), case ets:lookup(queue_metrics, QName) of [{_, M, _}] -> proplists:get_value(consumers, M, 0); [] -> 0 end; -i(memory, #amqqueue{pid = {Name, _}}) -> +i(memory, Q) when ?is_amqqueue(Q) -> + {Name, _} = amqqueue:get_pid(Q), try {memory, M} = process_info(whereis(Name), memory), M @@ -752,33 +806,38 @@ i(memory, #amqqueue{pid = {Name, _}}) -> error:badarg -> 0 end; -i(state, #amqqueue{pid = {Name, Node}}) -> +i(state, Q) when ?is_amqqueue(Q) -> + {Name, Node} = amqqueue:get_pid(Q), %% Check against the leader or last known leader case rpc:call(Node, ?MODULE, cluster_state, [Name], ?TICK_TIME) of {badrpc, _} -> down; State -> State end; -i(local_state, #amqqueue{pid = {Name, _}}) -> +i(local_state, Q) when ?is_amqqueue(Q) -> + {Name, _} = amqqueue:get_pid(Q), case ets:lookup(ra_state, Name) of [{_, State}] -> State; _ -> not_member end; -i(garbage_collection, #amqqueue{pid = {Name, _}}) -> +i(garbage_collection, Q) when ?is_amqqueue(Q) -> + {Name, _} = amqqueue:get_pid(Q), try rabbit_misc:get_gc_info(whereis(Name)) catch error:badarg -> [] end; -i(members, #amqqueue{quorum_nodes = Nodes}) -> - Nodes; +i(members, Q) when ?is_amqqueue(Q) -> + amqqueue:get_quorum_nodes(Q); i(online, Q) -> online(Q); i(leader, Q) -> leader(Q); -i(open_files, #amqqueue{pid = {Name, _}, - quorum_nodes = Nodes}) -> +i(open_files, Q) when ?is_amqqueue(Q) -> + {Name, _} = amqqueue:get_pid(Q), + Nodes = amqqueue:get_quorum_nodes(Q), {Data, _} = rpc:multicall(Nodes, rabbit_quorum_queue, open_files, [Name]), lists:flatten(Data); -i(single_active_consumer_pid, #amqqueue{pid = QPid}) -> +i(single_active_consumer_pid, Q) when ?is_amqqueue(Q) -> + QPid = amqqueue:get_pid(Q), {ok, {_, SacResult}, _} = ra:local_query(QPid, fun rabbit_fifo:query_single_active_consumer/1), case SacResult of @@ -787,7 +846,8 @@ i(single_active_consumer_pid, #amqqueue{pid = QPid}) -> _ -> '' end; -i(single_active_consumer_ctag, #amqqueue{pid = QPid}) -> +i(single_active_consumer_ctag, Q) when ?is_amqqueue(Q) -> + QPid = amqqueue:get_pid(Q), {ok, {_, SacResult}, _} = ra:local_query(QPid, fun rabbit_fifo:query_single_active_consumer/1), case SacResult of @@ -807,22 +867,27 @@ open_files(Name) -> end end. -leader(#amqqueue{pid = {Name, Leader}}) -> +leader(Q) when ?is_amqqueue(Q) -> + {Name, Leader} = amqqueue:get_pid(Q), case is_process_alive(Name, Leader) of true -> Leader; false -> '' end. -online(#amqqueue{quorum_nodes = Nodes, - pid = {Name, _Leader}}) -> +online(Q) when ?is_amqqueue(Q) -> + Nodes = amqqueue:get_quorum_nodes(Q), + {Name, _} = amqqueue:get_pid(Q), [Node || Node <- Nodes, is_process_alive(Name, Node)]. -format(#amqqueue{quorum_nodes = Nodes} = Q) -> +format(Q) when ?is_amqqueue(Q) -> + Nodes = amqqueue:get_quorum_nodes(Q), [{members, Nodes}, {online, online(Q)}, {leader, leader(Q)}]. is_process_alive(Name, Node) -> erlang:is_pid(rpc:call(Node, erlang, whereis, [Name], ?TICK_TIME)). +-spec quorum_messages(atom()) -> non_neg_integer(). + quorum_messages(QName) -> case ets:lookup(queue_coarse_metrics, QName) of [{_, _, _, M, _}] -> @@ -839,11 +904,6 @@ quorum_ctag(Other) -> maybe_send_reply(_ChPid, undefined) -> ok; maybe_send_reply(ChPid, Msg) -> ok = rabbit_channel:send_command(ChPid, Msg). -qnode(QPid) when is_pid(QPid) -> - node(QPid); -qnode({_, Node}) -> - Node. - check_invalid_arguments(QueueName, Args) -> Keys = [<<"x-expires">>, <<"x-message-ttl">>, <<"x-max-priority">>, <<"x-queue-mode">>, <<"x-overflow">>], @@ -856,7 +916,8 @@ check_invalid_arguments(QueueName, Args) -> end || Key <- Keys], ok. -check_auto_delete(#amqqueue{auto_delete = true, name = Name}) -> +check_auto_delete(Q) when ?amqqueue_is_auto_delete(Q) -> + Name = amqqueue:get_name(Q), rabbit_misc:protocol_error( precondition_failed, "invalid property 'auto-delete' for ~s", @@ -864,18 +925,19 @@ check_auto_delete(#amqqueue{auto_delete = true, name = Name}) -> check_auto_delete(_) -> ok. -check_exclusive(#amqqueue{exclusive_owner = none}) -> +check_exclusive(Q) when ?amqqueue_exclusive_owner_is(Q, none) -> ok; -check_exclusive(#amqqueue{name = Name}) -> +check_exclusive(Q) when ?is_amqqueue(Q) -> + Name = amqqueue:get_name(Q), rabbit_misc:protocol_error( precondition_failed, "invalid property 'exclusive-owner' for ~s", [rabbit_misc:rs(Name)]). -check_non_durable(#amqqueue{durable = true}) -> +check_non_durable(Q) when ?amqqueue_is_durable(Q) -> ok; -check_non_durable(#amqqueue{name = Name, - durable = false}) -> +check_non_durable(Q) when not ?amqqueue_is_durable(Q) -> + Name = amqqueue:get_name(Q), rabbit_misc:protocol_error( precondition_failed, "invalid property 'non-durable' for ~s", diff --git a/src/rabbit_reader.erl b/src/rabbit_reader.erl index 9a4c143c54..95a6b185c2 100644 --- a/src/rabbit_reader.erl +++ b/src/rabbit_reader.erl @@ -57,7 +57,7 @@ -include("rabbit_framing.hrl"). -include("rabbit.hrl"). --export([start_link/2, info_keys/0, info/1, info/2, +-export([start_link/2, info_keys/0, info/1, info/2, force_event_refresh/2, shutdown/2]). -export([system_continue/3, system_terminate/4, system_code_change/4]). @@ -66,6 +66,8 @@ -export([conserve_resources/3, server_properties/1]). +-deprecated([{force_event_refresh, 2, eventually}]). + -define(NORMAL_TIMEOUT, 3). -define(CLOSING_TIMEOUT, 30). -define(CHANNEL_TERMINATION_TIMEOUT, 3). @@ -157,38 +159,26 @@ %%-------------------------------------------------------------------------- --spec start_link(pid(), any()) -> rabbit_types:ok(pid()). --spec info_keys() -> rabbit_types:info_keys(). --spec info(pid()) -> rabbit_types:infos(). --spec info(pid(), rabbit_types:info_keys()) -> rabbit_types:infos(). --spec shutdown(pid(), string()) -> 'ok'. -type resource_alert() :: {WasAlarmSetForNode :: boolean(), IsThereAnyAlarmsWithSameSourceInTheCluster :: boolean(), NodeForWhichAlarmWasSetOrCleared :: node()}. --spec conserve_resources(pid(), atom(), resource_alert()) -> 'ok'. --spec server_properties(rabbit_types:protocol()) -> - rabbit_framing:amqp_table(). - -%% These specs only exists to add no_return() to keep dialyzer happy --spec init(pid(), pid(), any()) -> no_return(). --spec start_connection(pid(), pid(), any(), rabbit_net:socket()) -> - no_return(). - --spec mainloop(_,[binary()], non_neg_integer(), #v1{}) -> any(). --spec system_code_change(_,_,_,_) -> {'ok',_}. --spec system_continue(_,_,{[binary()], non_neg_integer(), #v1{}}) -> any(). --spec system_terminate(_,_,_,_) -> none(). %%-------------------------------------------------------------------------- +-spec start_link(pid(), any()) -> rabbit_types:ok(pid()). + start_link(HelperSup, Ref) -> Pid = proc_lib:spawn_link(?MODULE, init, [self(), HelperSup, Ref]), {ok, Pid}. +-spec shutdown(pid(), string()) -> 'ok'. + shutdown(Pid, Explanation) -> gen_server:call(Pid, {shutdown, Explanation}, infinity). +-spec init(pid(), pid(), any()) -> no_return(). + init(Parent, HelperSup, Ref) -> ?LG_PROCESS_TYPE(reader), {ok, Sock} = rabbit_networking:handshake(Ref, @@ -196,30 +186,52 @@ init(Parent, HelperSup, Ref) -> Deb = sys:debug_options([]), start_connection(Parent, HelperSup, Deb, Sock). +-spec system_continue(_,_,{[binary()], non_neg_integer(), #v1{}}) -> any(). + system_continue(Parent, Deb, {Buf, BufLen, State}) -> mainloop(Deb, Buf, BufLen, State#v1{parent = Parent}). +-spec system_terminate(_,_,_,_) -> none(). + system_terminate(Reason, _Parent, _Deb, _State) -> exit(Reason). +-spec system_code_change(_,_,_,_) -> {'ok',_}. + system_code_change(Misc, _Module, _OldVsn, _Extra) -> {ok, Misc}. +-spec info_keys() -> rabbit_types:info_keys(). + info_keys() -> ?INFO_KEYS. +-spec info(pid()) -> rabbit_types:infos(). + info(Pid) -> gen_server:call(Pid, info, infinity). +-spec info(pid(), rabbit_types:info_keys()) -> rabbit_types:infos(). + info(Pid, Items) -> case gen_server:call(Pid, {info, Items}, infinity) of {ok, Res} -> Res; {error, Error} -> throw(Error) end. +-spec force_event_refresh(pid(), reference()) -> 'ok'. + +force_event_refresh(Pid, Ref) -> + gen_server:cast(Pid, {force_event_refresh, Ref}). + +-spec conserve_resources(pid(), atom(), resource_alert()) -> 'ok'. + conserve_resources(Pid, Source, {_, Conserve, _}) -> Pid ! {conserve_resources, Source, Conserve}, ok. +-spec server_properties(rabbit_types:protocol()) -> + rabbit_framing:amqp_table(). + server_properties(Protocol) -> {ok, Product} = application:get_key(rabbit, description), {ok, Version} = application:get_key(rabbit, vsn), @@ -297,6 +309,9 @@ socket_op(Sock, Fun) -> exit(normal) end. +-spec start_connection(pid(), pid(), any(), rabbit_net:socket()) -> + no_return(). + start_connection(Parent, HelperSup, Deb, Sock) -> process_flag(trap_exit, true), RealSocket = rabbit_net:unwrap_socket(Sock), @@ -487,6 +502,8 @@ binlist_split(Len, L, [Acc0|Acc]) when Len < 0 -> binlist_split(Len, [H|T], Acc) -> binlist_split(Len - size(H), T, [H|Acc]). +-spec mainloop(_,[binary()], non_neg_integer(), #v1{}) -> any(). + mainloop(Deb, Buf, BufLen, State = #v1{sock = Sock, connection_state = CS, connection = #connection{ @@ -615,6 +632,17 @@ handle_other({'$gen_call', From, {info, Items}}, State) -> catch Error -> {error, Error} end), State; +handle_other({'$gen_cast', {force_event_refresh, Ref}}, State) + when ?IS_RUNNING(State) -> + rabbit_event:notify( + connection_created, + augment_infos_with_user_provided_connection_name( + [{type, network} | infos(?CREATION_EVENT_KEYS, State)], State), + Ref), + rabbit_event:init_stats_timer(State, #v1.stats_timer); +handle_other({'$gen_cast', {force_event_refresh, _Ref}}, State) -> + %% Ignore, we will emit a created event once we start running. + State; handle_other(ensure_stats, State) -> ensure_stats_timer(State); handle_other(emit_stats, State) -> diff --git a/src/rabbit_recovery_terms.erl b/src/rabbit_recovery_terms.erl index ab70fa2be7..28bd9fc23a 100644 --- a/src/rabbit_recovery_terms.erl +++ b/src/rabbit_recovery_terms.erl @@ -40,12 +40,6 @@ %%---------------------------------------------------------------------------- -spec start(rabbit_types:vhost()) -> rabbit_types:ok_or_error(term()). --spec stop(rabbit_types:vhost()) -> rabbit_types:ok_or_error(term()). --spec store(rabbit_types:vhost(), file:filename(), term()) -> rabbit_types:ok_or_error(term()). --spec read(rabbit_types:vhost(), file:filename()) -> rabbit_types:ok_or_error2(term(), not_found). --spec clear(rabbit_types:vhost()) -> 'ok'. - -%%---------------------------------------------------------------------------- start(VHost) -> case rabbit_vhost_sup_sup:get_vhost_sup(VHost) of @@ -64,6 +58,8 @@ start(VHost) -> end, ok. +-spec stop(rabbit_types:vhost()) -> rabbit_types:ok_or_error(term()). + stop(VHost) -> case rabbit_vhost_sup_sup:get_vhost_sup(VHost) of {ok, VHostSup} -> @@ -79,15 +75,21 @@ stop(VHost) -> ok end. +-spec store(rabbit_types:vhost(), file:filename(), term()) -> rabbit_types:ok_or_error(term()). + store(VHost, DirBaseName, Terms) -> dets:insert(VHost, {DirBaseName, Terms}). +-spec read(rabbit_types:vhost(), file:filename()) -> rabbit_types:ok_or_error2(term(), not_found). + read(VHost, DirBaseName) -> case dets:lookup(VHost, DirBaseName) of [{_, Terms}] -> {ok, Terms}; _ -> {error, not_found} end. +-spec clear(rabbit_types:vhost()) -> 'ok'. + clear(VHost) -> try dets:delete_all_objects(VHost) @@ -115,7 +117,7 @@ upgrade_recovery_terms() -> [begin File = filename:join([QueuesDir, Dir, "clean.dot"]), case rabbit_file:read_term_file(File) of - {ok, Terms} -> ok = store(?MODULE, Dir, Terms); + {ok, Terms} -> ok = store_global_table(Dir, Terms); {error, _} -> ok end, file:delete(File) @@ -132,7 +134,7 @@ dets_upgrade(Fun)-> open_global_table(), try ok = dets:foldl(fun ({DirBaseName, Terms}, Acc) -> - store(?MODULE, DirBaseName, Fun(Terms)), + store_global_table(DirBaseName, Fun(Terms)), Acc end, ok, ?MODULE), ok @@ -158,8 +160,14 @@ close_global_table() -> ok end. +store_global_table(DirBaseName, Terms) -> + dets:insert(?MODULE, {DirBaseName, Terms}). + read_global(DirBaseName) -> - read(?MODULE, DirBaseName). + case dets:lookup(?MODULE, DirBaseName) of + [{_, Terms}] -> {ok, Terms}; + _ -> {error, not_found} + end. delete_global_table() -> file:delete(filename:join(rabbit_mnesia:dir(), "recovery.dets")). diff --git a/src/rabbit_restartable_sup.erl b/src/rabbit_restartable_sup.erl index ecbd10a3d7..dbf5a24b50 100644 --- a/src/rabbit_restartable_sup.erl +++ b/src/rabbit_restartable_sup.erl @@ -31,8 +31,6 @@ -spec start_link(atom(), rabbit_types:mfargs(), boolean()) -> rabbit_types:ok_pid_or_error(). -%%---------------------------------------------------------------------------- - start_link(Name, {_M, _F, _A} = Fun, Delay) -> supervisor2:start_link({local, Name}, ?MODULE, [Fun, Delay]). diff --git a/src/rabbit_sup.erl b/src/rabbit_sup.erl index a1a0e4897d..2a0ae34e3c 100644 --- a/src/rabbit_sup.erl +++ b/src/rabbit_sup.erl @@ -34,53 +34,63 @@ %%---------------------------------------------------------------------------- -spec start_link() -> rabbit_types:ok_pid_or_error(). --spec start_child(atom()) -> 'ok'. --spec start_child(atom(), [any()]) -> 'ok'. --spec start_child(atom(), atom(), [any()]) -> 'ok'. --spec start_child(atom(), atom(), atom(), [any()]) -> 'ok'. --spec start_supervisor_child(atom()) -> 'ok'. --spec start_supervisor_child(atom(), [any()]) -> 'ok'. --spec start_supervisor_child(atom(), atom(), [any()]) -> 'ok'. --spec start_restartable_child(atom()) -> 'ok'. --spec start_restartable_child(atom(), [any()]) -> 'ok'. --spec start_delayed_restartable_child(atom()) -> 'ok'. --spec start_delayed_restartable_child(atom(), [any()]) -> 'ok'. --spec stop_child(atom()) -> rabbit_types:ok_or_error(any()). - -%%---------------------------------------------------------------------------- start_link() -> supervisor:start_link({local, ?SERVER}, ?MODULE, []). +-spec start_child(atom()) -> 'ok'. + start_child(Mod) -> start_child(Mod, []). +-spec start_child(atom(), [any()]) -> 'ok'. + start_child(Mod, Args) -> start_child(Mod, Mod, Args). +-spec start_child(atom(), atom(), [any()]) -> 'ok'. + start_child(ChildId, Mod, Args) -> child_reply(supervisor:start_child( ?SERVER, {ChildId, {Mod, start_link, Args}, transient, ?WORKER_WAIT, worker, [Mod]})). +-spec start_child(atom(), atom(), atom(), [any()]) -> 'ok'. + start_child(ChildId, Mod, Fun, Args) -> child_reply(supervisor:start_child( ?SERVER, {ChildId, {Mod, Fun, Args}, transient, ?WORKER_WAIT, worker, [Mod]})). +-spec start_supervisor_child(atom()) -> 'ok'. start_supervisor_child(Mod) -> start_supervisor_child(Mod, []). +-spec start_supervisor_child(atom(), [any()]) -> 'ok'. + start_supervisor_child(Mod, Args) -> start_supervisor_child(Mod, Mod, Args). +-spec start_supervisor_child(atom(), atom(), [any()]) -> 'ok'. + start_supervisor_child(ChildId, Mod, Args) -> child_reply(supervisor:start_child( ?SERVER, {ChildId, {Mod, start_link, Args}, transient, infinity, supervisor, [Mod]})). +-spec start_restartable_child(atom()) -> 'ok'. + start_restartable_child(M) -> start_restartable_child(M, [], false). + +-spec start_restartable_child(atom(), [any()]) -> 'ok'. + start_restartable_child(M, A) -> start_restartable_child(M, A, false). + +-spec start_delayed_restartable_child(atom()) -> 'ok'. + start_delayed_restartable_child(M) -> start_restartable_child(M, [], true). + +-spec start_delayed_restartable_child(atom(), [any()]) -> 'ok'. + start_delayed_restartable_child(M, A) -> start_restartable_child(M, A, true). start_restartable_child(Mod, Args, Delay) -> @@ -91,6 +101,8 @@ start_restartable_child(Mod, Args, Delay) -> [Name, {Mod, start_link, Args}, Delay]}, transient, infinity, supervisor, [rabbit_restartable_sup]})). +-spec stop_child(atom()) -> rabbit_types:ok_or_error(any()). + stop_child(ChildId) -> case supervisor:terminate_child(?SERVER, ChildId) of ok -> supervisor:delete_child(?SERVER, ChildId); diff --git a/src/rabbit_table.erl b/src/rabbit_table.erl index 1fab94fe34..9bf1d2c3f6 100644 --- a/src/rabbit_table.erl +++ b/src/rabbit_table.erl @@ -24,28 +24,18 @@ %% for testing purposes -export([definitions/0]). --include("rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). %%---------------------------------------------------------------------------- --type retry() :: boolean(). --spec create() -> 'ok'. --spec create_local_copy('disc' | 'ram') -> 'ok'. --spec wait_for_replicated(retry()) -> 'ok'. --spec wait_for_replicated() -> 'ok'. --spec wait([atom()]) -> 'ok'. --spec retry_timeout() -> {non_neg_integer() | infinity, non_neg_integer()}. --spec force_load() -> 'ok'. --spec is_present() -> boolean(). --spec is_empty() -> boolean(). --spec needs_default_data() -> boolean(). --spec check_schema_integrity(retry()) -> rabbit_types:ok_or_error(any()). --spec clear_ram_only_tables() -> 'ok'. +-type retry() :: boolean(). %%---------------------------------------------------------------------------- %% Main interface %%---------------------------------------------------------------------------- +-spec create() -> 'ok'. + create() -> lists:foreach(fun ({Tab, TabDef}) -> TabDef1 = proplists:delete(match, TabDef), @@ -74,6 +64,9 @@ ensure_secondary_index(Table, Field) -> %% tables is important: if we delete the schema first when moving to %% RAM mnesia will loudly complain since it doesn't make much sense to %% do that. But when moving to disc, we need to move the schema first. + +-spec create_local_copy('disc' | 'ram') -> 'ok'. + create_local_copy(disc) -> create_local_copy(schema, disc_copies), create_local_copies(disc); @@ -83,13 +76,20 @@ create_local_copy(ram) -> %% This arity only exists for backwards compatibility with certain %% plugins. See https://github.com/rabbitmq/rabbitmq-clusterer/issues/19. + +-spec wait_for_replicated() -> 'ok'. + wait_for_replicated() -> wait_for_replicated(false). +-spec wait_for_replicated(retry()) -> 'ok'. + wait_for_replicated(Retry) -> wait([Tab || {Tab, TabDef} <- definitions(), not lists:member({local_content, true}, TabDef)], Retry). +-spec wait([atom()]) -> 'ok'. + wait(TableNames) -> wait(TableNames, _Retry = false). @@ -117,8 +117,6 @@ wait(TableNames, Timeout, Retries) -> throw(Error); {_, {error, Error}} -> rabbit_log:warning("Error while waiting for Mnesia tables: ~p~n", [Error]), - wait(TableNames, Timeout, Retries - 1); - _ -> wait(TableNames, Timeout, Retries - 1) end. @@ -131,17 +129,28 @@ retry_timeout(_Retry = true) -> end, {retry_timeout(), Retries}. +-spec retry_timeout() -> non_neg_integer() | infinity. + retry_timeout() -> case application:get_env(rabbit, mnesia_table_loading_retry_timeout) of {ok, T} -> T; undefined -> 30000 end. +-spec force_load() -> 'ok'. + force_load() -> [mnesia:force_load_table(T) || T <- names()], ok. +-spec is_present() -> boolean(). + is_present() -> names() -- mnesia:system_info(tables) =:= []. +-spec is_empty() -> boolean(). + is_empty() -> is_empty(names()). + +-spec needs_default_data() -> boolean(). + needs_default_data() -> is_empty([rabbit_user, rabbit_user_permission, rabbit_vhost]). @@ -149,6 +158,8 @@ is_empty(Names) -> lists:all(fun (Tab) -> mnesia:dirty_first(Tab) == '$end_of_table' end, Names). +-spec check_schema_integrity(retry()) -> rabbit_types:ok_or_error(any()). + check_schema_integrity(Retry) -> Tables = mnesia:system_info(tables), case check(fun (Tab, TabDef) -> @@ -162,6 +173,8 @@ check_schema_integrity(Retry) -> Other -> Other end. +-spec clear_ram_only_tables() -> 'ok'. + clear_ram_only_tables() -> Node = node(), lists:foreach( @@ -349,13 +362,13 @@ definitions() -> {match, #runtime_parameters{_='_'}}]}, {rabbit_durable_queue, [{record_name, amqqueue}, - {attributes, record_info(fields, amqqueue)}, + {attributes, amqqueue:fields()}, {disc_copies, [node()]}, - {match, #amqqueue{name = queue_name_match(), _='_'}}]}, + {match, amqqueue:pattern_match_on_name(queue_name_match())}]}, {rabbit_queue, [{record_name, amqqueue}, - {attributes, record_info(fields, amqqueue)}, - {match, #amqqueue{name = queue_name_match(), _='_'}}]}] + {attributes, amqqueue:fields()}, + {match, amqqueue:pattern_match_on_name(queue_name_match())}]}] ++ gm:table_definitions() ++ mirrored_supervisor:table_definitions(). diff --git a/src/rabbit_trace.erl b/src/rabbit_trace.erl index 6047eb24a3..2c85de2f3a 100644 --- a/src/rabbit_trace.erl +++ b/src/rabbit_trace.erl @@ -28,20 +28,10 @@ -type state() :: rabbit_types:exchange() | 'none'. --spec init(rabbit_types:vhost()) -> state(). --spec enabled(rabbit_types:vhost()) -> boolean(). --spec tap_in(rabbit_types:basic_message(), [rabbit_amqqueue:name()], - binary(), rabbit_channel:channel_number(), - rabbit_types:username(), state()) -> 'ok'. --spec tap_out(rabbit_amqqueue:qmsg(), binary(), - rabbit_channel:channel_number(), - rabbit_types:username(), state()) -> 'ok'. - --spec start(rabbit_types:vhost()) -> 'ok'. --spec stop(rabbit_types:vhost()) -> 'ok'. - %%---------------------------------------------------------------------------- +-spec init(rabbit_types:vhost()) -> state(). + init(VHost) -> case enabled(VHost) of false -> none; @@ -50,10 +40,16 @@ init(VHost) -> X end. +-spec enabled(rabbit_types:vhost()) -> boolean(). + enabled(VHost) -> {ok, VHosts} = application:get_env(rabbit, ?TRACE_VHOSTS), lists:member(VHost, VHosts). +-spec tap_in(rabbit_types:basic_message(), [rabbit_amqqueue:name()], + binary(), rabbit_channel:channel_number(), + rabbit_types:username(), state()) -> 'ok'. + tap_in(_Msg, _QNames, _ConnName, _ChannelNum, _Username, none) -> ok; tap_in(Msg = #basic_message{exchange_name = #resource{name = XName, virtual_host = VHost}}, @@ -66,6 +62,10 @@ tap_in(Msg = #basic_message{exchange_name = #resource{name = XName, {<<"routed_queues">>, array, [{longstr, QName#resource.name} || QName <- QNames]}]). +-spec tap_out(rabbit_amqqueue:qmsg(), binary(), + rabbit_channel:channel_number(), + rabbit_types:username(), state()) -> 'ok'. + tap_out(_Msg, _ConnName, _ChannelNum, _Username, none) -> ok; tap_out({#resource{name = QName, virtual_host = VHost}, _QPid, _QMsgId, Redelivered, Msg}, @@ -80,10 +80,14 @@ tap_out({#resource{name = QName, virtual_host = VHost}, %%---------------------------------------------------------------------------- +-spec start(rabbit_types:vhost()) -> 'ok'. + start(VHost) -> rabbit_log:info("Enabling tracing for vhost '~s'~n", [VHost]), update_config(fun (VHosts) -> [VHost | VHosts -- [VHost]] end). +-spec stop(rabbit_types:vhost()) -> 'ok'. + stop(VHost) -> rabbit_log:info("Disabling tracing for vhost '~s'~n", [VHost]), update_config(fun (VHosts) -> VHosts -- [VHost] end). diff --git a/src/rabbit_upgrade.erl b/src/rabbit_upgrade.erl index 07eef293d3..f452d5c92f 100644 --- a/src/rabbit_upgrade.erl +++ b/src/rabbit_upgrade.erl @@ -27,14 +27,6 @@ %% ------------------------------------------------------------------- --spec maybe_upgrade_mnesia() -> 'ok'. --spec maybe_upgrade_local() -> - 'ok' | - 'version_not_available' | - 'starting_from_scratch'. - -%% ------------------------------------------------------------------- - %% The upgrade logic is quite involved, due to the existence of %% clusters. %% @@ -125,6 +117,8 @@ remove_backup() -> ok = rabbit_file:recursive_delete([backup_dir()]), info("upgrades: Mnesia backup removed~n", []). +-spec maybe_upgrade_mnesia() -> 'ok'. + maybe_upgrade_mnesia() -> AllNodes = rabbit_mnesia:cluster_nodes(all), ok = rabbit_mnesia_rename:maybe_finish(AllNodes), @@ -180,25 +174,34 @@ upgrade_mode(AllNodes) -> end; [Another|_] -> MyVersion = rabbit_version:desired_for_scope(mnesia), - ErrFun = fun (ClusterVersion) -> - %% The other node(s) are running an - %% unexpected version. - die("Cluster upgrade needed but other nodes are " - "running ~p~nand I want ~p", - [ClusterVersion, MyVersion]) - end, case rpc:call(Another, rabbit_version, desired_for_scope, [mnesia]) of - {badrpc, {'EXIT', {undef, _}}} -> ErrFun(unknown_old_version); - {badrpc, Reason} -> ErrFun({unknown, Reason}); - CV -> case rabbit_version:matches( - MyVersion, CV) of - true -> secondary; - false -> ErrFun(CV) - end + {badrpc, {'EXIT', {undef, _}}} -> + die_because_cluster_upgrade_needed(unknown_old_version, + MyVersion); + {badrpc, Reason} -> + die_because_cluster_upgrade_needed({unknown, Reason}, + MyVersion); + CV -> case rabbit_version:matches( + MyVersion, CV) of + true -> secondary; + false -> die_because_cluster_upgrade_needed( + CV, MyVersion) + end end end. +-spec die_because_cluster_upgrade_needed(any(), any()) -> no_return(). + +die_because_cluster_upgrade_needed(ClusterVersion, MyVersion) -> + %% The other node(s) are running an + %% unexpected version. + die("Cluster upgrade needed but other nodes are " + "running ~p~nand I want ~p", + [ClusterVersion, MyVersion]). + +-spec die(string(), list()) -> no_return(). + die(Msg, Args) -> %% We don't throw or exit here since that gets thrown %% straight out into do_boot, generating an erl_crash.dump @@ -244,6 +247,11 @@ nodes_running(Nodes) -> %% ------------------------------------------------------------------- +-spec maybe_upgrade_local() -> + 'ok' | + 'version_not_available' | + 'starting_from_scratch'. + maybe_upgrade_local() -> case rabbit_version:upgrades_required(local) of {error, version_not_available} -> version_not_available; diff --git a/src/rabbit_upgrade_functions.erl b/src/rabbit_upgrade_functions.erl index 6be812dad3..afbcb863aa 100644 --- a/src/rabbit_upgrade_functions.erl +++ b/src/rabbit_upgrade_functions.erl @@ -60,60 +60,16 @@ -rabbit_upgrade({queue_vhost_field, mnesia, [operator_policies]}). -rabbit_upgrade({topic_permission, mnesia, []}). -rabbit_upgrade({queue_options, mnesia, [queue_vhost_field]}). --rabbit_upgrade({queue_type, mnesia, [queue_options]}). --rabbit_upgrade({queue_quorum_nodes, mnesia, [queue_type]}). -rabbit_upgrade({exchange_options, mnesia, [operator_policies]}). -%% TODO: move that to feature flags --rabbit_upgrade({remove_explicit_default_exchange_bindings, mnesia, [queue_state]}). - %% ------------------------------------------------------------------- --spec remove_user_scope() -> 'ok'. --spec hash_passwords() -> 'ok'. --spec add_ip_to_listener() -> 'ok'. --spec add_opts_to_listener() -> 'ok'. --spec internal_exchanges() -> 'ok'. --spec user_to_internal_user() -> 'ok'. --spec topic_trie() -> 'ok'. --spec semi_durable_route() -> 'ok'. --spec exchange_event_serial() -> 'ok'. --spec trace_exchanges() -> 'ok'. --spec user_admin_to_tags() -> 'ok'. --spec ha_mirrors() -> 'ok'. --spec gm() -> 'ok'. --spec exchange_scratch() -> 'ok'. --spec mirrored_supervisor() -> 'ok'. --spec topic_trie_node() -> 'ok'. --spec runtime_parameters() -> 'ok'. --spec policy() -> 'ok'. --spec sync_slave_pids() -> 'ok'. --spec no_mirror_nodes() -> 'ok'. --spec gm_pids() -> 'ok'. --spec exchange_decorators() -> 'ok'. --spec policy_apply_to() -> 'ok'. --spec queue_decorators() -> 'ok'. --spec internal_system_x() -> 'ok'. --spec cluster_name() -> 'ok'. --spec down_slave_nodes() -> 'ok'. --spec queue_state() -> 'ok'. --spec recoverable_slaves() -> 'ok'. --spec user_password_hashing() -> 'ok'. --spec vhost_limits() -> 'ok'. --spec operator_policies() -> 'ok'. --spec queue_vhost_field() -> 'ok'. --spec queue_options() -> 'ok'. --spec queue_type() -> 'ok'. --spec queue_quorum_nodes() -> 'ok'. --spec exchange_options() -> 'ok'. - --spec remove_explicit_default_exchange_bindings() -> 'ok'. - -%%-------------------------------------------------------------------- - %% replaces vhost.dummy (used to avoid having a single-field record %% which Mnesia doesn't like) with vhost.limits (which is actually %% used) + +-spec vhost_limits() -> 'ok'. + vhost_limits() -> transform( rabbit_vhost, @@ -128,6 +84,8 @@ vhost_limits() -> %% would be messy to have to go back and fix old transforms at that %% point. +-spec remove_user_scope() -> 'ok'. + remove_user_scope() -> transform( rabbit_user_permission, @@ -140,6 +98,9 @@ remove_user_scope() -> %% only relevant to those migrating from 2.1.1. %% all users created after in 3.6.0 or later will use SHA-256 (unless configured %% otherwise) + +-spec hash_passwords() -> 'ok'. + hash_passwords() -> transform( rabbit_user, @@ -149,6 +110,8 @@ hash_passwords() -> end, [username, password_hash, is_admin]). +-spec add_ip_to_listener() -> 'ok'. + add_ip_to_listener() -> transform( rabbit_listener, @@ -157,6 +120,8 @@ add_ip_to_listener() -> end, [node, protocol, host, ip_address, port]). +-spec add_opts_to_listener() -> 'ok'. + add_opts_to_listener() -> transform( rabbit_listener, @@ -165,6 +130,8 @@ add_opts_to_listener() -> end, [node, protocol, host, ip_address, port, opts]). +-spec internal_exchanges() -> 'ok'. + internal_exchanges() -> Tables = [rabbit_exchange, rabbit_durable_exchange], AddInternalFun = @@ -177,6 +144,8 @@ internal_exchanges() -> || T <- Tables ], ok. +-spec user_to_internal_user() -> 'ok'. + user_to_internal_user() -> transform( rabbit_user, @@ -185,6 +154,8 @@ user_to_internal_user() -> end, [username, password_hash, is_admin], internal_user). +-spec topic_trie() -> 'ok'. + topic_trie() -> create(rabbit_topic_trie_edge, [{record_name, topic_trie_edge}, {attributes, [trie_edge, node_id]}, @@ -193,20 +164,28 @@ topic_trie() -> {attributes, [trie_binding, value]}, {type, ordered_set}]). +-spec semi_durable_route() -> 'ok'. + semi_durable_route() -> create(rabbit_semi_durable_route, [{record_name, route}, {attributes, [binding, value]}]). +-spec exchange_event_serial() -> 'ok'. + exchange_event_serial() -> create(rabbit_exchange_serial, [{record_name, exchange_serial}, {attributes, [name, next]}]). +-spec trace_exchanges() -> 'ok'. + trace_exchanges() -> [declare_exchange( rabbit_misc:r(VHost, exchange, <<"amq.rabbitmq.trace">>), topic) || VHost <- rabbit_vhost:list()], ok. +-spec user_admin_to_tags() -> 'ok'. + user_admin_to_tags() -> transform( rabbit_user, @@ -217,6 +196,8 @@ user_admin_to_tags() -> end, [username, password_hash, tags], internal_user). +-spec ha_mirrors() -> 'ok'. + ha_mirrors() -> Tables = [rabbit_queue, rabbit_durable_queue], AddMirrorPidsFun = @@ -231,10 +212,14 @@ ha_mirrors() -> || T <- Tables ], ok. +-spec gm() -> 'ok'. + gm() -> create(gm_group, [{record_name, gm_group}, {attributes, [name, version, members]}]). +-spec exchange_scratch() -> 'ok'. + exchange_scratch() -> ok = exchange_scratch(rabbit_exchange), ok = exchange_scratch(rabbit_durable_exchange). @@ -247,17 +232,23 @@ exchange_scratch(Table) -> end, [name, type, durable, auto_delete, internal, arguments, scratch]). +-spec mirrored_supervisor() -> 'ok'. + mirrored_supervisor() -> create(mirrored_sup_childspec, [{record_name, mirrored_sup_childspec}, {attributes, [key, mirroring_pid, childspec]}]). +-spec topic_trie_node() -> 'ok'. + topic_trie_node() -> create(rabbit_topic_trie_node, [{record_name, topic_trie_node}, {attributes, [trie_node, edge_count, binding_count]}, {type, ordered_set}]). +-spec runtime_parameters() -> 'ok'. + runtime_parameters() -> create(rabbit_runtime_parameters, [{record_name, runtime_parameters}, @@ -281,6 +272,8 @@ exchange_scratches(Table) -> end, [name, type, durable, auto_delete, internal, arguments, scratches]). +-spec policy() -> 'ok'. + policy() -> ok = exchange_policy(rabbit_exchange), ok = exchange_policy(rabbit_durable_exchange), @@ -307,6 +300,8 @@ queue_policy(Table) -> [name, durable, auto_delete, exclusive_owner, arguments, pid, slave_pids, mirror_nodes, policy]). +-spec sync_slave_pids() -> 'ok'. + sync_slave_pids() -> Tables = [rabbit_queue, rabbit_durable_queue], AddSyncSlavesFun = @@ -319,6 +314,8 @@ sync_slave_pids() -> || T <- Tables], ok. +-spec no_mirror_nodes() -> 'ok'. + no_mirror_nodes() -> Tables = [rabbit_queue, rabbit_durable_queue], RemoveMirrorNodesFun = @@ -331,6 +328,8 @@ no_mirror_nodes() -> || T <- Tables], ok. +-spec gm_pids() -> 'ok'. + gm_pids() -> Tables = [rabbit_queue, rabbit_durable_queue], AddGMPidsFun = @@ -343,6 +342,8 @@ gm_pids() -> || T <- Tables], ok. +-spec exchange_decorators() -> 'ok'. + exchange_decorators() -> ok = exchange_decorators(rabbit_exchange), ok = exchange_decorators(rabbit_durable_exchange). @@ -358,6 +359,8 @@ exchange_decorators(Table) -> [name, type, durable, auto_delete, internal, arguments, scratches, policy, decorators]). +-spec policy_apply_to() -> 'ok'. + policy_apply_to() -> transform( rabbit_runtime_parameters, @@ -380,6 +383,8 @@ apply_to(Def) -> [_, _] -> <<"all">> end. +-spec queue_decorators() -> 'ok'. + queue_decorators() -> ok = queue_decorators(rabbit_queue), ok = queue_decorators(rabbit_durable_queue). @@ -395,6 +400,8 @@ queue_decorators(Table) -> [name, durable, auto_delete, exclusive_owner, arguments, pid, slave_pids, sync_slave_pids, policy, gm_pids, decorators]). +-spec internal_system_x() -> 'ok'. + internal_system_x() -> transform( rabbit_durable_exchange, @@ -408,6 +415,8 @@ internal_system_x() -> [name, type, durable, auto_delete, internal, arguments, scratches, policy, decorators]). +-spec cluster_name() -> 'ok'. + cluster_name() -> {atomic, ok} = mnesia:transaction(fun cluster_name_tx/0), ok. @@ -434,6 +443,8 @@ cluster_name_tx() -> [mnesia:delete(T, K, write) || K <- Ks], ok. +-spec down_slave_nodes() -> 'ok'. + down_slave_nodes() -> ok = down_slave_nodes(rabbit_queue), ok = down_slave_nodes(rabbit_durable_queue). @@ -449,6 +460,8 @@ down_slave_nodes(Table) -> [name, durable, auto_delete, exclusive_owner, arguments, pid, slave_pids, sync_slave_pids, down_slave_nodes, policy, gm_pids, decorators]). +-spec queue_state() -> 'ok'. + queue_state() -> ok = queue_state(rabbit_queue), ok = queue_state(rabbit_durable_queue). @@ -465,6 +478,8 @@ queue_state(Table) -> [name, durable, auto_delete, exclusive_owner, arguments, pid, slave_pids, sync_slave_pids, down_slave_nodes, policy, gm_pids, decorators, state]). +-spec recoverable_slaves() -> 'ok'. + recoverable_slaves() -> ok = recoverable_slaves(rabbit_queue), ok = recoverable_slaves(rabbit_durable_queue). @@ -512,6 +527,8 @@ slave_pids_pending_shutdown(Table) -> sync_slave_pids, recoverable_slaves, policy, gm_pids, decorators, state, policy_version, slave_pids_pending_shutdown]). +-spec operator_policies() -> 'ok'. + operator_policies() -> ok = exchange_operator_policies(rabbit_exchange), ok = exchange_operator_policies(rabbit_durable_exchange), @@ -543,6 +560,7 @@ queue_operator_policies(Table) -> sync_slave_pids, recoverable_slaves, policy, operator_policy, gm_pids, decorators, state, policy_version, slave_pids_pending_shutdown]). +-spec queue_vhost_field() -> 'ok'. queue_vhost_field() -> ok = queue_vhost_field(rabbit_queue), @@ -565,6 +583,8 @@ queue_vhost_field(Table) -> sync_slave_pids, recoverable_slaves, policy, operator_policy, gm_pids, decorators, state, policy_version, slave_pids_pending_shutdown, vhost]). +-spec queue_options() -> 'ok'. + queue_options() -> ok = queue_options(rabbit_queue), ok = queue_options(rabbit_durable_queue), @@ -584,51 +604,13 @@ queue_options(Table) -> sync_slave_pids, recoverable_slaves, policy, operator_policy, gm_pids, decorators, state, policy_version, slave_pids_pending_shutdown, vhost, options]). -queue_type() -> - ok = queue_type(rabbit_queue), - ok = queue_type(rabbit_durable_queue), - ok. - -queue_type(Table) -> - transform( - Table, - fun ({amqqueue, Name, Durable, AutoDelete, ExclusiveOwner, Arguments, - Pid, SlavePids, SyncSlavePids, DSN, Policy, OperatorPolicy, GmPids, Decorators, - State, PolicyVersion, SlavePidsPendingShutdown, VHost, Options}) -> - {amqqueue, Name, Durable, AutoDelete, ExclusiveOwner, Arguments, - Pid, SlavePids, SyncSlavePids, DSN, Policy, OperatorPolicy, GmPids, Decorators, - State, PolicyVersion, SlavePidsPendingShutdown, VHost, Options, classic} - end, - [name, durable, auto_delete, exclusive_owner, arguments, pid, slave_pids, - sync_slave_pids, recoverable_slaves, policy, operator_policy, - gm_pids, decorators, state, policy_version, slave_pids_pending_shutdown, vhost, options, - type]). - -queue_quorum_nodes() -> - ok = queue_quorum_nodes(rabbit_queue), - ok = queue_quorum_nodes(rabbit_durable_queue), - ok. - -queue_quorum_nodes(Table) -> - transform( - Table, - fun ({amqqueue, Name, Durable, AutoDelete, ExclusiveOwner, Arguments, - Pid, SlavePids, SyncSlavePids, DSN, Policy, OperatorPolicy, GmPids, Decorators, - State, PolicyVersion, SlavePidsPendingShutdown, VHost, Options, Type}) -> - {amqqueue, Name, Durable, AutoDelete, ExclusiveOwner, Arguments, - Pid, SlavePids, SyncSlavePids, DSN, Policy, OperatorPolicy, GmPids, Decorators, - State, PolicyVersion, SlavePidsPendingShutdown, VHost, Options, Type, - undefined} - end, - [name, durable, auto_delete, exclusive_owner, arguments, pid, slave_pids, - sync_slave_pids, recoverable_slaves, policy, operator_policy, - gm_pids, decorators, state, policy_version, slave_pids_pending_shutdown, vhost, options, - type, quorum_nodes]). - %% Prior to 3.6.0, passwords were hashed using MD5, this populates %% existing records with said default. Users created with 3.6.0+ will %% have internal_user.hashing_algorithm populated by the internal %% authn backend. + +-spec user_password_hashing() -> 'ok'. + user_password_hashing() -> transform( rabbit_user, @@ -643,6 +625,8 @@ topic_permission() -> {attributes, [topic_permission_key, permission]}, {disc_copies, [node()]}]). +-spec exchange_options() -> 'ok'. + exchange_options() -> ok = exchange_options(rabbit_exchange), ok = exchange_options(rabbit_durable_exchange). @@ -658,24 +642,6 @@ exchange_options(Table) -> [name, type, durable, auto_delete, internal, arguments, scratches, policy, operator_policy, decorators, options]). -remove_explicit_default_exchange_bindings() -> - Tab = rabbit_durable_queue, - rabbit_table:wait([Tab]), - %% Default exchange bindings are now implicit - %% (not stored in the route tables). - %% It should be safe to remove them outside of a - %% transaction. - Queues = mnesia:dirty_all_keys(Tab), - N = length(Queues), - case N of - 0 -> ok; - _ -> - error_logger:info_msg("Will delete explicit default exchange bindings for ~p queues. " - "This can take some time...", [N]), - [rabbit_binding:remove_default_exchange_binding_rows_of(Q) || Q <- Queues] - end, - ok. - %%-------------------------------------------------------------------- transform(TableName, Fun, FieldList) -> diff --git a/src/rabbit_variable_queue.erl b/src/rabbit_variable_queue.erl index 4da073a518..8b773f2cc2 100644 --- a/src/rabbit_variable_queue.erl +++ b/src/rabbit_variable_queue.erl @@ -356,8 +356,9 @@ -define(QUEUE, lqueue). --include("rabbit.hrl"). --include("rabbit_framing.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit_framing.hrl"). +-include("amqqueue.hrl"). %%---------------------------------------------------------------------------- @@ -427,10 +428,6 @@ io_batch_size :: pos_integer(), mode :: 'default' | 'lazy', memory_reduction_run_count :: non_neg_integer()}. -%% Duplicated from rabbit_backing_queue --spec ack([ack()], state()) -> {[rabbit_guid:guid()], state()}. - --spec multiple_routing_keys() -> 'ok'. -define(BLANK_DELTA, #delta { start_seq_id = undefined, count = 0, @@ -532,8 +529,9 @@ init(Queue, Recover, Callback) -> fun (MsgIds) -> msg_indices_written_to_disk(Callback, MsgIds) end, fun (MsgIds) -> msgs_and_indices_written_to_disk(Callback, MsgIds) end). -init(#amqqueue { name = QueueName, durable = IsDurable }, new, - AsyncCallback, MsgOnDiskFun, MsgIdxOnDiskFun, MsgAndIdxOnDiskFun) -> +init(Q, new, AsyncCallback, MsgOnDiskFun, MsgIdxOnDiskFun, MsgAndIdxOnDiskFun) when ?is_amqqueue(Q) -> + QueueName = amqqueue:get_name(Q), + IsDurable = amqqueue:is_durable(Q), IndexState = rabbit_queue_index:init(QueueName, MsgIdxOnDiskFun, MsgAndIdxOnDiskFun), VHost = QueueName#resource.virtual_host, @@ -547,8 +545,9 @@ init(#amqqueue { name = QueueName, durable = IsDurable }, new, AsyncCallback, VHost), VHost); %% We can be recovering a transient queue if it crashed -init(#amqqueue { name = QueueName, durable = IsDurable }, Terms, - AsyncCallback, MsgOnDiskFun, MsgIdxOnDiskFun, MsgAndIdxOnDiskFun) -> +init(Q, Terms, AsyncCallback, MsgOnDiskFun, MsgIdxOnDiskFun, MsgAndIdxOnDiskFun) when ?is_amqqueue(Q) -> + QueueName = amqqueue:get_name(Q), + IsDurable = amqqueue:is_durable(Q), {PRef, RecoveryTerms} = process_recovery_terms(Terms), VHost = QueueName#resource.virtual_host, {PersistentClient, ContainsCheckFun} = @@ -620,7 +619,8 @@ delete_and_terminate(_Reason, State) -> rabbit_msg_store:client_delete_and_terminate(MSCStateT), a(State2 #vqstate { msg_store_clients = undefined }). -delete_crashed(#amqqueue{name = QName}) -> +delete_crashed(Q) when ?is_amqqueue(Q) -> + QName = amqqueue:get_name(Q), ok = rabbit_queue_index:erase(QName). purge(State = #vqstate { len = Len }) -> @@ -700,6 +700,9 @@ drop(AckRequired, State) -> {{MsgStatus#msg_status.msg_id, AckTag}, a(State2)} end. +%% Duplicated from rabbit_backing_queue +-spec ack([ack()], state()) -> {[rabbit_guid:guid()], state()}. + ack([], State) -> {[], State}; %% optimisation: this head is essentially a partial evaluation of the @@ -2788,6 +2791,8 @@ ui(#vqstate{index_state = IndexState, %% Upgrading %%---------------------------------------------------------------------------- +-spec multiple_routing_keys() -> 'ok'. + multiple_routing_keys() -> transform_storage( fun ({basic_message, ExchangeName, Routing_Key, Content, @@ -2825,7 +2830,7 @@ move_messages_to_vhost_store(Queues) -> %% Move the queue index for each persistent queue to the new store lists:foreach( fun(Queue) -> - #amqqueue{name = QueueName} = Queue, + QueueName = amqqueue:get_name(Queue), rabbit_queue_index:move_to_per_vhost_stores(QueueName) end, Queues), @@ -2938,17 +2943,16 @@ list_persistent_queues() -> Node = node(), mnesia:async_dirty( fun () -> - qlc:e(qlc:q([Q || Q = #amqqueue{name = Name, - pid = Pid} - <- mnesia:table(rabbit_durable_queue), - node(Pid) == Node, - mnesia:read(rabbit_queue, Name, read) =:= []])) + qlc:e(qlc:q([Q || Q <- mnesia:table(rabbit_durable_queue), + ?amqqueue_is_classic(Q), + amqqueue:qnode(Q) == Node, + mnesia:read(rabbit_queue, amqqueue:get_name(Q), read) =:= []])) end). read_old_recovery_terms([]) -> {[], [], ?EMPTY_START_FUN_STATE}; read_old_recovery_terms(Queues) -> - QueueNames = [Name || #amqqueue{name = Name} <- Queues], + QueueNames = [amqqueue:get_name(Q) || Q <- Queues], {AllTerms, StartFunState} = rabbit_queue_index:read_global_recovery_terms(QueueNames), Refs = [Ref || Terms <- AllTerms, Terms /= non_clean_shutdown, diff --git a/src/rabbit_version.erl b/src/rabbit_version.erl index 5b1722fdbf..4a79ef9938 100644 --- a/src/rabbit_version.erl +++ b/src/rabbit_version.erl @@ -33,23 +33,6 @@ -type version() :: [atom()]. --spec recorded() -> rabbit_types:ok_or_error2(version(), any()). --spec matches([A], [A]) -> boolean(). --spec desired() -> version(). --spec desired_for_scope(scope()) -> scope_version(). --spec record_desired() -> 'ok'. --spec record_desired_for_scope - (scope()) -> rabbit_types:ok_or_error(any()). --spec upgrades_required - (scope()) -> rabbit_types:ok_or_error2([step()], any()). --spec check_version_consistency - (string(), string(), string()) -> rabbit_types:ok_or_error(any()). --spec check_version_consistency - (string(), string(), string(), string()) -> - rabbit_types:ok_or_error(any()). --spec check_otp_consistency - (string()) -> rabbit_types:ok_or_error(any()). - %% ------------------------------------------------------------------- -define(VERSION_FILENAME, "schema_version"). @@ -57,6 +40,8 @@ %% ------------------------------------------------------------------- +-spec recorded() -> rabbit_types:ok_or_error2(version(), any()). + recorded() -> case rabbit_file:read_term_file(schema_filename()) of {ok, [V]} -> {ok, V}; {error, _} = Err -> Err @@ -87,20 +72,34 @@ record_for_scope(Scope, ScopeVersion) -> %% ------------------------------------------------------------------- +-spec matches([A], [A]) -> boolean(). + matches(VerA, VerB) -> lists:usort(VerA) =:= lists:usort(VerB). %% ------------------------------------------------------------------- +-spec desired() -> version(). + desired() -> [Name || Scope <- ?SCOPES, Name <- desired_for_scope(Scope)]. +-spec desired_for_scope(scope()) -> scope_version(). + desired_for_scope(Scope) -> with_upgrade_graph(fun heads/1, Scope). +-spec record_desired() -> 'ok'. + record_desired() -> record(desired()). +-spec record_desired_for_scope + (scope()) -> rabbit_types:ok_or_error(any()). + record_desired_for_scope(Scope) -> record_for_scope(Scope, desired_for_scope(Scope)). +-spec upgrades_required + (scope()) -> rabbit_types:ok_or_error2([step()], any()). + upgrades_required(Scope) -> case recorded_for_scope(Scope) of {error, enoent} -> @@ -208,9 +207,17 @@ schema_filename() -> filename:join(dir(), ?VERSION_FILENAME). %% -------------------------------------------------------------------- +-spec check_version_consistency + (string(), string(), string()) -> rabbit_types:ok_or_error(any()). + check_version_consistency(This, Remote, Name) -> check_version_consistency(This, Remote, Name, fun (A, B) -> A =:= B end). +-spec check_version_consistency + (string(), string(), string(), + fun((string(), string()) -> boolean())) -> + rabbit_types:ok_or_error(any()). + check_version_consistency(This, Remote, Name, Comp) -> case Comp(This, Remote) of true -> ok; @@ -222,5 +229,8 @@ version_error(Name, This, Remote) -> rabbit_misc:format("~s version mismatch: local node is ~s, " "remote node ~s", [Name, This, Remote])}}. +-spec check_otp_consistency + (string()) -> rabbit_types:ok_or_error(any()). + check_otp_consistency(Remote) -> check_version_consistency(rabbit_misc:otp_release(), Remote, "OTP"). diff --git a/src/rabbit_vhost.erl b/src/rabbit_vhost.erl index cf12d04ce8..9180f9ca0a 100644 --- a/src/rabbit_vhost.erl +++ b/src/rabbit_vhost.erl @@ -16,7 +16,7 @@ -module(rabbit_vhost). --include("rabbit.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). %%---------------------------------------------------------------------------- @@ -28,24 +28,6 @@ -export([delete_storage/1]). -export([vhost_down/1]). --spec add(rabbit_types:vhost(), rabbit_types:username()) -> rabbit_types:ok_or_error(any()). --spec delete(rabbit_types:vhost(), rabbit_types:username()) -> rabbit_types:ok_or_error(any()). --spec update(rabbit_types:vhost(), rabbit_misc:thunk(A)) -> A. --spec exists(rabbit_types:vhost()) -> boolean(). --spec list() -> [rabbit_types:vhost()]. --spec with(rabbit_types:vhost(), rabbit_misc:thunk(A)) -> A. --spec with_user_and_vhost - (rabbit_types:username(), rabbit_types:vhost(), rabbit_misc:thunk(A)) -> A. --spec assert(rabbit_types:vhost()) -> 'ok'. - --spec info(rabbit_types:vhost()) -> rabbit_types:infos(). --spec info(rabbit_types:vhost(), rabbit_types:info_keys()) - -> rabbit_types:infos(). --spec info_all() -> [rabbit_types:infos()]. --spec info_all(rabbit_types:info_keys()) -> [rabbit_types:infos()]. --spec info_all(rabbit_types:info_keys(), reference(), pid()) -> - 'ok'. - recover() -> %% Clear out remnants of old incarnation, in case we restarted %% faster than other nodes handled DOWN messages from us. @@ -72,8 +54,8 @@ recover(VHost) -> ok = rabbit_file:ensure_dir(VHostStubFile), ok = file:write_file(VHostStubFile, VHost), Qs = rabbit_amqqueue:recover(VHost), - ok = rabbit_binding:recover(rabbit_exchange:recover(VHost), - [QName || #amqqueue{name = QName} <- Qs]), + QNames = [amqqueue:get_name(Q) || Q <- Qs], + ok = rabbit_binding:recover(rabbit_exchange:recover(VHost), QNames), ok = rabbit_amqqueue:start(Qs), %% Start queue mirrors. ok = rabbit_mirror_queue_misc:on_vhost_up(VHost), @@ -83,6 +65,8 @@ recover(VHost) -> -define(INFO_KEYS, [name, tracing, cluster_state]). +-spec add(rabbit_types:vhost(), rabbit_types:username()) -> rabbit_types:ok_or_error(any()). + add(VHost, ActingUser) -> case exists(VHost) of true -> ok; @@ -124,16 +108,14 @@ do_add(VHostPath, ActingUser) -> rabbit_event:notify(vhost_created, info(VHostPath) ++ [{user_who_performed_action, ActingUser}]), R; - {error, {no_such_vhost, VHostPath}} -> - Msg = rabbit_misc:format("failed to set up vhost '~s': it was concurrently deleted!", - [VHostPath]), - {error, Msg}; {error, Reason} -> Msg = rabbit_misc:format("failed to set up vhost '~s': ~p", [VHostPath, Reason]), {error, Msg} end. +-spec delete(rabbit_types:vhost(), rabbit_types:username()) -> rabbit_types:ok_or_error(any()). + delete(VHostPath, ActingUser) -> %% FIXME: We are forced to delete the queues and exchanges outside %% the TX below. Queue deletion involves sending messages to the queue @@ -142,8 +124,10 @@ delete(VHostPath, ActingUser) -> %% notifications which must be sent outside the TX rabbit_log:info("Deleting vhost '~s'~n", [VHostPath]), QDelFun = fun (Q) -> rabbit_amqqueue:delete(Q, false, false, ActingUser) end, - [assert_benign(rabbit_amqqueue:with(Name, QDelFun), ActingUser) || - #amqqueue{name = Name} <- rabbit_amqqueue:list(VHostPath)], + [begin + Name = amqqueue:get_name(Q), + assert_benign(rabbit_amqqueue:with(Name, QDelFun), ActingUser) + end || Q <- rabbit_amqqueue:list(VHostPath)], [assert_benign(rabbit_exchange:delete(Name, false, ActingUser), ActingUser) || #exchange{name = Name} <- rabbit_exchange:list(VHostPath)], Funs = rabbit_misc:execute_mnesia_transaction( @@ -226,10 +210,8 @@ assert_benign({error, not_found}, _) -> ok; assert_benign({error, {absent, Q, _}}, ActingUser) -> %% Removing the mnesia entries here is safe. If/when the down node %% restarts, it will clear out the on-disk storage of the queue. - case rabbit_amqqueue:internal_delete(Q#amqqueue.name, ActingUser) of - ok -> ok; - {error, not_found} -> ok - end. + QName = amqqueue:get_name(Q), + rabbit_amqqueue:internal_delete(QName, ActingUser). internal_delete(VHostPath, ActingUser) -> [ok = rabbit_auth_backend_internal:clear_permissions( @@ -249,12 +231,18 @@ internal_delete(VHostPath, ActingUser) -> ok = mnesia:delete({rabbit_vhost, VHostPath}), Fs1 ++ Fs2. +-spec exists(rabbit_types:vhost()) -> boolean(). + exists(VHostPath) -> mnesia:dirty_read({rabbit_vhost, VHostPath}) /= []. +-spec list() -> [rabbit_types:vhost()]. + list() -> mnesia:dirty_all_keys(rabbit_vhost). +-spec with(rabbit_types:vhost(), rabbit_misc:thunk(A)) -> A. + with(VHostPath, Thunk) -> fun () -> case mnesia:read({rabbit_vhost, VHostPath}) of @@ -265,15 +253,23 @@ with(VHostPath, Thunk) -> end end. +-spec with_user_and_vhost + (rabbit_types:username(), rabbit_types:vhost(), rabbit_misc:thunk(A)) -> A. + with_user_and_vhost(Username, VHostPath, Thunk) -> rabbit_misc:with_user(Username, with(VHostPath, Thunk)). %% Like with/2 but outside an Mnesia tx + +-spec assert(rabbit_types:vhost()) -> 'ok'. + assert(VHostPath) -> case exists(VHostPath) of true -> ok; false -> throw({error, {no_such_vhost, VHostPath}}) end. +-spec update(rabbit_types:vhost(), fun((#vhost{}) -> #vhost{})) -> #vhost{}. + update(VHostPath, Fun) -> case mnesia:read({rabbit_vhost, VHostPath}) of [] -> @@ -326,13 +322,27 @@ i(tracing, VHost) -> rabbit_trace:enabled(VHost); i(cluster_state, VHost) -> vhost_cluster_state(VHost); i(Item, _) -> throw({bad_argument, Item}). +-spec info(rabbit_types:vhost()) -> rabbit_types:infos(). + info(VHost) -> infos(?INFO_KEYS, VHost). + +-spec info(rabbit_types:vhost(), rabbit_types:info_keys()) + -> rabbit_types:infos(). + info(VHost, Items) -> infos(Items, VHost). +-spec info_all() -> [rabbit_types:infos()]. + info_all() -> info_all(?INFO_KEYS). + +-spec info_all(rabbit_types:info_keys()) -> [rabbit_types:infos()]. + info_all(Items) -> [info(VHost, Items) || VHost <- list()]. info_all(Ref, AggregatorPid) -> info_all(?INFO_KEYS, Ref, AggregatorPid). + +-spec info_all(rabbit_types:info_keys(), reference(), pid()) -> + 'ok'. info_all(Items, Ref, AggregatorPid) -> rabbit_control_misc:emitting_map( AggregatorPid, Ref, fun(VHost) -> info(VHost, Items) end, list()). diff --git a/src/rabbit_vm.erl b/src/rabbit_vm.erl index 1b11017076..e78aca0a54 100644 --- a/src/rabbit_vm.erl +++ b/src/rabbit_vm.erl @@ -23,12 +23,6 @@ %%---------------------------------------------------------------------------- -spec memory() -> rabbit_types:infos(). --spec binary() -> rabbit_types:infos(). --spec ets_tables_memory(Owners) -> rabbit_types:infos() - when Owners :: all | OwnerProcessName | [OwnerProcessName], - OwnerProcessName :: atom(). - -%%---------------------------------------------------------------------------- memory() -> All = interesting_sups(), @@ -115,6 +109,8 @@ memory() -> %% claims about negative memory. See %% http://erlang.org/pipermail/erlang-questions/2012-September/069320.html +-spec binary() -> rabbit_types:infos(). + binary() -> All = interesting_sups(), {Sums, Rest} = @@ -153,6 +149,10 @@ mnesia_memory() -> ets_memory(Owners) -> lists:sum([V || {_K, V} <- ets_tables_memory(Owners)]). +-spec ets_tables_memory(Owners) -> rabbit_types:infos() + when Owners :: all | OwnerProcessName | [OwnerProcessName], + OwnerProcessName :: atom(). + ets_tables_memory(all) -> [{ets:info(T, name), bytes(ets:info(T, memory))} || T <- ets:all(), diff --git a/src/supervised_lifecycle.erl b/src/supervised_lifecycle.erl index 82f6728f17..4be7922125 100644 --- a/src/supervised_lifecycle.erl +++ b/src/supervised_lifecycle.erl @@ -39,8 +39,6 @@ -spec start_link(atom(), rabbit_types:mfargs(), rabbit_types:mfargs()) -> rabbit_types:ok_pid_or_error(). -%%---------------------------------------------------------------------------- - start_link(Name, StartMFA, StopMFA) -> gen_server:start_link({local, Name}, ?MODULE, [StartMFA, StopMFA], []). diff --git a/src/tcp_listener.erl b/src/tcp_listener.erl index d6c615e3b6..d5fb900198 100644 --- a/src/tcp_listener.erl +++ b/src/tcp_listener.erl @@ -64,8 +64,6 @@ mfargs(), mfargs(), string()) -> rabbit_types:ok_pid_or_error(). -%%-------------------------------------------------------------------- - start_link(IPAddress, Port, OnStartup, OnShutdown, Label) -> gen_server:start_link( diff --git a/src/tcp_listener_sup.erl b/src/tcp_listener_sup.erl index 074a4e4d6a..a31a60e505 100644 --- a/src/tcp_listener_sup.erl +++ b/src/tcp_listener_sup.erl @@ -38,8 +38,6 @@ module(), any(), mfargs(), mfargs(), integer(), string()) -> rabbit_types:ok_pid_or_error(). -%%---------------------------------------------------------------------------- - start_link(IPAddress, Port, Transport, SocketOpts, ProtoSup, ProtoOpts, OnStartup, OnShutdown, ConcurrentAcceptorCount, Label) -> supervisor:start_link( diff --git a/test/amqqueue_backward_compatibility_SUITE.erl b/test/amqqueue_backward_compatibility_SUITE.erl new file mode 100644 index 0000000000..05a049c9bb --- /dev/null +++ b/test/amqqueue_backward_compatibility_SUITE.erl @@ -0,0 +1,302 @@ +-module(amqqueue_backward_compatibility_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-include("amqqueue.hrl"). + +-export([all/0, + groups/0, + init_per_suite/2, + end_per_suite/2, + init_per_group/2, + end_per_group/2, + init_per_testcase/2, + end_per_testcase/2, + + new_amqqueue_v1_is_amqqueue/1, + new_amqqueue_v2_is_amqqueue/1, + random_term_is_not_amqqueue/1, + + amqqueue_v1_is_durable/1, + amqqueue_v2_is_durable/1, + random_term_is_not_durable/1, + + amqqueue_v1_state_matching/1, + amqqueue_v2_state_matching/1, + random_term_state_matching/1, + + amqqueue_v1_type_matching/1, + amqqueue_v2_type_matching/1, + random_term_type_matching/1, + + upgrade_v1_to_v2/1 + ]). + +-define(long_tuple, {random_tuple, a, b, c, d, e, f, g, h, i, j, k, l, m, + n, o, p, q, r, s, t, u, v, w, x, y, z}). + +all() -> + [ + {group, parallel_tests} + ]. + +groups() -> + [ + {parallel_tests, [parallel], [new_amqqueue_v1_is_amqqueue, + new_amqqueue_v2_is_amqqueue, + random_term_is_not_amqqueue, + amqqueue_v1_is_durable, + amqqueue_v2_is_durable, + random_term_is_not_durable, + amqqueue_v1_state_matching, + amqqueue_v2_state_matching, + random_term_state_matching, + amqqueue_v1_type_matching, + amqqueue_v2_type_matching, + random_term_type_matching]} + ]. + +init_per_suite(_, Config) -> Config. +end_per_suite(_, Config) -> Config. + +init_per_group(_, Config) -> Config. +end_per_group(_, Config) -> Config. + +init_per_testcase(_, Config) -> Config. +end_per_testcase(_, Config) -> Config. + +new_amqqueue_v1_is_amqqueue(_) -> + VHost = <<"/">>, + Name = rabbit_misc:r(VHost, queue, my_amqqueue_v1), + Queue = amqqueue:new_with_version(amqqueue_v1, + Name, + self(), + false, + false, + none, + [], + VHost, + #{}, + ?amqqueue_v1_type), + ?assert(?is_amqqueue(Queue)), + ?assert(?is_amqqueue_v1(Queue)), + ?assert(not ?is_amqqueue_v2(Queue)), + ?assert(?amqqueue_is_classic(Queue)), + ?assert(amqqueue:is_classic(Queue)), + ?assert(not ?amqqueue_is_quorum(Queue)), + ?assert(not ?amqqueue_vhost_equals(Queue, <<"frazzle">>)), + ?assert(?amqqueue_has_valid_pid(Queue)), + ?assert(?amqqueue_pid_equals(Queue, self())), + ?assert(?amqqueue_pids_are_equal(Queue, Queue)), + ?assert(?amqqueue_pid_runs_on_local_node(Queue)), + ?assert(amqqueue:qnode(Queue) == node()). + +new_amqqueue_v2_is_amqqueue(_) -> + VHost = <<"/">>, + Name = rabbit_misc:r(VHost, queue, my_amqqueue_v2), + Queue = amqqueue:new_with_version(amqqueue_v2, + Name, + self(), + false, + false, + none, + [], + VHost, + #{}, + classic), + ?assert(?is_amqqueue(Queue)), + ?assert(?is_amqqueue_v2(Queue)), + ?assert(not ?is_amqqueue_v1(Queue)), + ?assert(?amqqueue_is_classic(Queue)), + ?assert(amqqueue:is_classic(Queue)), + ?assert(not ?amqqueue_is_quorum(Queue)), + ?assert(not ?amqqueue_vhost_equals(Queue, <<"frazzle">>)), + ?assert(?amqqueue_has_valid_pid(Queue)), + ?assert(?amqqueue_pid_equals(Queue, self())), + ?assert(?amqqueue_pids_are_equal(Queue, Queue)), + ?assert(?amqqueue_pid_runs_on_local_node(Queue)), + ?assert(amqqueue:qnode(Queue) == node()). + +random_term_is_not_amqqueue(_) -> + Term = ?long_tuple, + ?assert(not ?is_amqqueue(Term)), + ?assert(not ?is_amqqueue_v2(Term)), + ?assert(not ?is_amqqueue_v1(Term)). + +%% ------------------------------------------------------------------- + +amqqueue_v1_is_durable(_) -> + VHost = <<"/">>, + Name = rabbit_misc:r(VHost, queue, my_amqqueue_v1), + TransientQueue = amqqueue:new_with_version(amqqueue_v1, + Name, + self(), + false, + false, + none, + [], + VHost, + #{}, + ?amqqueue_v1_type), + DurableQueue = amqqueue:new_with_version(amqqueue_v1, + Name, + self(), + true, + false, + none, + [], + VHost, + #{}, + ?amqqueue_v1_type), + ?assert(not ?amqqueue_is_durable(TransientQueue)), + ?assert(?amqqueue_is_durable(DurableQueue)). + +amqqueue_v2_is_durable(_) -> + VHost = <<"/">>, + Name = rabbit_misc:r(VHost, queue, my_amqqueue_v1), + TransientQueue = amqqueue:new_with_version(amqqueue_v2, + Name, + self(), + false, + false, + none, + [], + VHost, + #{}, + classic), + DurableQueue = amqqueue:new_with_version(amqqueue_v2, + Name, + self(), + true, + false, + none, + [], + VHost, + #{}, + classic), + ?assert(not ?amqqueue_is_durable(TransientQueue)), + ?assert(?amqqueue_is_durable(DurableQueue)). + +random_term_is_not_durable(_) -> + Term = ?long_tuple, + ?assert(not ?amqqueue_is_durable(Term)). + +%% ------------------------------------------------------------------- + +amqqueue_v1_state_matching(_) -> + VHost = <<"/">>, + Name = rabbit_misc:r(VHost, queue, my_amqqueue_v1), + Queue1 = amqqueue:new_with_version(amqqueue_v1, + Name, + self(), + true, + false, + none, + [], + VHost, + #{}, + ?amqqueue_v1_type), + ?assert(?amqqueue_state_is(Queue1, live)), + Queue2 = amqqueue:set_state(Queue1, stopped), + ?assert(?amqqueue_state_is(Queue2, stopped)). + +amqqueue_v2_state_matching(_) -> + VHost = <<"/">>, + Name = rabbit_misc:r(VHost, queue, my_amqqueue_v1), + Queue1 = amqqueue:new_with_version(amqqueue_v2, + Name, + self(), + true, + false, + none, + [], + VHost, + #{}, + classic), + ?assert(?amqqueue_state_is(Queue1, live)), + Queue2 = amqqueue:set_state(Queue1, stopped), + ?assert(?amqqueue_state_is(Queue2, stopped)). + +random_term_state_matching(_) -> + Term = ?long_tuple, + ?assert(not ?amqqueue_state_is(Term, live)). + +%% ------------------------------------------------------------------- + +amqqueue_v1_type_matching(_) -> + VHost = <<"/">>, + Name = rabbit_misc:r(VHost, queue, my_amqqueue_v1), + Queue = amqqueue:new_with_version(amqqueue_v1, + Name, + self(), + true, + false, + none, + [], + VHost, + #{}, + ?amqqueue_v1_type), + ?assert(?amqqueue_is_classic(Queue)), + ?assert(amqqueue:is_classic(Queue)), + ?assert(not ?amqqueue_is_quorum(Queue)). + +amqqueue_v2_type_matching(_) -> + VHost = <<"/">>, + Name = rabbit_misc:r(VHost, queue, my_amqqueue_v1), + ClassicQueue = amqqueue:new_with_version(amqqueue_v2, + Name, + self(), + true, + false, + none, + [], + VHost, + #{}, + classic), + ?assert(?amqqueue_is_classic(ClassicQueue)), + ?assert(amqqueue:is_classic(ClassicQueue)), + ?assert(not ?amqqueue_is_quorum(ClassicQueue)), + ?assert(not amqqueue:is_quorum(ClassicQueue)), + QuorumQueue = amqqueue:new_with_version(amqqueue_v2, + Name, + self(), + true, + false, + none, + [], + VHost, + #{}, + quorum), + ?assert(not ?amqqueue_is_classic(QuorumQueue)), + ?assert(not amqqueue:is_classic(QuorumQueue)), + ?assert(?amqqueue_is_quorum(QuorumQueue)), + ?assert(amqqueue:is_quorum(QuorumQueue)). + +random_term_type_matching(_) -> + Term = ?long_tuple, + ?assert(not ?amqqueue_is_classic(Term)), + ?assert(not ?amqqueue_is_quorum(Term)), + ?assertException(error, function_clause, amqqueue:is_classic(Term)), + ?assertException(error, function_clause, amqqueue:is_quorum(Term)). + +%% ------------------------------------------------------------------- + +upgrade_v1_to_v2(_) -> + VHost = <<"/">>, + Name = rabbit_misc:r(VHost, queue, my_amqqueue_v1), + OldQueue = amqqueue:new_with_version(amqqueue_v1, + Name, + self(), + true, + false, + none, + [], + VHost, + #{}, + ?amqqueue_v1_type), + ?assert(?is_amqqueue_v1(OldQueue)), + ?assert(not ?is_amqqueue_v2(OldQueue)), + NewQueue = amqqueue:upgrade_to(amqqueue_v2, OldQueue), + ?assert(not ?is_amqqueue_v1(NewQueue)), + ?assert(?is_amqqueue_v2(NewQueue)). diff --git a/test/backing_queue_SUITE.erl b/test/backing_queue_SUITE.erl index 433bc66bff..c3f87cce59 100644 --- a/test/backing_queue_SUITE.erl +++ b/test/backing_queue_SUITE.erl @@ -18,6 +18,7 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("amqp_client/include/amqp_client.hrl"). +-include("amqqueue.hrl"). -compile(export_all). @@ -686,11 +687,10 @@ bq_variable_queue_delete_msg_store_files_callback(Config) -> bq_variable_queue_delete_msg_store_files_callback1(Config) -> ok = restart_msg_store_empty(), - {new, #amqqueue { pid = QPid, name = QName } = Q} = - rabbit_amqqueue:declare( - queue_name(Config, - <<"bq_variable_queue_delete_msg_store_files_callback-q">>), - true, false, [], none, <<"acting-user">>), + QName0 = queue_name(Config, <<"bq_variable_queue_delete_msg_store_files_callback-q">>), + {new, Q} = rabbit_amqqueue:declare(QName0, true, false, [], none, <<"acting-user">>), + QName = amqqueue:get_name(Q), + QPid = amqqueue:get_pid(Q), Payload = <<0:8388608>>, %% 1MB Count = 30, publish_and_confirm(Q, Payload, Count), @@ -718,9 +718,10 @@ bq_queue_recover(Config) -> bq_queue_recover1(Config) -> Count = 2 * rabbit_queue_index:next_segment_boundary(0), - {new, #amqqueue { pid = QPid, name = QName } = Q} = - rabbit_amqqueue:declare(queue_name(Config, <<"bq_queue_recover-q">>), - true, false, [], none, <<"acting-user">>), + QName0 = queue_name(Config, <<"bq_queue_recover-q">>), + {new, Q} = rabbit_amqqueue:declare(QName0, true, false, [], none, <<"acting-user">>), + QName = amqqueue:get_name(Q), + QPid = amqqueue:get_pid(Q), publish_and_confirm(Q, <<>>, Count), SupPid = get_queue_sup_pid(Q), @@ -736,7 +737,8 @@ bq_queue_recover1(Config) -> {ok, Limiter} = rabbit_limiter:start_link(no_id), rabbit_amqqueue:with_or_die( QName, - fun (Q1 = #amqqueue { pid = QPid1 }) -> + fun (Q1) when ?is_amqqueue(Q1) -> + QPid1 = amqqueue:get_pid(Q1), CountMinusOne = Count - 1, {ok, CountMinusOne, {QName, QPid1, _AckTag, true, _Msg}} = rabbit_amqqueue:basic_get(Q1, self(), false, Limiter, @@ -752,7 +754,9 @@ bq_queue_recover1(Config) -> passed. %% Return the PID of the given queue's supervisor. -get_queue_sup_pid(#amqqueue { pid = QPid, name = QName }) -> +get_queue_sup_pid(Q) when ?is_amqqueue(Q) -> + QName = amqqueue:get_name(Q), + QPid = amqqueue:get_pid(Q), VHost = QName#resource.virtual_host, {ok, AmqSup} = rabbit_amqqueue_sup_sup:find_for_vhost(VHost, node(QPid)), Sups = supervisor:which_children(AmqSup), @@ -1413,8 +1417,8 @@ with_fresh_variable_queue(Fun, Mode) -> shutdown, Fun(VQ1, QName)), Me ! Ref catch - Type:Error -> - Me ! {Ref, Type, Error, erlang:get_stacktrace()} + Type:Error:Stacktrace -> + Me ! {Ref, Type, Error, Stacktrace} end end), receive @@ -1498,8 +1502,7 @@ variable_queue_fetch(Count, IsPersistent, IsDelivered, Len, VQ) -> end, {VQ, []}, lists:seq(1, Count)). test_amqqueue(QName, Durable) -> - (rabbit_amqqueue:pseudo_queue(QName, self())) - #amqqueue { durable = Durable }. + rabbit_amqqueue:pseudo_queue(QName, self(), Durable). assert_prop(List, Prop, Value) -> case proplists:get_value(Prop, List)of diff --git a/test/channel_operation_timeout_SUITE.erl b/test/channel_operation_timeout_SUITE.erl index 9bfa0ae07a..77da6133d8 100644 --- a/test/channel_operation_timeout_SUITE.erl +++ b/test/channel_operation_timeout_SUITE.erl @@ -18,6 +18,7 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("amqp_client/include/amqp_client.hrl"). +-include("amqqueue.hrl"). -compile([export_all]). @@ -169,9 +170,16 @@ get_consumers(Config, Node, VHost) when is_atom(Node), rabbit_ct_broker_helpers:rpc(Config, Node, rabbit_amqqueue, consumers_all, [VHost]). -get_amqqueue(Q, []) -> throw({not_found, Q}); -get_amqqueue(Q, [AMQQ = #amqqueue{name = Q} | _]) -> AMQQ; -get_amqqueue(Q, [_| Rem]) -> get_amqqueue(Q, Rem). +get_amqqueue(QName0, []) -> + throw({not_found, QName0}); +get_amqqueue(QName0, [Q | Rem]) when ?is_amqqueue(Q) -> + QName1 = amqqueue:get_name(Q), + compare_amqqueue(QName0, QName1, Q, Rem). + +compare_amqqueue(QName, QName, Q, _Rem) -> + Q; +compare_amqqueue(QName, _, _, Rem) -> + get_amqqueue(QName, Rem). qconfig(Ch, Name, Ex, Consume, Deliver) -> [{ch, Ch}, {name, Name}, {ex,Ex}, {consume, Consume}, {deliver, Deliver}]. diff --git a/test/cluster_SUITE.erl b/test/cluster_SUITE.erl index c52dc9ef64..dc30825f8c 100644 --- a/test/cluster_SUITE.erl +++ b/test/cluster_SUITE.erl @@ -18,6 +18,7 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("amqp_client/include/amqp_client.hrl"). +-include("include/amqqueue.hrl"). -compile(export_all). @@ -225,9 +226,9 @@ declare_on_dead_queue1(_Config, SecondaryNode) -> Self = self(), Pid = spawn(SecondaryNode, fun () -> - {new, #amqqueue{name = QueueName, pid = QPid}} = - rabbit_amqqueue:declare(QueueName, false, false, [], - none, <<"acting-user">>), + {new, Q} = rabbit_amqqueue:declare(QueueName, false, false, [], none, <<"acting-user">>), + QueueName = ?amqqueue_field_name(Q), + QPid = ?amqqueue_field_pid(Q), exit(QPid, kill), Self ! {self(), killed, QPid} end), @@ -269,12 +270,12 @@ must_exit(Fun) -> end. dead_queue_loop(QueueName, OldPid) -> - {existing, Q} = rabbit_amqqueue:declare(QueueName, false, false, [], none, - <<"acting-user">>), - case Q#amqqueue.pid of + {existing, Q} = rabbit_amqqueue:declare(QueueName, false, false, [], none, <<"acting-user">>), + QPid = ?amqqueue_field_pid(Q), + case QPid of OldPid -> timer:sleep(25), dead_queue_loop(QueueName, OldPid); - _ -> true = rabbit_misc:is_process_alive(Q#amqqueue.pid), + _ -> true = rabbit_misc:is_process_alive(QPid), Q end. diff --git a/test/clustering_management_SUITE.erl b/test/clustering_management_SUITE.erl index 120257feb9..5ae2fb687c 100644 --- a/test/clustering_management_SUITE.erl +++ b/test/clustering_management_SUITE.erl @@ -315,14 +315,14 @@ forget_offline_removes_things(Config) -> forget_promotes_offline_slave(Config) -> [A, B, C, D] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename), ACh = rabbit_ct_client_helpers:open_channel(Config, A), - Q = <<"mirrored-queue">>, - declare(ACh, Q), - set_ha_policy(Config, Q, A, [B, C]), - set_ha_policy(Config, Q, A, [C, D]), %% Test add and remove from recoverable_slaves + QName = <<"mirrored-queue">>, + declare(ACh, QName), + set_ha_policy(Config, QName, A, [B, C]), + set_ha_policy(Config, QName, A, [C, D]), %% Test add and remove from recoverable_slaves %% Publish and confirm amqp_channel:call(ACh, #'confirm.select'{}), - amqp_channel:cast(ACh, #'basic.publish'{routing_key = Q}, + amqp_channel:cast(ACh, #'basic.publish'{routing_key = QName}, #amqp_msg{props = #'P_basic'{delivery_mode = 2}}), amqp_channel:wait_for_confirms(ACh), @@ -353,26 +353,50 @@ forget_promotes_offline_slave(Config) -> ok = rabbit_ct_broker_helpers:start_node(Config, D), DCh2 = rabbit_ct_client_helpers:open_channel(Config, D), - #'queue.declare_ok'{message_count = 1} = declare(DCh2, Q), + #'queue.declare_ok'{message_count = 1} = declare(DCh2, QName), ok. -set_ha_policy(Config, Q, Master, Slaves) -> +set_ha_policy(Config, QName, Master, Slaves) -> Nodes = [list_to_binary(atom_to_list(N)) || N <- [Master | Slaves]], - rabbit_ct_broker_helpers:set_ha_policy(Config, Master, Q, - {<<"nodes">>, Nodes}), - await_slaves(Q, Master, Slaves). - -await_slaves(Q, Master, Slaves) -> - {ok, #amqqueue{pid = MPid, - slave_pids = SPids}} = - rpc:call(Master, rabbit_amqqueue, lookup, - [rabbit_misc:r(<<"/">>, queue, Q)]), - ActMaster = node(MPid), + HaPolicy = {<<"nodes">>, Nodes}, + rabbit_ct_broker_helpers:set_ha_policy(Config, Master, QName, HaPolicy), + await_slaves(QName, Master, Slaves). + +await_slaves(QName, Master, Slaves) -> + await_slaves_0(QName, Master, Slaves, 10). + +await_slaves_0(QName, Master, Slaves0, Tries) -> + {ok, Queue} = await_slaves_lookup_queue(QName, Master), + SPids = amqqueue:get_slave_pids(Queue), + ActMaster = amqqueue:qnode(Queue), ActSlaves = lists:usort([node(P) || P <- SPids]), - case {Master, lists:usort(Slaves)} of - {ActMaster, ActSlaves} -> ok; - _ -> timer:sleep(100), - await_slaves(Q, Master, Slaves) + Slaves1 = lists:usort(Slaves0), + await_slaves_1(QName, ActMaster, ActSlaves, Master, Slaves1, Tries). + +await_slaves_1(QName, _ActMaster, _ActSlaves, _Master, _Slaves, 0) -> + error({timeout_waiting_for_slaves, QName}); +await_slaves_1(QName, ActMaster, ActSlaves, Master, Slaves, Tries) -> + case {Master, Slaves} of + {ActMaster, ActSlaves} -> + ok; + _ -> + timer:sleep(250), + await_slaves_0(QName, Master, Slaves, Tries - 1) + end. + +await_slaves_lookup_queue(QName, Master) -> + await_slaves_lookup_queue(QName, Master, 10). + +await_slaves_lookup_queue(QName, _Master, 0) -> + error({timeout_looking_up_queue, QName}); +await_slaves_lookup_queue(QName, Master, Tries) -> + RpcArgs = [rabbit_misc:r(<<"/">>, queue, QName)], + case rpc:call(Master, rabbit_amqqueue, lookup, RpcArgs) of + {error, not_found} -> + timer:sleep(250), + await_slaves_lookup_queue(QName, Master, Tries - 1); + {ok, Q} -> + {ok, Q} end. force_boot(Config) -> diff --git a/test/crashing_queues_SUITE.erl b/test/crashing_queues_SUITE.erl index 2d91083abc..7b8ec91346 100644 --- a/test/crashing_queues_SUITE.erl +++ b/test/crashing_queues_SUITE.erl @@ -217,9 +217,10 @@ kill_queue(Node, QName) -> await_new_pid(Node, QName, Pid1). queue_pid(Node, QName) -> - #amqqueue{pid = QPid, - state = State, - name = #resource{virtual_host = VHost}} = lookup(Node, QName), + Q = lookup(Node, QName), + QPid = amqqueue:get_pid(Q), + State = amqqueue:get_state(Q), + #resource{virtual_host = VHost} = amqqueue:get_name(Q), case State of crashed -> case rabbit_amqqueue_sup_sup:find_for_vhost(VHost, Node) of diff --git a/test/dynamic_ha_SUITE.erl b/test/dynamic_ha_SUITE.erl index e41c07a888..6ccf3a75c3 100644 --- a/test/dynamic_ha_SUITE.erl +++ b/test/dynamic_ha_SUITE.erl @@ -614,8 +614,8 @@ get_stacktrace() -> try throw(e) catch - _:e -> - erlang:get_stacktrace() + _:e:Stacktrace -> + Stacktrace end. %%---------------------------------------------------------------------------- diff --git a/test/dynamic_qq_SUITE.erl b/test/dynamic_qq_SUITE.erl index fbc1e81827..89344af30c 100644 --- a/test/dynamic_qq_SUITE.erl +++ b/test/dynamic_qq_SUITE.erl @@ -83,9 +83,26 @@ init_per_testcase(Testcase, Config) -> {queue_name, Q}, {queue_args, [{<<"x-queue-type">>, longstr, <<"quorum">>}]} ]), - rabbit_ct_helpers:run_steps(Config1, - rabbit_ct_broker_helpers:setup_steps() ++ - rabbit_ct_client_helpers:setup_steps()). + Config2 = rabbit_ct_helpers:run_steps( + Config1, + rabbit_ct_broker_helpers:setup_steps() ++ + rabbit_ct_client_helpers:setup_steps()), + Nodes = rabbit_ct_broker_helpers:get_node_configs( + Config2, nodename), + Ret = rabbit_ct_broker_helpers:rpc( + Config2, 0, + rabbit_feature_flags, + is_supported_remotely, + [Nodes, [quorum_queue], 60000]), + case Ret of + true -> + ok = rabbit_ct_broker_helpers:rpc( + Config2, 0, rabbit_feature_flags, enable, [quorum_queue]), + Config2; + false -> + end_per_testcase(Testcase, Config2), + {skip, "Quorum queues are unsupported"} + end. end_per_testcase(Testcase, Config) -> Config1 = rabbit_ct_helpers:run_steps(Config, diff --git a/test/feature_flags_SUITE.erl b/test/feature_flags_SUITE.erl new file mode 100644 index 0000000000..db87442105 --- /dev/null +++ b/test/feature_flags_SUITE.erl @@ -0,0 +1,372 @@ +%% The contents of this file are subject to the Mozilla Public License +%% Version 1.1 (the "License"); you may not use this file except in +%% compliance with the License. You may obtain a copy of the License at +%% http://www.mozilla.org/MPL/ +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the +%% License for the specific language governing rights and limitations +%% under the License. +%% +%% The Original Code is RabbitMQ. +%% +%% The Initial Developer of the Original Code is GoPivotal, Inc. +%% Copyright (c) 2018-2019 Pivotal Software, Inc. All rights reserved. +%% + +-module(feature_flags_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-export([suite/0, + all/0, + groups/0, + init_per_suite/1, + end_per_suite/1, + init_per_group/2, + end_per_group/2, + init_per_testcase/2, + end_per_testcase/2, + + enable_quorum_queue_in_a_healthy_situation/1, + enable_unsupported_feature_flag_in_a_healthy_situation/1, + enable_quorum_queue_when_ff_file_is_unwritable/1, + enable_quorum_queue_with_a_network_partition/1, + mark_quorum_queue_as_enabled_with_a_network_partition/1 + ]). + +suite() -> + [{timetrap, 5 * 60000}]. + +all() -> + [ + {group, unclustered}, + {group, clustered} + ]. + +groups() -> + [ + {unclustered, [], + [ + enable_quorum_queue_in_a_healthy_situation, + enable_unsupported_feature_flag_in_a_healthy_situation, + enable_quorum_queue_when_ff_file_is_unwritable + ]}, + {clustered, [], + [ + enable_quorum_queue_in_a_healthy_situation, + enable_unsupported_feature_flag_in_a_healthy_situation, + enable_quorum_queue_when_ff_file_is_unwritable, + enable_quorum_queue_with_a_network_partition, + mark_quorum_queue_as_enabled_with_a_network_partition + ]} + ]. + +%% ------------------------------------------------------------------- +%% Testsuite setup/teardown. +%% ------------------------------------------------------------------- + +init_per_suite(Config) -> + rabbit_ct_helpers:log_environment(), + rabbit_ct_helpers:run_setup_steps(Config, [ + fun rabbit_ct_broker_helpers:enable_dist_proxy_manager/1 + ]). + +end_per_suite(Config) -> + rabbit_ct_helpers:run_teardown_steps(Config). + +init_per_group(clustered, Config) -> + rabbit_ct_helpers:set_config(Config, [{rmq_nodes_count, 5}]); +init_per_group(unclustered, Config) -> + rabbit_ct_helpers:set_config(Config, [{rmq_nodes_count, 1}]); +init_per_group(_, Config) -> + Config. + +end_per_group(_, Config) -> + Config. + +init_per_testcase(Testcase, Config) -> + rabbit_ct_helpers:testcase_started(Config, Testcase), + TestNumber = rabbit_ct_helpers:testcase_number(Config, ?MODULE, Testcase), + ClusterSize = ?config(rmq_nodes_count, Config), + Config1 = rabbit_ct_helpers:set_config(Config, [ + {rmq_nodes_clustered, false}, + {rmq_nodename_suffix, Testcase}, + {tcp_ports_base, {skip_n_nodes, TestNumber * ClusterSize}}, + {net_ticktime, 5} + ]), + Config2 = rabbit_ct_helpers:merge_app_env( + Config1, + {rabbit, + [{forced_feature_flags_on_init, []}, + {log, [{file, [{level, debug}]}]}]}), + Config3 = rabbit_ct_helpers:run_steps( + Config2, + rabbit_ct_broker_helpers:setup_steps() ++ + rabbit_ct_client_helpers:setup_steps() ++ + [fun rabbit_ct_broker_helpers:enable_dist_proxy/1, + fun rabbit_ct_broker_helpers:cluster_nodes/1]), + Ret = rabbit_ct_broker_helpers:rpc( + Config3, 0, rabbit_feature_flags, is_supported, [quorum_queue]), + case Ret of + true -> + Config3; + false -> + end_per_testcase(Testcase, Config3), + {skip, "Quorum queues are unsupported"} + end. + +end_per_testcase(Testcase, Config) -> + Config1 = rabbit_ct_helpers:run_steps(Config, + rabbit_ct_client_helpers:teardown_steps() ++ + rabbit_ct_broker_helpers:teardown_steps()), + rabbit_ct_helpers:testcase_finished(Config1, Testcase). + +%% ------------------------------------------------------------------- +%% Testcases. +%% ------------------------------------------------------------------- + +enable_quorum_queue_in_a_healthy_situation(Config) -> + FeatureName = quorum_queue, + ClusterSize = ?config(rmq_nodes_count, Config), + Node = ClusterSize - 1, + True = lists:duplicate(ClusterSize, true), + False = lists:duplicate(ClusterSize, false), + + %% The feature flag is supported but disabled initially. + ?assertEqual( + True, + is_feature_flag_supported(Config, FeatureName)), + ?assertEqual( + False, + is_feature_flag_enabled(Config, FeatureName)), + + %% Enabling the feature flag works. + ?assertEqual( + ok, + enable_feature_flag_on(Config, Node, FeatureName)), + ?assertEqual( + True, + is_feature_flag_enabled(Config, FeatureName)), + + %% Re-enabling the feature flag also works. + ?assertEqual( + ok, + enable_feature_flag_on(Config, Node, FeatureName)), + ?assertEqual( + True, + is_feature_flag_enabled(Config, FeatureName)). + +enable_unsupported_feature_flag_in_a_healthy_situation(Config) -> + FeatureName = unsupported_feature_flag, + ClusterSize = ?config(rmq_nodes_count, Config), + Node = ClusterSize - 1, + False = lists:duplicate(ClusterSize, false), + + %% The feature flag is unsupported and thus disabled. + ?assertEqual( + False, + is_feature_flag_supported(Config, FeatureName)), + ?assertEqual( + False, + is_feature_flag_enabled(Config, FeatureName)), + + %% Enabling the feature flag works. + ?assertEqual( + {error, unsupported}, + enable_feature_flag_on(Config, Node, FeatureName)), + ?assertEqual( + False, + is_feature_flag_enabled(Config, FeatureName)). + +enable_quorum_queue_when_ff_file_is_unwritable(Config) -> + FeatureName = quorum_queue, + ClusterSize = ?config(rmq_nodes_count, Config), + Node = ClusterSize - 1, + True = lists:duplicate(ClusterSize, true), + False = lists:duplicate(ClusterSize, false), + Files = feature_flags_files(Config), + + %% The feature flag is supported but disabled initially. + ?assertEqual( + True, + is_feature_flag_supported(Config, FeatureName)), + ?assertEqual( + False, + is_feature_flag_enabled(Config, FeatureName)), + + %% Restrict permissions on the `feature_flags` files. + [?assertEqual(ok, file:change_mode(File, 8#0444)) || File <- Files], + + %% Enabling the feature flag works. + ?assertEqual( + ok, + enable_feature_flag_on(Config, Node, FeatureName)), + ?assertEqual( + True, + is_feature_flag_enabled(Config, FeatureName)), + + %% The `feature_flags` file were not updated. + ?assertEqual( + lists:duplicate(ClusterSize, {ok, [[]]}), + [file:consult(File) || File <- feature_flags_files(Config)]), + + %% Stop all nodes and restore permissions on the `feature_flags` files. + Nodes = rabbit_ct_broker_helpers:get_node_configs(Config, nodename), + [?assertEqual(ok, rabbit_ct_broker_helpers:stop_node(Config, N)) + || N <- Nodes], + [?assertEqual(ok, file:change_mode(File, 8#0644)) || File <- Files], + + %% Restart all nodes and assert the feature flag is still enabled and + %% the `feature_flags` files were correctly repaired. + [?assertEqual(ok, rabbit_ct_broker_helpers:start_node(Config, N)) + || N <- lists:reverse(Nodes)], + + ?assertEqual( + True, + is_feature_flag_enabled(Config, FeatureName)), + ?assertEqual( + lists:duplicate(ClusterSize, {ok, [[FeatureName]]}), + [file:consult(File) || File <- feature_flags_files(Config)]). + +enable_quorum_queue_with_a_network_partition(Config) -> + FeatureName = quorum_queue, + ClusterSize = ?config(rmq_nodes_count, Config), + [A, B, C, D, E] = rabbit_ct_broker_helpers:get_node_configs( + Config, nodename), + True = lists:duplicate(ClusterSize, true), + False = lists:duplicate(ClusterSize, false), + + %% The feature flag is supported but disabled initially. + ?assertEqual( + True, + is_feature_flag_supported(Config, FeatureName)), + ?assertEqual( + False, + is_feature_flag_enabled(Config, FeatureName)), + + %% Isolate nodes B and E from the rest of the cluster. + NodePairs = [{B, A}, + {B, C}, + {B, D}, + {E, A}, + {E, C}, + {E, D}], + block(NodePairs), + timer:sleep(1000), + + %% Enabling the feature flag should fail in the specific case of + %% `quorum_queue`, if the network is broken. + ?assertEqual( + {error, unsupported}, + enable_feature_flag_on(Config, B, FeatureName)), + ?assertEqual( + False, + is_feature_flag_enabled(Config, FeatureName)), + + %% Repair the network and try again to enable the feature flag. + unblock(NodePairs), + timer:sleep(1000), + [?assertEqual(ok, rabbit_ct_broker_helpers:stop_node(Config, N)) + || N <- [A, C, D]], + [?assertEqual(ok, rabbit_ct_broker_helpers:start_node(Config, N)) + || N <- [A, C, D]], + + %% Enabling the feature flag works. + ?assertEqual( + ok, + enable_feature_flag_on(Config, B, FeatureName)), + ?assertEqual( + True, + is_feature_flag_enabled(Config, FeatureName)). + +mark_quorum_queue_as_enabled_with_a_network_partition(Config) -> + FeatureName = quorum_queue, + ClusterSize = ?config(rmq_nodes_count, Config), + [A, B, C, D, E] = rabbit_ct_broker_helpers:get_node_configs( + Config, nodename), + True = lists:duplicate(ClusterSize, true), + False = lists:duplicate(ClusterSize, false), + + %% The feature flag is supported but disabled initially. + ?assertEqual( + True, + is_feature_flag_supported(Config, FeatureName)), + ?assertEqual( + False, + is_feature_flag_enabled(Config, FeatureName)), + + %% Isolate node B from the rest of the cluster. + NodePairs = [{B, A}, + {B, C}, + {B, D}, + {B, E}], + block(NodePairs), + timer:sleep(1000), + + %% Mark the feature flag as enabled on all nodes from node B. This + %% is expected to timeout. + RemoteNodes = [A, C, D, E], + ?assertEqual( + {failed_to_mark_feature_flag_as_enabled_on_remote_nodes, + FeatureName, + true, + RemoteNodes}, + rabbit_ct_broker_helpers:rpc( + Config, B, + rabbit_feature_flags, mark_as_enabled_remotely, + [RemoteNodes, FeatureName, true, 20000])), + + RepairFun = fun() -> + %% Wait a few seconds before we repair the network. + timer:sleep(5000), + + %% Repair the network and try again to enable + %% the feature flag. + unblock(NodePairs), + timer:sleep(1000) + end, + spawn(RepairFun), + + %% Mark the feature flag as enabled on all nodes from node B. This + %% is expected to work this time. + ct:pal(?LOW_IMPORTANCE, + "Marking the feature flag as enabled on remote nodes...", []), + ?assertEqual( + ok, + rabbit_ct_broker_helpers:rpc( + Config, B, + rabbit_feature_flags, mark_as_enabled_remotely, + [RemoteNodes, FeatureName, true, 120000])). + +%% FIXME: Finish the testcase above ^ + +%% ------------------------------------------------------------------- +%% Internal helpers. +%% ------------------------------------------------------------------- + +enable_feature_flag_on(Config, Node, FeatureName) -> + rabbit_ct_broker_helpers:rpc( + Config, Node, rabbit_feature_flags, enable, [FeatureName]). + +is_feature_flag_supported(Config, FeatureName) -> + rabbit_ct_broker_helpers:rpc_all( + Config, rabbit_feature_flags, is_supported, [FeatureName]). + +is_feature_flag_enabled(Config, FeatureName) -> + rabbit_ct_broker_helpers:rpc_all( + Config, rabbit_feature_flags, is_enabled, [FeatureName]). + +feature_flags_files(Config) -> + rabbit_ct_broker_helpers:rpc_all( + Config, rabbit_feature_flags, enabled_feature_flags_list_file, []). + +block(Pairs) -> [block(X, Y) || {X, Y} <- Pairs]. +unblock(Pairs) -> [allow(X, Y) || {X, Y} <- Pairs]. + +block(X, Y) -> + rabbit_ct_broker_helpers:block_traffic_between(X, Y). + +allow(X, Y) -> + rabbit_ct_broker_helpers:allow_traffic_between(X, Y). diff --git a/test/mirrored_supervisor_SUITE.erl b/test/mirrored_supervisor_SUITE.erl index d3cc080eeb..aa114e0a84 100644 --- a/test/mirrored_supervisor_SUITE.erl +++ b/test/mirrored_supervisor_SUITE.erl @@ -294,14 +294,17 @@ get_group(Group) -> call(Id, Msg) -> call(Id, Msg, 10*1000, 100). -call(Id, Msg, 0, _Decr) -> - exit({timeout_waiting_for_server, {Id, Msg}, erlang:get_stacktrace()}); - call(Id, Msg, MaxDelay, Decr) -> + call(Id, Msg, MaxDelay, Decr, undefined). + +call(Id, Msg, 0, _Decr, Stacktrace) -> + exit({timeout_waiting_for_server, {Id, Msg}, Stacktrace}); + +call(Id, Msg, MaxDelay, Decr, _) -> try gen_server:call(Id, Msg, infinity) - catch exit:_ -> timer:sleep(Decr), - call(Id, Msg, MaxDelay - Decr, Decr) + catch exit:_:Stacktrace -> timer:sleep(Decr), + call(Id, Msg, MaxDelay - Decr, Decr, Stacktrace) end. kill(Pid) -> kill(Pid, []). diff --git a/test/priority_queue_SUITE.erl b/test/priority_queue_SUITE.erl index 9db866e3c6..8a0ab98241 100644 --- a/test/priority_queue_SUITE.erl +++ b/test/priority_queue_SUITE.erl @@ -366,9 +366,9 @@ info_head_message_timestamp1(_Config) -> QName = rabbit_misc:r(<<"/">>, queue, <<"info_head_message_timestamp-queue">>), Q0 = rabbit_amqqueue:pseudo_queue(QName, self()), - Q = Q0#amqqueue{arguments = [{<<"x-max-priority">>, long, 2}]}, + Q1 = amqqueue:set_arguments(Q0, [{<<"x-max-priority">>, long, 2}]), PQ = rabbit_priority_queue, - BQS1 = PQ:init(Q, new, fun(_, _) -> ok end), + BQS1 = PQ:init(Q1, new, fun(_, _) -> ok end), %% The queue is empty: no timestamp. true = PQ:is_empty(BQS1), '' = PQ:info(head_message_timestamp, BQS1), @@ -415,9 +415,9 @@ info_head_message_timestamp1(_Config) -> ram_duration(_Config) -> QName = rabbit_misc:r(<<"/">>, queue, <<"ram_duration-queue">>), Q0 = rabbit_amqqueue:pseudo_queue(QName, self()), - Q = Q0#amqqueue{arguments = [{<<"x-max-priority">>, long, 5}]}, + Q1 = amqqueue:set_arguments(Q0, [{<<"x-max-priority">>, long, 5}]), PQ = rabbit_priority_queue, - BQS1 = PQ:init(Q, new, fun(_, _) -> ok end), + BQS1 = PQ:init(Q1, new, fun(_, _) -> ok end), {_Duration1, BQS2} = PQ:ram_duration(BQS1), BQS3 = PQ:set_ram_duration_target(infinity, BQS2), BQS4 = PQ:set_ram_duration_target(1, BQS3), diff --git a/test/quorum_queue_SUITE.erl b/test/quorum_queue_SUITE.erl index 48dac3ca57..0f9010f204 100644 --- a/test/quorum_queue_SUITE.erl +++ b/test/quorum_queue_SUITE.erl @@ -28,6 +28,9 @@ -compile(export_all). +suite() -> + [{timetrap, 5 * 60000}]. + all() -> [ {group, single_node}, @@ -172,17 +175,32 @@ init_per_group(Group, Config) -> Config2 = rabbit_ct_helpers:run_steps(Config1b, [fun merge_app_env/1 ] ++ rabbit_ct_broker_helpers:setup_steps()), - ok = rabbit_ct_broker_helpers:rpc( - Config2, 0, application, set_env, - [rabbit, channel_queue_cleanup_interval, 100]), - %% HACK: the larger cluster sizes benefit for a bit more time - %% after clustering before running the tests. - case Group of - cluster_size_5 -> - timer:sleep(5000), - Config2; - _ -> - Config2 + Nodes = rabbit_ct_broker_helpers:get_node_configs( + Config2, nodename), + Ret = rabbit_ct_broker_helpers:rpc( + Config2, 0, + rabbit_feature_flags, + is_supported_remotely, + [Nodes, [quorum_queue], 60000]), + case Ret of + true -> + ok = rabbit_ct_broker_helpers:rpc( + Config2, 0, rabbit_feature_flags, enable, [quorum_queue]), + ok = rabbit_ct_broker_helpers:rpc( + Config2, 0, application, set_env, + [rabbit, channel_queue_cleanup_interval, 100]), + %% HACK: the larger cluster sizes benefit for a bit more time + %% after clustering before running the tests. + case Group of + cluster_size_5 -> + timer:sleep(5000), + Config2; + _ -> + Config2 + end; + false -> + end_per_group(Group, Config2), + {skip, "Quorum queues are unsupported"} end. end_per_group(clustered, Config) -> @@ -206,11 +224,28 @@ init_per_testcase(Testcase, Config) when Testcase == reconnect_consumer_and_publ {tcp_ports_base}, {queue_name, Q} ]), - rabbit_ct_helpers:run_steps(Config2, - rabbit_ct_broker_helpers:setup_steps() ++ - rabbit_ct_client_helpers:setup_steps() ++ - [fun rabbit_ct_broker_helpers:enable_dist_proxy/1, - fun rabbit_ct_broker_helpers:cluster_nodes/1]); + Config3 = rabbit_ct_helpers:run_steps( + Config2, + rabbit_ct_broker_helpers:setup_steps() ++ + rabbit_ct_client_helpers:setup_steps() ++ + [fun rabbit_ct_broker_helpers:enable_dist_proxy/1, + fun rabbit_ct_broker_helpers:cluster_nodes/1]), + Nodes = rabbit_ct_broker_helpers:get_node_configs( + Config3, nodename), + Ret = rabbit_ct_broker_helpers:rpc( + Config3, 0, + rabbit_feature_flags, + is_supported_remotely, + [Nodes, [quorum_queue], 60000]), + case Ret of + true -> + ok = rabbit_ct_broker_helpers:rpc( + Config3, 0, rabbit_feature_flags, enable, [quorum_queue]), + Config3; + false -> + end_per_testcase(Testcase, Config3), + {skip, "Quorum queues are unsupported"} + end; init_per_testcase(Testcase, Config) -> Config1 = rabbit_ct_helpers:testcase_started(Config, Testcase), rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, delete_queues, []), @@ -2193,11 +2228,10 @@ assert_queue_type(Server, Q, Expected) -> Actual = get_queue_type(Server, Q), Expected = Actual. -get_queue_type(Server, Q) -> - QNameRes = rabbit_misc:r(<<"/">>, queue, Q), - {ok, AMQQueue} = - rpc:call(Server, rabbit_amqqueue, lookup, [QNameRes]), - AMQQueue#amqqueue.type. +get_queue_type(Server, Q0) -> + QNameRes = rabbit_misc:r(<<"/">>, queue, Q0), + {ok, Q1} = rpc:call(Server, rabbit_amqqueue, lookup, [QNameRes]), + amqqueue:get_type(Q1). wait_for_messages(Config, Stats) -> wait_for_messages(Config, lists:sort(Stats), 60). diff --git a/test/rabbit_core_metrics_gc_SUITE.erl b/test/rabbit_core_metrics_gc_SUITE.erl index 7ae43aa7e1..42ac863ead 100644 --- a/test/rabbit_core_metrics_gc_SUITE.erl +++ b/test/rabbit_core_metrics_gc_SUITE.erl @@ -364,11 +364,9 @@ cluster_queue_metrics(Config) -> % Synchronize Name = rabbit_misc:r(VHost, queue, QueueName), - [#amqqueue{pid = QPid}] = rabbit_ct_broker_helpers:rpc(Config, Node0, - ets, lookup, - [rabbit_queue, Name]), - ok = rabbit_ct_broker_helpers:rpc(Config, Node0, rabbit_amqqueue, - sync_mirrors, [QPid]), + [Q] = rabbit_ct_broker_helpers:rpc(Config, Node0, ets, lookup, [rabbit_queue, Name]), + QPid = amqqueue:get_pid(Q), + ok = rabbit_ct_broker_helpers:rpc(Config, Node0, rabbit_amqqueue, sync_mirrors, [QPid]), % Check ETS table for data wait_for(fun () -> diff --git a/test/single_active_consumer_SUITE.erl b/test/single_active_consumer_SUITE.erl index 25261042b2..6071aeb5a5 100644 --- a/test/single_active_consumer_SUITE.erl +++ b/test/single_active_consumer_SUITE.erl @@ -66,13 +66,21 @@ init_per_group(classic_queue, Config) -> auto_delete = true} } | Config]; init_per_group(quorum_queue, Config) -> - [{single_active_consumer_queue_declare, - #'queue.declare'{arguments = [ - {<<"x-single-active-consumer">>, bool, true}, - {<<"x-queue-type">>, longstr, <<"quorum">>} - ], - durable = true, exclusive = false, auto_delete = false} - } | Config]. + Ret = rabbit_ct_broker_helpers:rpc( + Config, 0, rabbit_feature_flags, enable, [quorum_queue]), + case Ret of + ok -> + [{single_active_consumer_queue_declare, + #'queue.declare'{ + arguments = [ + {<<"x-single-active-consumer">>, bool, true}, + {<<"x-queue-type">>, longstr, <<"quorum">>} + ], + durable = true, exclusive = false, auto_delete = false} + } | Config]; + Error -> + {skip, {"Quorum queues are unsupported", Error}} + end. end_per_group(_, Config) -> Config. diff --git a/test/unit_inbroker_non_parallel_SUITE.erl b/test/unit_inbroker_non_parallel_SUITE.erl index e3e282f233..d2db382e30 100644 --- a/test/unit_inbroker_non_parallel_SUITE.erl +++ b/test/unit_inbroker_non_parallel_SUITE.erl @@ -568,7 +568,7 @@ head_message_timestamp1(_Config) -> QRes = rabbit_misc:r(<<"/">>, queue, QName), {ok, Q1} = rabbit_amqqueue:lookup(QRes), - QPid = Q1#amqqueue.pid, + QPid = amqqueue:get_pid(Q1), %% Set up event receiver for queue dummy_event_receiver:start(self(), [node()], [queue_stats]), diff --git a/test/unit_inbroker_parallel_SUITE.erl b/test/unit_inbroker_parallel_SUITE.erl index 581440d179..f4f4971517 100644 --- a/test/unit_inbroker_parallel_SUITE.erl +++ b/test/unit_inbroker_parallel_SUITE.erl @@ -99,10 +99,24 @@ init_per_group(max_length_classic, Config) -> [{queue_args, [{<<"x-queue-type">>, longstr, <<"classic">>}]}, {queue_durable, false}]); init_per_group(max_length_quorum, Config) -> - rabbit_ct_helpers:set_config( - Config, - [{queue_args, [{<<"x-queue-type">>, longstr, <<"quorum">>}]}, - {queue_durable, true}]); + Nodes = rabbit_ct_broker_helpers:get_node_configs( + Config, nodename), + Ret = rabbit_ct_broker_helpers:rpc( + Config, 0, + rabbit_feature_flags, + is_supported_remotely, + [Nodes, [quorum_queue], 60000]), + case Ret of + true -> + ok = rabbit_ct_broker_helpers:rpc( + Config, 0, rabbit_feature_flags, enable, [quorum_queue]), + rabbit_ct_helpers:set_config( + Config, + [{queue_args, [{<<"x-queue-type">>, longstr, <<"quorum">>}]}, + {queue_durable, true}]); + false -> + {skip, "Quorum queues are unsupported"} + end; init_per_group(max_length_mirrored, Config) -> rabbit_ct_broker_helpers:set_ha_policy(Config, 0, <<"^max_length.*queue">>, <<"all">>, [{<<"ha-sync-mode">>, <<"automatic">>}]), @@ -269,8 +283,7 @@ wait_for_confirms(Unconfirmed) -> end. test_amqqueue(Durable) -> - (rabbit_amqqueue:pseudo_queue(test_queue(), self())) - #amqqueue { durable = Durable }. + rabbit_amqqueue:pseudo_queue(test_queue(), self(), Durable). assert_prop(List, Prop, Value) -> case proplists:get_value(Prop, List)of @@ -695,7 +708,7 @@ head_message_timestamp1(_Config) -> QRes = rabbit_misc:r(<<"/">>, queue, QName), {ok, Q1} = rabbit_amqqueue:lookup(QRes), - QPid = Q1#amqqueue.pid, + QPid = amqqueue:get_pid(Q1), %% Set up event receiver for queue dummy_event_receiver:start(self(), [node()], [queue_stats]), @@ -944,7 +957,7 @@ confirms1(_Config) -> QName2 = DeclareBindDurableQueue(), %% Get the first one's pid (we'll crash it later) {ok, Q1} = rabbit_amqqueue:lookup(rabbit_misc:r(<<"/">>, queue, QName1)), - QPid1 = Q1#amqqueue.pid, + QPid1 = amqqueue:get_pid(Q1), %% Enable confirms rabbit_channel:do(Ch, #'confirm.select'{}), receive diff --git a/test/vhost_SUITE.erl b/test/vhost_SUITE.erl index 2de5819b54..5b6a6bd6ff 100644 --- a/test/vhost_SUITE.erl +++ b/test/vhost_SUITE.erl @@ -354,7 +354,7 @@ node_starts_with_dead_vhosts_and_ignore_slaves(Config) -> Node1 = rabbit_ct_broker_helpers:get_node_config(Config, 1, nodename), - #amqqueue{sync_slave_pids = [Pid]} = Q, + [Pid] = amqqueue:get_sync_slave_pids(Q), Node1 = node(Pid), |
