Skip to content

Commit a4e8cdd

Browse files
committed
rabbit_feature_flags: Support required feature flags in plugins
[Why] If a plugin was already enabled when RabbitMQ starts, its required feature flags were correctly handled and thus enabled. However, this was not the case for a plugin enabled at runtime. Here is an example with the `drop_unroutable_metric` from the rabbitmq_management_agent plugin: Feature flags: `drop_unroutable_metric`: required feature flag not enabled! It must be enabled before upgrading RabbitMQ. Supporting required feature flags in plugin is trickier than in the core broker. Indeed, with the broker, we know when this is the first time the broker is started. Therefore we are sure that a required feature flag can be enabled directly, there is no existing data/context that could conflict with the code behind the required feature flag. For plugins, this is different: a plugin can be enabled/disabled at runtime and between broker restarts (and thus upgrades). So, when a plugin is enabled and it has a required feature flag, we have no way to make sure that there is no existing and conflicting data/context. [How] In this patch, if the required feature flag is provided by a plugin (i.e. not `rabbit`), we always mark it as enabled. The plugin is responsible for handling any existing data/context and perform any cleanup/conversion. Reported by: @ansd
1 parent 2dc45e8 commit a4e8cdd

File tree

5 files changed

+162
-53
lines changed

5 files changed

+162
-53
lines changed

