diff --git a/deps/rabbit/src/rabbit_core_ff.erl b/deps/rabbit/src/rabbit_core_ff.erl index 5475909eec54..c83548030829 100644 --- a/deps/rabbit/src/rabbit_core_ff.erl +++ b/deps/rabbit/src/rabbit_core_ff.erl @@ -10,14 +10,16 @@ -rabbit_feature_flag( {classic_mirrored_queue_version, #{desc => "Support setting version for classic mirrored queues", - stability => required + stability => required, + require_level => hard }}). -rabbit_feature_flag( {quorum_queue, #{desc => "Support queues of type `quorum`", doc_url => "https://www.rabbitmq.com/docs/quorum-queues", - stability => required + stability => required, + require_level => hard }}). -rabbit_feature_flag( @@ -25,6 +27,7 @@ #{desc => "Support queues of type `stream`", doc_url => "https://www.rabbitmq.com/docs/stream", stability => required, + require_level => hard, depends_on => [quorum_queue] }}). @@ -32,25 +35,29 @@ {implicit_default_bindings, #{desc => "Default bindings are now implicit, instead of " "being stored in the database", - stability => required + stability => required, + require_level => hard }}). -rabbit_feature_flag( {virtual_host_metadata, #{desc => "Virtual host metadata (description, tags, etc)", - stability => required + stability => required, + require_level => hard }}). -rabbit_feature_flag( {maintenance_mode_status, #{desc => "Maintenance mode status", - stability => required + stability => required, + require_level => hard }}). -rabbit_feature_flag( - {user_limits, - #{desc => "Configure connection and channel limits for a user", - stability => required + {user_limits, + #{desc => "Configure connection and channel limits for a user", + stability => required, + require_level => hard }}). -rabbit_feature_flag( @@ -58,33 +65,38 @@ #{desc => "Single active consumer for streams", doc_url => "https://www.rabbitmq.com/docs/stream", stability => required, + require_level => hard, depends_on => [stream_queue] }}). -rabbit_feature_flag( - {feature_flags_v2, - #{desc => "Feature flags subsystem V2", - stability => required + {feature_flags_v2, + #{desc => "Feature flags subsystem V2", + stability => required, + require_level => hard }}). -rabbit_feature_flag( {direct_exchange_routing_v2, - #{desc => "v2 direct exchange routing implementation", - stability => required, - depends_on => [feature_flags_v2, implicit_default_bindings] + #{desc => "v2 direct exchange routing implementation", + stability => required, + require_level => hard, + depends_on => [feature_flags_v2, implicit_default_bindings] }}). -rabbit_feature_flag( {listener_records_in_ets, - #{desc => "Store listener records in ETS instead of Mnesia", - stability => required, - depends_on => [feature_flags_v2] + #{desc => "Store listener records in ETS instead of Mnesia", + stability => required, + require_level => hard, + depends_on => [feature_flags_v2] }}). -rabbit_feature_flag( {tracking_records_in_ets, #{desc => "Store tracking records in ETS instead of Mnesia", stability => required, + require_level => hard, depends_on => [feature_flags_v2] }}). @@ -94,6 +106,7 @@ doc_url => "https://github.com/rabbitmq/rabbitmq-server/issues/5931", %%TODO remove compatibility code stability => required, + require_level => hard, depends_on => [stream_queue] }}). @@ -102,6 +115,7 @@ #{desc => "Support for restarting streams with optional preferred next leader argument." "Used to implement stream leader rebalancing", stability => required, + require_level => hard, depends_on => [stream_queue] }}). @@ -110,6 +124,7 @@ #{desc => "Bug fix to unblock a group of consumers in a super stream partition", doc_url => "https://github.com/rabbitmq/rabbitmq-server/issues/7743", stability => required, + require_level => hard, depends_on => [stream_single_active_consumer] }}). @@ -117,6 +132,7 @@ {stream_filtering, #{desc => "Support for stream filtering.", stability => required, + require_level => hard, depends_on => [stream_queue] }}). @@ -124,6 +140,7 @@ {message_containers, #{desc => "Message containers.", stability => required, + require_level => hard, depends_on => [feature_flags_v2] }}). @@ -154,6 +171,7 @@ #{desc => "A new internal command that is used to update streams as " "part of a policy.", stability => required, + require_level => hard, depends_on => [stream_queue] }}). diff --git a/deps/rabbit/src/rabbit_feature_flags.erl b/deps/rabbit/src/rabbit_feature_flags.erl index 12fc1b7b939f..d50e30375c81 100644 --- a/deps/rabbit/src/rabbit_feature_flags.erl +++ b/deps/rabbit/src/rabbit_feature_flags.erl @@ -105,6 +105,7 @@ init/0, get_state/1, get_stability/1, + get_require_level/1, check_node_compatibility/1, check_node_compatibility/2, sync_feature_flags_with_cluster/2, refresh_feature_flags_after_app_load/0, @@ -147,6 +148,7 @@ -type feature_props() :: #{desc => string(), doc_url => string(), stability => stability(), + require_level => require_level(), depends_on => [feature_name()], callbacks => #{callback_name() => callback_fun_name()}}. @@ -183,6 +185,7 @@ desc => string(), doc_url => string(), stability => stability(), + require_level => require_level(), depends_on => [feature_name()], callbacks => #{callback_name() => callback_fun_name()}, @@ -207,6 +210,15 @@ %% Experimental feature flags are not enabled by default on a fresh RabbitMQ %% node. They must be enabled by the user. +-type require_level() :: hard | soft. +%% The level of requirement of a feature flag. +%% +%% A hard required feature flags must be enabled before a RabbitMQ node is +%% upgraded to a version where it is required. +%% +%% A soft required feature flag will be automatically enabled when a RabbitMQ +%% node is upgraded to a version where it is required. + -type callback_fun_name() :: {Module :: module(), Function :: atom()}. %% The name of the module and function to call when changing the state of %% the feature flag. @@ -755,6 +767,48 @@ get_stability(FeatureProps) when ?IS_DEPRECATION(FeatureProps) -> permitted_by_default -> experimental end. +-spec get_require_level +(FeatureName) -> RequireLevel | undefined when + FeatureName :: feature_name(), + RequireLevel :: require_level() | none; +(FeatureProps) -> RequireLevel when + FeatureProps :: + feature_props_extended() | + rabbit_deprecated_features:feature_props_extended(), + RequireLevel :: require_level() | none. +%% @doc +%% Returns the requirement level of a feature flag. +%% +%% The possible requirement levels are: +%% +%% +%% @param FeatureName The name of the feature flag to check. +%% @param FeatureProps A feature flag properties map. +%% @returns `hard', `soft' or `none', or `undefined' if the given feature flag +%% name doesn't correspond to a known feature flag. + +get_require_level(FeatureName) when is_atom(FeatureName) -> + case rabbit_ff_registry_wrapper:get(FeatureName) of + undefined -> undefined; + FeatureProps -> get_require_level(FeatureProps) + end; +get_require_level(FeatureProps) when ?IS_FEATURE_FLAG(FeatureProps) -> + case get_stability(FeatureProps) of + required -> maps:get(require_level, FeatureProps, soft); + _ -> none + end; +get_require_level(FeatureProps) when ?IS_DEPRECATION(FeatureProps) -> + case get_stability(FeatureProps) of + required -> hard; + _ -> none + end. + %% ------------------------------------------------------------------- %% Feature flags registry. %% ------------------------------------------------------------------- @@ -913,6 +967,7 @@ assert_feature_flag_is_valid(FeatureName, FeatureProps) -> ValidProps = [desc, doc_url, stability, + require_level, depends_on, callbacks], ?assertEqual([], maps:keys(FeatureProps) -- ValidProps), @@ -1363,7 +1418,7 @@ run_feature_flags_mod_on_remote_node(Node, Function, Args, Timeout) -> sync_feature_flags_with_cluster([] = _Nodes, true = _NodeIsVirgin) -> rabbit_ff_controller:enable_default(); sync_feature_flags_with_cluster([] = _Nodes, false = _NodeIsVirgin) -> - ok; + rabbit_ff_controller:enable_required(); sync_feature_flags_with_cluster(Nodes, _NodeIsVirgin) -> %% We don't use `rabbit_nodes:filter_running()' here because the given %% `Nodes' list may contain nodes which are not members yet (the cluster diff --git a/deps/rabbit/src/rabbit_ff_controller.erl b/deps/rabbit/src/rabbit_ff_controller.erl index d6f11a73c9ab..2690d261700f 100644 --- a/deps/rabbit/src/rabbit_ff_controller.erl +++ b/deps/rabbit/src/rabbit_ff_controller.erl @@ -38,6 +38,7 @@ -export([is_supported/1, is_supported/2, enable/1, enable_default/0, + enable_required/0, check_node_compatibility/2, sync_cluster/1, refresh_after_app_load/0, @@ -136,6 +137,24 @@ enable_default() -> Ret end. +enable_required() -> + ?LOG_DEBUG( + "Feature flags: enable required feature flags", + #{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS}), + case erlang:whereis(?LOCAL_NAME) of + Pid when is_pid(Pid) -> + %% The function is called while `rabbit' is running. + gen_statem:call(?LOCAL_NAME, enable_required); + undefined -> + %% The function is called while `rabbit' is stopped. We need to + %% start a one-off controller, again to make sure concurrent + %% changes are blocked. + {ok, Pid} = start_link(), + Ret = gen_statem:call(Pid, enable_required), + gen_statem:stop(Pid), + Ret + end. + check_node_compatibility(RemoteNode, LocalNodeAsVirgin) -> ThisNode = node(), case LocalNodeAsVirgin of @@ -206,7 +225,7 @@ standing_by( when EventContent =/= notify_when_done -> ?LOG_DEBUG( "Feature flags: registering controller globally before " - "proceeding with task: ~tp", + "proceeding with task: ~0tp", [EventContent], #{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS}), @@ -304,6 +323,8 @@ proceed_with_task({enable, FeatureNames}) -> enable_task(FeatureNames); proceed_with_task(enable_default) -> enable_default_task(); +proceed_with_task(enable_required) -> + enable_required_task(); proceed_with_task({sync_cluster, Nodes}) -> sync_cluster_task(Nodes); proceed_with_task(refresh_after_app_load) -> @@ -841,6 +862,24 @@ get_forced_feature_flag_names_from_config() -> _ when is_list(Value) -> {ok, Value} end. +-spec enable_required_task() -> Ret when + Ret :: ok | {error, Reason}, + Reason :: term(). + +enable_required_task() -> + {ok, Inventory} = collect_inventory_on_nodes([node()]), + RequiredFeatureNames = list_soft_required_feature_flags(Inventory), + case RequiredFeatureNames of + [] -> + ok; + _ -> + ?LOG_DEBUG( + "Feature flags: enabling required feature flags: ~0p", + [RequiredFeatureNames], + #{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS}) + end, + enable_many(Inventory, RequiredFeatureNames). + -spec sync_cluster_task() -> Ret when Ret :: ok | {error, Reason}, Reason :: term(). @@ -855,23 +894,6 @@ sync_cluster_task() -> Reason :: term(). sync_cluster_task(Nodes) -> - %% We assume that a feature flag can only be enabled, not disabled. - %% Therefore this synchronization searches for feature flags enabled on - %% some nodes but not all, and make sure they are enabled everywhere. - %% - %% This happens when a node joins a cluster and that node has a different - %% set of enabled feature flags. - %% - %% FIXME: `enable_task()' requires that all nodes in the cluster run to - %% enable anything. Should we require the same here? On one hand, this - %% would make sure a feature flag isn't enabled while there is a network - %% partition. On the other hand, this would require that all nodes are - %% running before we can expand the cluster... - ?LOG_DEBUG( - "Feature flags: synchronizing feature flags on nodes: ~tp", - [Nodes], - #{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS}), - case collect_inventory_on_nodes(Nodes) of {ok, Inventory} -> CantEnable = list_deprecated_features_that_cant_be_denied( @@ -880,7 +902,27 @@ sync_cluster_task(Nodes) -> [] -> FeatureNames = list_feature_flags_enabled_somewhere( Inventory, false), - enable_many(Inventory, FeatureNames); + + %% In addition to feature flags enabled somewhere, we also + %% ensure required feature flags are enabled accross the + %% board. + RequiredFeatureNames = list_soft_required_feature_flags( + Inventory), + case RequiredFeatureNames of + [] -> + ok; + _ -> + ?LOG_DEBUG( + "Feature flags: enabling required feature " + "flags as part of cluster sync: ~0p", + [RequiredFeatureNames], + #{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS}) + end, + + FeatureNamesToEnable = lists:usort( + FeatureNames ++ + RequiredFeatureNames), + enable_many(Inventory, FeatureNamesToEnable); _ -> ?LOG_ERROR( "Feature flags: the following deprecated features " @@ -1026,13 +1068,13 @@ check_required_and_enable( FeatureName) -> %% Required feature flags vs. virgin nodes. FeatureProps = maps:get(FeatureName, FeatureFlags), - Stability = rabbit_feature_flags:get_stability(FeatureProps), + RequireLevel = rabbit_feature_flags:get_require_level(FeatureProps), ProvidedBy = maps:get(provided_by, FeatureProps), NodesWhereDisabled = list_nodes_where_feature_flag_is_disabled( Inventory, FeatureName), - MarkDirectly = case Stability of - required when ProvidedBy =:= rabbit -> + MarkDirectly = case RequireLevel of + hard when ProvidedBy =:= rabbit -> ?LOG_DEBUG( "Feature flags: `~s`: the feature flag is " "required on some nodes; list virgin nodes " @@ -1051,7 +1093,7 @@ check_required_and_enable( end end, NodesWhereDisabled), VirginNodesWhereDisabled =:= NodesWhereDisabled; - required when ProvidedBy =/= rabbit -> + hard when ProvidedBy =/= rabbit -> %% A plugin can be enabled/disabled at runtime and %% between restarts. Thus we have no way to %% distinguish a newly enabled plugin from a plugin @@ -1076,8 +1118,8 @@ check_required_and_enable( case MarkDirectly of false -> - case Stability of - required -> + case RequireLevel of + hard -> ?LOG_DEBUG( "Feature flags: `~s`: some nodes where the feature " "flag is disabled are not virgin, we need to perform " @@ -1445,6 +1487,26 @@ list_feature_flags_enabled_somewhere( end, #{}, StatesPerNode), lists:sort(maps:keys(MergedStates)). +list_soft_required_feature_flags( + #{feature_flags := FeatureFlags, states_per_node := StatesPerNode}) -> + FeatureStates = maps:get(node(), StatesPerNode), + RequiredFeatureNames = maps:fold( + fun(FeatureName, FeatureProps, Acc) -> + RequireLevel = ( + rabbit_feature_flags:get_require_level( + FeatureProps)), + IsEnabled = maps:get( + FeatureName, FeatureStates, + false), + case RequireLevel of + soft when IsEnabled =:= false -> + [FeatureName | Acc]; + _ -> + Acc + end + end, [], FeatureFlags), + lists:sort(RequiredFeatureNames). + -spec list_deprecated_features_that_cant_be_denied(Inventory) -> Ret when Inventory :: rabbit_feature_flags:cluster_inventory(), @@ -1517,7 +1579,7 @@ list_nodes_where_feature_flag_is_disabled( %% disabled. not Enabled; _ -> - %% The feature flags is unknown on this + %% The feature flag is unknown on this %% node, don't run the migration function. false end diff --git a/deps/rabbit/src/rabbit_ff_registry_factory.erl b/deps/rabbit/src/rabbit_ff_registry_factory.erl index a0197171efa9..28cf9f7bd6ed 100644 --- a/deps/rabbit/src/rabbit_ff_registry_factory.erl +++ b/deps/rabbit/src/rabbit_ff_registry_factory.erl @@ -261,26 +261,27 @@ maybe_initialize_registry(NewSupportedFeatureFlags, maps:map( fun (FeatureName, FeatureProps) when ?IS_FEATURE_FLAG(FeatureProps) -> - Stability = rabbit_feature_flags:get_stability(FeatureProps), + RequireLevel = ( + rabbit_feature_flags:get_require_level(FeatureProps)), ProvidedBy = maps:get(provided_by, FeatureProps), State = case FeatureStates0 of #{FeatureName := FeatureState} -> FeatureState; _ -> false end, - case Stability of - required when State =:= true -> + case RequireLevel of + hard when State =:= true -> %% The required feature flag is already enabled, we keep %% it this way. State; - required when NewNode -> + hard when NewNode -> %% This is the very first time the node starts, we %% already mark the required feature flag as enabled. ?assertNotEqual(state_changing, State), true; - required when ProvidedBy =/= rabbit -> + hard when ProvidedBy =/= rabbit -> ?assertNotEqual(state_changing, State), true; - required -> + hard -> %% This is not a new node and the required feature flag %% is disabled. This is an error and RabbitMQ must be %% downgraded to enable the feature flag. diff --git a/deps/rabbit/src/rabbit_prelaunch_feature_flags.erl b/deps/rabbit/src/rabbit_prelaunch_feature_flags.erl index cc8918a6b085..c5fa5f74845b 100644 --- a/deps/rabbit/src/rabbit_prelaunch_feature_flags.erl +++ b/deps/rabbit/src/rabbit_prelaunch_feature_flags.erl @@ -37,7 +37,9 @@ setup(#{feature_flags_file := FFFile}) -> "Failed to initialize feature flags registry: ~tp", [Reason], #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}), - throw({error, failed_to_initialize_feature_flags_registry}) + throw({error, + {failed_to_initialize_feature_flags_registry, + Reason}}) end; {error, Reason} -> ?LOG_ERROR( diff --git a/deps/rabbit/test/feature_flags_v2_SUITE.erl b/deps/rabbit/test/feature_flags_v2_SUITE.erl index 534c5cbdd651..ef009b4cfe9d 100644 --- a/deps/rabbit/test/feature_flags_v2_SUITE.erl +++ b/deps/rabbit/test/feature_flags_v2_SUITE.erl @@ -47,8 +47,9 @@ enable_feature_flag_in_cluster_and_remove_member_concurrently_mfv2/1, enable_feature_flag_with_post_enable/1, failed_enable_feature_flag_with_post_enable/1, - have_required_feature_flag_in_cluster_and_add_member_with_it_disabled/1, - have_required_feature_flag_in_cluster_and_add_member_without_it/1, + have_soft_required_feature_flag_in_cluster_and_add_member_with_it_disabled/1, + have_soft_required_feature_flag_in_cluster_and_add_member_without_it/1, + have_hard_required_feature_flag_in_cluster_and_add_member_without_it/1, have_unknown_feature_flag_in_cluster_and_add_member_with_it_enabled/1, error_during_migration_after_initial_success/1, controller_waits_for_own_task_to_finish_before_exiting/1, @@ -97,8 +98,9 @@ groups() -> enable_feature_flag_in_cluster_and_remove_member_concurrently_mfv2, enable_feature_flag_with_post_enable, failed_enable_feature_flag_with_post_enable, - have_required_feature_flag_in_cluster_and_add_member_with_it_disabled, - have_required_feature_flag_in_cluster_and_add_member_without_it, + have_soft_required_feature_flag_in_cluster_and_add_member_with_it_disabled, + have_soft_required_feature_flag_in_cluster_and_add_member_without_it, + have_hard_required_feature_flag_in_cluster_and_add_member_without_it, have_unknown_feature_flag_in_cluster_and_add_member_with_it_enabled, error_during_migration_after_initial_success, controller_waits_for_own_task_to_finish_before_exiting, @@ -1327,7 +1329,7 @@ failed_enable_feature_flag_with_post_enable(Config) -> ok. -have_required_feature_flag_in_cluster_and_add_member_with_it_disabled( +have_soft_required_feature_flag_in_cluster_and_add_member_with_it_disabled( Config) -> AllNodes = [NewNode | [FirstNode | _] = Nodes] = ?config(nodes, Config), connect_nodes(Nodes), @@ -1410,7 +1412,7 @@ have_required_feature_flag_in_cluster_and_add_member_with_it_disabled( || Node <- AllNodes], ok. -have_required_feature_flag_in_cluster_and_add_member_without_it( +have_soft_required_feature_flag_in_cluster_and_add_member_without_it( Config) -> AllNodes = [NewNode | [FirstNode | _] = Nodes] = ?config(nodes, Config), connect_nodes(Nodes), @@ -1427,6 +1429,98 @@ have_required_feature_flag_in_cluster_and_add_member_without_it( ?assertEqual(ok, inject_on_nodes([NewNode], FeatureFlags)), ?assertEqual(ok, inject_on_nodes(Nodes, RequiredFeatureFlags)), + ct:pal( + "Checking the feature flag is supported and enabled on existing the " + "cluster only"), + ok = run_on_node( + NewNode, + fun() -> + ?assert(rabbit_feature_flags:is_supported(FeatureName)), + ?assertNot(rabbit_feature_flags:is_enabled(FeatureName)), + + DBDir = rabbit_db:dir(), + ok = filelib:ensure_path(DBDir), + SomeFile = filename:join(DBDir, "some-file.db"), + ok = file:write_file(SomeFile, <<>>), + ?assertNot(rabbit_db:is_virgin_node()), + ok + end, + []), + _ = [ok = + run_on_node( + Node, + fun() -> + ?assert(rabbit_feature_flags:is_supported(FeatureName)), + ?assert(rabbit_feature_flags:is_enabled(FeatureName)), + ok + end, + []) + || Node <- Nodes], + + %% Check compatibility between NewNodes and Nodes. + ok = run_on_node( + NewNode, + fun() -> + ?assertEqual( + ok, + rabbit_feature_flags:check_node_compatibility( + FirstNode)), + ok + end, []), + + %% Add node to cluster and synchronize feature flags. + connect_nodes(AllNodes), + override_running_nodes(AllNodes), + ct:pal( + "Synchronizing feature flags in the expanded cluster~n" + "~n" + "NOTE: Error messages about crashed migration functions can be " + "ignored for feature~n" + " flags other than `~ts`~n" + " because they assume they run inside RabbitMQ.", + [FeatureName]), + ok = run_on_node( + NewNode, + fun() -> + ?assertEqual( + ok, + rabbit_feature_flags:sync_feature_flags_with_cluster( + Nodes, false)), + ok + end, []), + + ct:pal("Checking the feature flag state is unchanged"), + _ = [ok = + run_on_node( + Node, + fun() -> + ?assertEqual( + true, + rabbit_feature_flags:is_enabled(FeatureName)), + ok + end, + []) + || Node <- AllNodes], + ok. + +have_hard_required_feature_flag_in_cluster_and_add_member_without_it( + Config) -> + AllNodes = [NewNode | [FirstNode | _] = Nodes] = ?config(nodes, Config), + connect_nodes(Nodes), + override_running_nodes([NewNode]), + override_running_nodes(Nodes), + + FeatureName = ?FUNCTION_NAME, + FeatureFlags = #{FeatureName => + #{provided_by => rabbit, + stability => stable}}, + RequiredFeatureFlags = #{FeatureName => + #{provided_by => rabbit, + stability => required, + require_level => hard}}, + ?assertEqual(ok, inject_on_nodes([NewNode], FeatureFlags)), + ?assertEqual(ok, inject_on_nodes(Nodes, RequiredFeatureFlags)), + ct:pal( "Checking the feature flag is supported and enabled on existing the " "cluster only"), diff --git a/deps/rabbitmq_management_agent/src/rabbit_mgmt_ff.erl b/deps/rabbitmq_management_agent/src/rabbit_mgmt_ff.erl index 5022adc020b3..65c562f35530 100644 --- a/deps/rabbitmq_management_agent/src/rabbit_mgmt_ff.erl +++ b/deps/rabbitmq_management_agent/src/rabbit_mgmt_ff.erl @@ -10,11 +10,13 @@ -rabbit_feature_flag( {empty_basic_get_metric, #{desc => "Count AMQP `basic.get` on empty queues in stats", - stability => required + stability => required, + require_level => hard }}). -rabbit_feature_flag( - {drop_unroutable_metric, - #{desc => "Count unroutable publishes to be dropped in stats", - stability => required - }}). + {drop_unroutable_metric, + #{desc => "Count unroutable publishes to be dropped in stats", + stability => required, + require_level => hard + }}). diff --git a/deps/rabbitmq_mqtt/src/rabbit_mqtt_ff.erl b/deps/rabbitmq_mqtt/src/rabbit_mqtt_ff.erl index 3b35c794af39..67dc19b87891 100644 --- a/deps/rabbitmq_mqtt/src/rabbit_mqtt_ff.erl +++ b/deps/rabbitmq_mqtt/src/rabbit_mqtt_ff.erl @@ -16,13 +16,15 @@ -rabbit_feature_flag( {?QUEUE_TYPE_QOS_0, #{desc => "Support pseudo queue type for MQTT QoS 0 subscribers omitting a queue process", - stability => required + stability => required, + require_level => hard }}). -rabbit_feature_flag( {delete_ra_cluster_mqtt_node, #{desc => "Delete Ra cluster 'mqtt_node' since MQTT client IDs are tracked locally", - stability => required + stability => required, + require_level => hard }}). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -38,6 +40,7 @@ {mqtt_v5, #{desc => "Support MQTT 5.0", stability => required, + require_level => hard, depends_on => [ %% MQTT 5.0 feature Will Delay Interval depends on client ID tracking in pg local. delete_ra_cluster_mqtt_node,