Skip to content

Commit 6a0008b

Browse files
committed
rabbit_feature_flags: Accept "+feature1,-feature2" in $RABBITMQ_FEATURE_FLAGS
[Why] Before this patch, the $RABBITMQ_FEATURE_FLAGS environment variable took an exhaustive list of feature flags to enable. This list overrode the default of enabling all stable feature flags. It made it inconvenient when a user wanted to enable an experimental feature flag like `khepri_db` while still leaving the default behavior. [How] $RABBITMQ_FEATURE_FLAGS now acceps the following syntax: RABBITMQ_FEATURE_FLAGS=+feature1,-feature2 This will start RabbitMQ with all stable feature flags, plus `feature1`, but without `feature2`. For users setting `forced_feature_flags_on_init` in the config, the corresponding syntax is: {forced_feature_flags_on_init, {rel, [feature1], [feature2]}}
1 parent 3540541 commit 6a0008b

File tree

2 files changed

+116
-65
lines changed

2 files changed

+116
-65
lines changed

deps/rabbit/src/rabbit_ff_controller.erl

Lines changed: 108 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -675,48 +675,83 @@ enable_task(FeatureNames) ->
675675
end.
676676

677677
enable_default_task() ->
678-
FeatureNames = get_forced_feature_flag_names(),
679-
case FeatureNames of
680-
undefined ->
678+
case get_forced_feature_flag_names() of
679+
{ok, undefined} ->
681680
?LOG_DEBUG(
682681
"Feature flags: starting an unclustered node for the first "
683682
"time: all stable feature flags will be enabled by default",
684683
#{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS}),
685684
{ok, Inventory} = collect_inventory_on_nodes([node()]),
686-
#{feature_flags := FeatureFlags} = Inventory,
687-
StableFeatureNames =
688-
maps:fold(
689-
fun(FeatureName, FeatureProps, Acc) ->
690-
Stability = rabbit_feature_flags:get_stability(
691-
FeatureProps),
692-
case Stability of
693-
stable -> [FeatureName | Acc];
694-
_ -> Acc
695-
end
696-
end, [], FeatureFlags),
685+
StableFeatureNames = get_stable_feature_flags(Inventory),
697686
enable_many(Inventory, StableFeatureNames);
698-
[] ->
687+
{ok, []} ->
699688
?LOG_DEBUG(
700689
"Feature flags: starting an unclustered node for the first "
701690
"time: all feature flags are forcibly left disabled from "
702691
"the $RABBITMQ_FEATURE_FLAGS environment variable",
703692
#{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS}),
704693
ok;
705-
_ ->
694+
{ok, FeatureNames} when is_list(FeatureNames) ->
706695
?LOG_DEBUG(
707696
"Feature flags: starting an unclustered node for the first "
708697
"time: only the following feature flags specified in the "
709698
"$RABBITMQ_FEATURE_FLAGS environment variable will be enabled: "
710-
"~tp",
699+
"~0tp",
711700
[FeatureNames],
712701
#{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS}),
713702
{ok, Inventory} = collect_inventory_on_nodes([node()]),
714-
enable_many(Inventory, FeatureNames)
703+
enable_many(Inventory, FeatureNames);
704+
{ok, {rel, Plus, Minus}} ->
705+
?LOG_DEBUG(
706+
"Feature flags: starting an unclustered node for the first "
707+
"time: all stable feature flags will be enabled, after "
708+
"applying changes from $RABBITMQ_FEATURE_FLAGS: adding ~0tp, "
709+
"skipping ~0tp",
710+
[Plus, Minus],
711+
#{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS}),
712+
{ok, Inventory} = collect_inventory_on_nodes([node()]),
713+
StableFeatureNames = get_stable_feature_flags(Inventory),
714+
Unsupported = lists:filter(
715+
fun(FeatureName) ->
716+
not is_known_and_supported(
717+
Inventory, FeatureName)
718+
end, Minus),
719+
case Unsupported of
720+
[] ->
721+
FeatureNames = (StableFeatureNames -- Minus) ++ Plus,
722+
enable_many(Inventory, FeatureNames);
723+
_ ->
724+
?LOG_ERROR(
725+
"Feature flags: unsupported feature flags to skip in "
726+
"$RABBITMQ_FEATURE_FLAGS: ~0tp",
727+
[Unsupported],
728+
#{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS}),
729+
{error, unsupported}
730+
end;
731+
{error, syntax_error_in_envvar} = Error ->
732+
?LOG_DEBUG(
733+
"Feature flags: invalid mix of `feature_flag` and "
734+
"`+/-feature_flag` in $RABBITMQ_FEATURE_FLAGS",
735+
#{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS}),
736+
Error
715737
end.
716738

739+
get_stable_feature_flags(#{feature_flags := FeatureFlags}) ->
740+
maps:fold(
741+
fun(FeatureName, FeatureProps, Acc) ->
742+
Stability = rabbit_feature_flags:get_stability(FeatureProps),
743+
case Stability of
744+
stable -> [FeatureName | Acc];
745+
_ -> Acc
746+
end
747+
end, [], FeatureFlags).
748+
717749
-spec get_forced_feature_flag_names() -> Ret when
718-
Ret :: FeatureNames | undefined,
719-
FeatureNames :: [rabbit_feature_flags:feature_name()].
750+
Ret :: {ok, Abs | Rel | undefined} | {error, syntax_error_in_envvar},
751+
Abs :: [rabbit_feature_flags:feature_name()],
752+
Rel :: {rel,
753+
[rabbit_feature_flags:feature_name()],
754+
[rabbit_feature_flags:feature_name()]}.
720755
%% @doc Returns the (possibly empty) list of feature flags the user wants to
721756
%% enable out-of-the-box when starting a node for the first time.
722757
%%
@@ -737,59 +772,73 @@ enable_default_task() ->
737772
%% @private
738773

739774
get_forced_feature_flag_names() ->
740-
Ret = case get_forced_feature_flag_names_from_env() of
741-
undefined -> get_forced_feature_flag_names_from_config();
742-
List -> List
743-
end,
744-
case Ret of
745-
undefined ->
746-
ok;
747-
[] ->
748-
?LOG_INFO(
749-
"Feature flags: automatic enablement of feature flags "
750-
"disabled (i.e. none will be enabled automatically)",
751-
#{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS});
752-
_ ->
753-
?LOG_INFO(
754-
"Feature flags: automatic enablement of feature flags "
755-
"limited to the following list: ~tp",
756-
[Ret],
757-
#{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS})
758-
end,
759-
Ret.
775+
case get_forced_feature_flag_names_from_env() of
776+
{ok, undefined} -> get_forced_feature_flag_names_from_config();
777+
{ok, _} = Ret -> Ret;
778+
{error, _} = Error -> Error
779+
end.
760780

761781
-spec get_forced_feature_flag_names_from_env() -> Ret when
762-
Ret :: FeatureNames | undefined,
763-
FeatureNames :: [rabbit_feature_flags:feature_name()].
782+
Ret :: {ok, Abs | Rel | undefined} | {error, syntax_error_in_envvar},
783+
Abs :: [rabbit_feature_flags:feature_name()],
784+
Rel :: {rel,
785+
[rabbit_feature_flags:feature_name()],
786+
[rabbit_feature_flags:feature_name()]}.
764787
%% @private
765788

766789
get_forced_feature_flag_names_from_env() ->
767-
case rabbit_prelaunch:get_context() of
768-
#{forced_feature_flags_on_init := ForcedFFs}
769-
when is_list(ForcedFFs) ->
770-
ForcedFFs;
771-
_ ->
772-
undefined
790+
Value = case rabbit_prelaunch:get_context() of
791+
#{forced_feature_flags_on_init := ForcedFFs} -> ForcedFFs;
792+
_ -> undefined
793+
end,
794+
case Value of
795+
undefined ->
796+
{ok, Value};
797+
[] ->
798+
{ok, Value};
799+
[[Op | _] | _] when Op =:= $+ orelse Op =:= $- ->
800+
lists:foldr(
801+
fun
802+
([$+ | NameS], {ok, {rel, Plus, Minus}}) ->
803+
Name = list_to_atom(NameS),
804+
Plus1 = [Name | Plus],
805+
{ok, {rel, Plus1, Minus}};
806+
([$- | NameS], {ok, {rel, Plus, Minus}}) ->
807+
Name = list_to_atom(NameS),
808+
Minus1 = [Name | Minus],
809+
{ok, {rel, Plus, Minus1}};
810+
(_, {error, _} = Error) ->
811+
Error;
812+
(_, _) ->
813+
{error, syntax_error_in_envvar}
814+
end, {ok, {rel, [], []}}, Value);
815+
_ when is_list(Value) ->
816+
lists:foldr(
817+
fun
818+
(Name, {ok, Abs}) when is_atom(Name) ->
819+
{ok, [Name | Abs]};
820+
([C | _] = NameS, {ok, Abs})
821+
when C =/= $+ andalso C =/= $- ->
822+
Name = list_to_atom(NameS),
823+
{ok, [Name | Abs]};
824+
(_, {error, _} = Error) ->
825+
Error;
826+
(_, _) ->
827+
{error, syntax_error_in_envvar}
828+
end, {ok, []}, Value)
773829
end.
774830

775831
-spec get_forced_feature_flag_names_from_config() -> Ret when
776-
Ret :: FeatureNames | undefined,
832+
Ret :: {ok, FeatureNames | undefined},
777833
FeatureNames :: [rabbit_feature_flags:feature_name()].
778834
%% @private
779835

780836
get_forced_feature_flag_names_from_config() ->
781837
Value = application:get_env(
782838
rabbit, forced_feature_flags_on_init, undefined),
783839
case Value of
784-
undefined ->
785-
Value;
786-
_ when is_list(Value) ->
787-
case lists:all(fun is_atom/1, Value) of
788-
true -> Value;
789-
false -> undefined
790-
end;
791-
_ ->
792-
undefined
840+
undefined -> {ok, Value};
841+
_ when is_list(Value) -> {ok, Value}
793842
end.
794843

795844
-spec sync_cluster_task() -> Ret when
@@ -914,7 +963,7 @@ enable_if_supported(#{states_per_node := _} = Inventory, FeatureName) ->
914963
#{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS}),
915964
enable_with_registry_locked(Inventory, FeatureName);
916965
false ->
917-
?LOG_DEBUG(
966+
?LOG_ERROR(
918967
"Feature flags: `~ts`: unsupported; aborting",
919968
[FeatureName],
920969
#{domain => ?RMQLOG_DOMAIN_FEAT_FLAGS}),

deps/rabbit_common/src/rabbit_env.erl

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -999,13 +999,15 @@ forced_feature_flags_on_init(Context) ->
999999
case Value of
10001000
false ->
10011001
%% get_prefixed_env_var() considers an empty string
1002-
%% is the same as an undefined environment variable.
1003-
update_context(Context,
1004-
forced_feature_flags_on_init, undefined, default);
1002+
%% as an undefined environment variable.
1003+
update_context(
1004+
Context,
1005+
forced_feature_flags_on_init, undefined, default);
10051006
_ ->
1006-
Flags = [list_to_atom(V) || V <- string:lexemes(Value, ",")],
1007-
update_context(Context,
1008-
forced_feature_flags_on_init, Flags, environment)
1007+
FeatureNames = string:lexemes(Value, ","),
1008+
update_context(
1009+
Context,
1010+
forced_feature_flags_on_init, FeatureNames, environment)
10091011
end.
10101012

10111013
log_feature_flags_registry(Context) ->

0 commit comments

Comments
 (0)