deps/rabbit/src/rabbit_ff_controller.erl

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -747,11 +747,12 @@ check_required_and_enable(
747747
%% Required feature flags vs. virgin nodes.
748748
FeatureProps = maps:get(FeatureName, FeatureFlags),
749749
Stability = rabbit_feature_flags:get_stability(FeatureProps),
750+
ProvidedBy = maps:get(provided_by, FeatureProps),
750751
NodesWhereDisabled = list_nodes_where_feature_flag_is_disabled(
751752
Inventory, FeatureName),
752753

753754
MarkDirectly = case Stability of
754-
required ->
755+
required when ProvidedBy =:= rabbit ->
755756
?LOG_DEBUG(
756757
"Feature flags: `~s`: the feature flag is "
757758
"required on some nodes; list virgin nodes "
@@ -770,6 +771,25 @@ check_required_and_enable(
770771
end
771772
end, NodesWhereDisabled),
772773
VirginNodesWhereDisabled =:= NodesWhereDisabled;
774+
required when ProvidedBy =/= rabbit ->
775+
%% A plugin can be enabled/disabled at runtime and
776+
%% between restarts. Thus we have no way to
777+
%% distinguish a newly enabled plugin from a plugin
778+
%% which was enabled in the past.
779+
%%
780+
%% Therefore, we always mark required feature flags
781+
%% from plugins directly as enabled. However, the
782+
%% plugin is responsible for checking that its
783+
%% possibly existing data is as it expects it or
784+
%% perform any cleanup/conversion!
785+
?LOG_DEBUG(
786+
"Feature flags: `~s`: the feature flag is "
787+
"required on some nodes; it comes from a "
788+
"plugin which can be enabled at runtime, "
789+
"so it can be marked as enabled",
790+
[FeatureName],
791+
#{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS}),
792+
true;
773793
_ ->
774794
false
775795
end,

deps/rabbit/src/rabbit_ff_registry_factory.erl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ maybe_initialize_registry(NewSupportedFeatureFlags,
247247
fun(FeatureName, FeatureProps) ->
248248
Stability = maps:get(
249249
stability, FeatureProps, stable),
250+
ProvidedBy = maps:get(
251+
provided_by, FeatureProps),
250252
State = case FeatureStates0 of
251253
#{FeatureName := FeatureState} ->
252254
FeatureState;
@@ -264,6 +266,9 @@ maybe_initialize_registry(NewSupportedFeatureFlags,
264266
%% feature flag as enabled.
265267
?assertNotEqual(state_changing, State),
266268
true;
269+
required when ProvidedBy =/= rabbit ->
270+
?assertNotEqual(state_changing, State),
271+
true;
267272
required ->
268273
%% This is not a new node and the
269274
%% required feature flag is disabled.

deps/rabbit/test/feature_flags_SUITE.erl

Lines changed: 122 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
enable_feature_flag_with_a_network_partition/1,
3232
mark_feature_flag_as_enabled_with_a_network_partition/1,
3333
required_feature_flag_enabled_by_default/1,
34+
required_plugin_feature_flag_enabled_by_default/1,
35+
required_plugin_feature_flag_enabled_after_activation/1,
3436
plugin_stable_ff_enabled_on_initial_node_start/1,
3537

3638
clustering_ok_with_ff_disabled_everywhere/1,
@@ -62,6 +64,8 @@ groups() ->
6264
enable_feature_flag_in_a_healthy_situation,
6365
enable_unsupported_feature_flag_in_a_healthy_situation,
6466
required_feature_flag_enabled_by_default,
67+
required_plugin_feature_flag_enabled_by_default,
68+
required_plugin_feature_flag_enabled_after_activation,
6569
plugin_stable_ff_enabled_on_initial_node_start
6670
]},
6771
{enabling_in_cluster, [],
@@ -70,7 +74,9 @@ groups() ->
7074
enable_unsupported_feature_flag_in_a_healthy_situation,
7175
enable_feature_flag_with_a_network_partition,
7276
mark_feature_flag_as_enabled_with_a_network_partition,
73-
required_feature_flag_enabled_by_default
77+
required_feature_flag_enabled_by_default,
78+
required_plugin_feature_flag_enabled_by_default,
79+
required_plugin_feature_flag_enabled_after_activation
7480
]},
7581
{clustering, [],
7682
[
@@ -168,9 +174,10 @@ init_per_group(enabling_on_single_node, Config) ->
168174
[{rmq_nodes_count, 1}]),
169175
rabbit_ct_helpers:run_setup_steps(Config1, [fun prepare_my_plugin/1]);
170176
init_per_group(enabling_in_cluster, Config) ->
171-
rabbit_ct_helpers:set_config(
172-
Config,
173-
[{rmq_nodes_count, 5}]);
177+
Config1 = rabbit_ct_helpers:set_config(
178+
Config,
179+
[{rmq_nodes_count, 5}]),
180+
rabbit_ct_helpers:run_setup_steps(Config1, [fun prepare_my_plugin/1]);
174181
init_per_group(clustering, Config) ->
175182
Config1 = rabbit_ct_helpers:set_config(
176183
Config,
@@ -194,81 +201,89 @@ end_per_group(_, Config) ->
194201
init_per_testcase(Testcase, Config) ->
195202
rabbit_ct_helpers:testcase_started(Config, Testcase),
196203
TestNumber = rabbit_ct_helpers:testcase_number(Config, ?MODULE, Testcase),
197-
case ?config(tc_group_properties, Config) of
204+
Config1 = case Testcase of
205+
required_plugin_feature_flag_enabled_after_activation ->
206+
rabbit_ct_helpers:set_config(
207+
Config,
208+
[{start_rmq_with_plugins_disabled, true}]);
209+
_ ->
210+
Config
211+
end,
212+
case ?config(tc_group_properties, Config1) of
198213
[{name, registry} | _] ->
199214
logger:set_primary_config(level, debug),
200-
FeatureFlagsFile = filename:join(?config(priv_dir, Config),
215+
FeatureFlagsFile = filename:join(?config(priv_dir, Config1),
201216
rabbit_misc:format(
202217
"feature_flags-~ts",
203218
[Testcase])),
204219
application:set_env(rabbit, feature_flags_file, FeatureFlagsFile),
205220
rabbit_ct_helpers:set_config(
206-
Config, {feature_flags_file, FeatureFlagsFile});
221+
Config1, {feature_flags_file, FeatureFlagsFile});
207222
[{name, Name} | _]
208223
when Name =:= enabling_on_single_node orelse
209224
Name =:= clustering orelse
210225
Name =:= activating_plugin ->
211-
ClusterSize = ?config(rmq_nodes_count, Config),
212-
Config1 = rabbit_ct_helpers:set_config(
213-
Config,
226+
ClusterSize = ?config(rmq_nodes_count, Config1),
227+
Config2 = rabbit_ct_helpers:set_config(
228+
Config1,
214229
[{rmq_nodename_suffix, Testcase},
215230
{tcp_ports_base, {skip_n_nodes,
216231
TestNumber * ClusterSize}}
217232
]),
218-
Config2 = rabbit_ct_helpers:merge_app_env(
219-
Config1,
233+
Config3 = rabbit_ct_helpers:merge_app_env(
234+
Config2,
220235
{rabbit,
221236
[{log, [{file, [{level, debug}]}]}]}),
222-
Config3 = rabbit_ct_helpers:run_steps(
223-
Config2,
237+
Config4 = rabbit_ct_helpers:run_steps(
238+
Config3,
224239
rabbit_ct_broker_helpers:setup_steps() ++
225240
rabbit_ct_client_helpers:setup_steps()),
226-
case Config3 of
241+
case Config4 of
227242
{skip, _} ->
228-
Config3;
243+
Config4;
229244
_ ->
230-
case is_feature_flag_subsystem_available(Config3) of
245+
case is_feature_flag_subsystem_available(Config4) of
231246
true ->
232247
%% We can declare a new feature flag at
233248
%% runtime. All of them are supported but
234249
%% still disabled.
235-
declare_arbitrary_feature_flag(Config3),
236-
Config3;
250+
declare_arbitrary_feature_flag(Config4),
251+
Config4;
237252
false ->
238-
end_per_testcase(Testcase, Config3),
253+
end_per_testcase(Testcase, Config4),
239254
{skip, "Feature flags subsystem unavailable"}
240255
end
241256
end;
242257
[{name, enabling_in_cluster} | _] ->
243-
ClusterSize = ?config(rmq_nodes_count, Config),
244-
Config1 = rabbit_ct_helpers:set_config(
245-
Config,
258+
ClusterSize = ?config(rmq_nodes_count, Config1),
259+
Config2 = rabbit_ct_helpers:set_config(
260+
Config1,
246261
[{rmq_nodename_suffix, Testcase},
247262
{tcp_ports_base, {skip_n_nodes,
248263
TestNumber * ClusterSize}},
249264
{net_ticktime, 5}
250265
]),
251-
Config2 = rabbit_ct_helpers:merge_app_env(
252-
Config1,
266+
Config3 = rabbit_ct_helpers:merge_app_env(
267+
Config2,
253268
{rabbit,
254269
[{log, [{file, [{level, debug}]}]}]}),
255-
Config3 = rabbit_ct_helpers:run_steps(
256-
Config2,
270+
Config4 = rabbit_ct_helpers:run_steps(
271+
Config3,
257272
rabbit_ct_broker_helpers:setup_steps() ++
258273
rabbit_ct_client_helpers:setup_steps()),
259-
case Config3 of
274+
case Config4 of
260275
{skip, _} ->
261-
Config3;
276+
Config4;
262277
_ ->
263-
case is_feature_flag_subsystem_available(Config3) of
278+
case is_feature_flag_subsystem_available(Config4) of
264279
true ->
265280
%% We can declare a new feature flag at
266281
%% runtime. All of them are supported but
267282
%% still disabled.
268-
declare_arbitrary_feature_flag(Config3),
269-
Config3;
283+
declare_arbitrary_feature_flag(Config4),
284+
Config4;
270285
false ->
271-
end_per_testcase(Testcase, Config3),
286+
end_per_testcase(Testcase, Config4),
272287
{skip, "Feature flags subsystem unavailable"}
273288
end
274289
end
@@ -996,14 +1011,6 @@ required_feature_flag_enabled_by_default(Config) ->
9961011
True = lists:duplicate(ClusterSize, true),
9971012
False = lists:duplicate(ClusterSize, false),
9981013

999-
%% Restart the first node to make sure it evaluates again the list of
1000-
%% required flags. Indeed, the testsuite injects its required feature
1001-
%% flag after the nodes booted and we don't want to verify a
1002-
%% testsuite-specific handling.
1003-
%[Node | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
1004-
%?assertEqual(ok, rabbit_ct_broker_helpers:stop_node(Config, Node)),
1005-
%?assertEqual(ok, rabbit_ct_broker_helpers:start_node(Config, Node)),
1006-
10071014
%% The stable feature flag is supported but disabled.
10081015
?assertEqual(
10091016
True,
@@ -1020,6 +1027,80 @@ required_feature_flag_enabled_by_default(Config) ->
10201027
True,
10211028
is_feature_flag_enabled(Config, RequiredFName)).
10221029

1030+
required_plugin_feature_flag_enabled_by_default(Config) ->
1031+
StableFName = plugin_ff,
1032+
RequiredFName = required_plugin_ff,
1033+
ClusterSize = ?config(rmq_nodes_count, Config),
1034+
True = lists:duplicate(ClusterSize, true),
1035+
1036+
%% The stable feature flag is supported and enabled because the plugin is
1037+
%% enabled during startup.
1038+
?assertEqual(
1039+
True,
1040+
is_feature_flag_supported(Config, StableFName)),
1041+
?assertEqual(
1042+
True,
1043+
is_feature_flag_enabled(Config, StableFName)),
1044+
1045+
%% Likewise for the required feature flag.
1046+
?assertEqual(
1047+
True,
1048+
is_feature_flag_supported(Config, RequiredFName)),
1049+
?assertEqual(
1050+
True,
1051+
is_feature_flag_enabled(Config, RequiredFName)).
1052+
1053+
required_plugin_feature_flag_enabled_after_activation(Config) ->
1054+
StableFName = plugin_ff,
1055+
RequiredFName = required_plugin_ff,
1056+
ClusterSize = ?config(rmq_nodes_count, Config),
1057+
True = lists:duplicate(ClusterSize, true),
1058+
False = lists:duplicate(ClusterSize, false),
1059+
1060+
?assertEqual(true, ?config(start_rmq_with_plugins_disabled, Config)),
1061+
1062+
%% The stable feature flag is unsupported.
1063+
?assertEqual(
1064+
False,
1065+
is_feature_flag_supported(Config, StableFName)),
1066+
?assertEqual(
1067+
False,
1068+
is_feature_flag_enabled(Config, StableFName)),
1069+
1070+
%% The required feature flag is unsupported.
1071+
?assertEqual(
1072+
False,
1073+
is_feature_flag_supported(Config, RequiredFName)),
1074+
?assertEqual(
1075+
False,
1076+
is_feature_flag_enabled(Config, RequiredFName)),
1077+
1078+
rabbit_ct_broker_helpers:enable_plugin(Config, 0, "my_plugin"),
1079+
1080+
LastI = ClusterSize - 1,
1081+
rabbit_ct_broker_helpers:enable_plugin(Config, LastI, "my_plugin"),
1082+
TrueWhereEnabled = lists:map(
1083+
fun
1084+
(I) when I =:= 0 orelse I =:= LastI -> true;
1085+
(_) -> false
1086+
end, lists:seq(0, LastI)),
1087+
1088+
%% The stable feature flag is supported but disabled.
1089+
?assertEqual(
1090+
True,
1091+
is_feature_flag_supported(Config, StableFName)),
1092+
?assertEqual(
1093+
False,
1094+
is_feature_flag_enabled(Config, StableFName)),
1095+
1096+
%% The required feature flag is supported and enabled.
1097+
?assertEqual(
1098+
True,
1099+
is_feature_flag_supported(Config, RequiredFName)),
1100+
?assertEqual(
1101+
TrueWhereEnabled,
1102+
is_feature_flag_enabled(Config, RequiredFName)).
1103+
10231104
plugin_stable_ff_enabled_on_initial_node_start(Config) ->
10241105
?assertEqual([true], is_feature_flag_supported(Config, plugin_ff)),
10251106
?assertEqual([true], is_feature_flag_enabled(Config, plugin_ff)),

deps/rabbit/test/feature_flags_SUITE_data/my_plugin/src/my_plugin.erl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@
88
-module(my_plugin).
99

1010
-rabbit_feature_flag({plugin_ff, #{desc => "Plugin's feature flag A"}}).
11+
12+
-rabbit_feature_flag({required_plugin_ff, #{desc => "Plugin's feature flag B",
13+
stability => required}}).

0 commit comments

Comments
 (0)