diff --git a/deps/rabbit/src/rabbit_core_ff.erl b/deps/rabbit/src/rabbit_core_ff.erl index c83548030829..0c0cb3e17da2 100644 --- a/deps/rabbit/src/rabbit_core_ff.erl +++ b/deps/rabbit/src/rabbit_core_ff.erl @@ -146,9 +146,10 @@ -rabbit_feature_flag( {khepri_db, - #{desc => "New Raft-based metadata store. Fully supported as of RabbitMQ 4.0", + #{desc => "New Raft-based metadata store.", doc_url => "https://www.rabbitmq.com/docs/next/metadata-store", stability => experimental, + experiment_level => supported, depends_on => [feature_flags_v2, direct_exchange_routing_v2, maintenance_mode_status, diff --git a/deps/rabbit/src/rabbit_feature_flags.erl b/deps/rabbit/src/rabbit_feature_flags.erl index d50e30375c81..8425dafa4cef 100644 --- a/deps/rabbit/src/rabbit_feature_flags.erl +++ b/deps/rabbit/src/rabbit_feature_flags.erl @@ -106,6 +106,7 @@ get_state/1, get_stability/1, get_require_level/1, + get_experiment_level/1, check_node_compatibility/1, check_node_compatibility/2, sync_feature_flags_with_cluster/2, refresh_feature_flags_after_app_load/0, @@ -149,6 +150,7 @@ doc_url => string(), stability => stability(), require_level => require_level(), + experiment_level => experiment_level(), depends_on => [feature_name()], callbacks => #{callback_name() => callback_fun_name()}}. @@ -186,6 +188,7 @@ doc_url => string(), stability => stability(), require_level => require_level(), + experiment_level => experiment_level(), depends_on => [feature_name()], callbacks => #{callback_name() => callback_fun_name()}, @@ -219,6 +222,24 @@ %% A soft required feature flag will be automatically enabled when a RabbitMQ %% node is upgraded to a version where it is required. +-type experiment_level() :: unsupported | supported. +%% The level of support of an experimental feature flag. +%% +%% At first, an experimental feature flag is offered to give a chance to users +%% to try it and give feedback as part of the design and development of the +%% feature. At this stage, it is unsupported: it must not be enabled in a +%% production environment and upgrade to a later version of RabbitMQ while +%% this experimental feature flag is enabled is not supported. +%% +%% Then, the experimental feature flag becomes supported. At this point, it is +%% stable enough that upgrading is guarantied and help will be provided. +%% However it is not mature enough to be marked as stable (which would make it +%% enabled by default in a new deployment or when running `rabbitmqctl +%% enable_feature_flag all'. +%% +%% The next step is to change its stability to `stable'. Once done, the +%% `experiment_level()' field is irrelevant. + -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. @@ -327,6 +348,8 @@ feature_state/0, feature_states/0, stability/0, + require_level/0, + experiment_level/0, callback_fun_name/0, callbacks/0, callback_name/0, @@ -696,13 +719,17 @@ info() -> info(Options) when is_map(Options) -> rabbit_ff_extra:info(Options). --spec get_state(feature_name()) -> enabled | disabled | unavailable. +-spec get_state(feature_name()) -> enabled | + state_changing | + disabled | + unavailable. %% @doc %% Returns the state of a feature flag. %% %% The possible states are: %% %% %% @param FeatureName The name of the feature flag to check. -%% @returns `enabled', `disabled' or `unavailable'. +%% @returns `enabled', `state_changing', `disabled' or `unavailable'. get_state(FeatureName) when is_atom(FeatureName) -> - IsEnabled = is_enabled(FeatureName), + IsEnabled = is_enabled(FeatureName, non_blocking), case IsEnabled of - true -> enabled; - false -> case is_supported(FeatureName) of - true -> disabled; - false -> unavailable - end + true -> + enabled; + state_changing -> + state_changing; + false -> + case is_supported(FeatureName) of + true -> disabled; + false -> unavailable + end end. -spec get_stability @@ -809,6 +840,45 @@ get_require_level(FeatureProps) when ?IS_DEPRECATION(FeatureProps) -> _ -> none end. +-spec get_experiment_level +(FeatureName) -> ExperimentLevel | undefined when + FeatureName :: feature_name(), + ExperimentLevel :: experiment_level() | none; +(FeatureProps) -> ExperimentLevel when + FeatureProps :: + feature_props_extended() | + rabbit_deprecated_features:feature_props_extended(), + ExperimentLevel :: experiment_level() | none. +%% @doc +%% Returns the experimental level of an experimental feature flag. +%% +%% The possible experiment levels are: +%% +%% +%% @param FeatureName The name of the feature flag to check. +%% @param FeatureProps A feature flag properties map. +%% @returns `unsupported', `supported', or `undefined' if the given feature +%% flag name doesn't correspond to a known feature flag. + +get_experiment_level(FeatureName) when is_atom(FeatureName) -> + case rabbit_ff_registry_wrapper:get(FeatureName) of + undefined -> undefined; + FeatureProps -> get_experiment_level(FeatureProps) + end; +get_experiment_level(FeatureProps) when ?IS_FEATURE_FLAG(FeatureProps) -> + case get_stability(FeatureProps) of + experimental -> maps:get(experiment_level, FeatureProps, unsupported); + _ -> supported + end; +get_experiment_level(FeatureProps) when ?IS_DEPRECATION(FeatureProps) -> + supported. + %% ------------------------------------------------------------------- %% Feature flags registry. %% ------------------------------------------------------------------- @@ -968,6 +1038,7 @@ assert_feature_flag_is_valid(FeatureName, FeatureProps) -> doc_url, stability, require_level, + experiment_level, depends_on, callbacks], ?assertEqual([], maps:keys(FeatureProps) -- ValidProps), @@ -979,6 +1050,17 @@ assert_feature_flag_is_valid(FeatureName, FeatureProps) -> ?assert(Stability =:= stable orelse Stability =:= experimental orelse Stability =:= required), + ?assert(Stability =:= experimental orelse + not maps:is_key(experiment_level, FeatureProps)), + ?assert(Stability =:= required orelse + not maps:is_key(require_level, FeatureProps)), + RequireLevel = maps:get(require_level, FeatureProps, soft), + ?assert(RequireLevel =:= hard orelse RequireLevel =:= soft), + ExperimentLevel = maps:get( + experiment_level, FeatureProps, + unsupported), + ?assert(ExperimentLevel =:= unsupported orelse + ExperimentLevel =:= supported), ?assertNot(maps:is_key(migration_fun, FeatureProps)), ?assertNot(maps:is_key(warning, FeatureProps)), case FeatureProps of diff --git a/deps/rabbit/src/rabbit_ff_extra.erl b/deps/rabbit/src/rabbit_ff_extra.erl index 0171c4200856..79c445e3aab3 100644 --- a/deps/rabbit/src/rabbit_ff_extra.erl +++ b/deps/rabbit/src/rabbit_ff_extra.erl @@ -24,6 +24,12 @@ -type cli_info_entry() :: [{name, rabbit_feature_flags:feature_name()} | {state, enabled | disabled | unavailable} | {stability, rabbit_feature_flags:stability()} | + {require_level, + rabbit_feature_flags:require_level()} | + {experiment_level, + rabbit_feature_flags:experiment_level()} | + {callbacks, + [rabbit_feature_flags:callback_name()]} | {provided_by, atom()} | {desc, string()} | {doc_url, string()}]. @@ -61,6 +67,11 @@ cli_info(FeatureFlags) -> FeatureProps = maps:get(FeatureName, FeatureFlags), State = rabbit_feature_flags:get_state(FeatureName), Stability = rabbit_feature_flags:get_stability(FeatureProps), + RequireLevel = rabbit_feature_flags:get_require_level( + FeatureProps), + ExperimentLevel = rabbit_feature_flags:get_experiment_level( + FeatureProps), + Callbacks = maps:keys(maps:get(callbacks, FeatureProps, #{})), App = maps:get(provided_by, FeatureProps), Desc = maps:get(desc, FeatureProps, ""), DocUrl = maps:get(doc_url, FeatureProps, ""), @@ -69,6 +80,9 @@ cli_info(FeatureFlags) -> {doc_url, unicode:characters_to_binary(DocUrl)}, {state, State}, {stability, Stability}, + {require_level, RequireLevel}, + {experiment_level, ExperimentLevel}, + {callbacks, Callbacks}, {provided_by, App}], [FFInfo | Acc] end, [], lists:sort(maps:keys(FeatureFlags))). @@ -160,6 +174,8 @@ info(FeatureFlags, Options) -> {State, Color} = case State0 of enabled -> {"Enabled", Green}; + state_changing -> + {"(Changing)", Yellow}; disabled -> {"Disabled", Yellow}; unavailable -> diff --git a/deps/rabbitmq_management/priv/www/css/main.css b/deps/rabbitmq_management/priv/www/css/main.css index a3bcaae5d5f5..b3e404b794b8 100644 --- a/deps/rabbitmq_management/priv/www/css/main.css +++ b/deps/rabbitmq_management/priv/www/css/main.css @@ -232,7 +232,7 @@ div.form-popup-help { width: 500px; z-index: 2; } -p.warning, div.form-popup-warn { background: #FF9; } +div.warning, p.warning, div.form-popup-warn { background: #FF9; } div.form-popup-options { z-index: 3; overflow:auto; max-height:95%; } @@ -255,7 +255,14 @@ div.form-popup-options span:hover { cursor: pointer; } -p.warning { padding: 15px; border-radius: 5px; -moz-border-radius: 5px; text-align: center; } +div.warning, p.warning { padding: 15px; border-radius: 5px; -moz-border-radius: 5px; text-align: center; } +div.warning { + margin: 15px 0; +} + +div.warning button { + margin: auto; +} .highlight { min-width: 120px; font-size: 120%; text-align:center; padding:10px; background-color: #ddd; margin: 0 20px 0 0; color: #888; border-radius: 5px; -moz-border-radius: 5px; } .highlight strong { font-size: 2em; display: block; color: #444; font-weight: normal; } @@ -367,3 +374,49 @@ div.bindings-wrapper p.arrow { font-size: 200%; } } table.dynamic-shovels td label {width: 200px; margin-right:10px;padding: 4px 0px 5px 0px} + +input[type=checkbox].toggle { + display: none; +} + +label.toggle { + cursor: pointer; + text-indent: -9999px; + width: 32px; + height: 16px; + background: #ff5630; + display: block; + border-radius: 16px; + position: relative; + margin: auto; +} + +label.toggle:after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 12px; + height: 12px; + background: #fff; + border-radius: 12px; + transition: 0.3s; +} + +input.toggle:indeterminate + label.toggle { + background: #ffab00; +} + +input.toggle:checked + label.toggle { + background: #36b37e; +} + +input.toggle:indeterminate + label.toggle:after { + left: calc(50%); + transform: translateX(-50%); +} + +input.toggle:checked + label.toggle:after { + left: calc(100% - 2px); + transform: translateX(-100%); +} diff --git a/deps/rabbitmq_management/priv/www/js/main.js b/deps/rabbitmq_management/priv/www/js/main.js index aa56b9d6a3df..3955f4a6dac1 100644 --- a/deps/rabbitmq_management/priv/www/js/main.js +++ b/deps/rabbitmq_management/priv/www/js/main.js @@ -303,6 +303,23 @@ function reset_timer() { } } +function pause_auto_refresh() { + if (typeof globalThis.rmq_webui_auto_refresh_paused == 'undefined') + globalThis.rmq_webui_auto_refresh_paused = 0; + + globalThis.rmq_webui_auto_refresh_paused++; + if (timer != null) { + clearInterval(timer); + } +} + +function resume_auto_refresh() { + globalThis.rmq_webui_auto_refresh_paused--; + if (globalThis.rmq_webui_auto_refresh_paused == 0) { + reset_timer(); + } +} + function update_manual(div, query) { var path; var template; diff --git a/deps/rabbitmq_management/priv/www/js/tmpl/feature-flags.ejs b/deps/rabbitmq_management/priv/www/js/tmpl/feature-flags.ejs index 070acdb39420..0ab4d6a16f55 100644 --- a/deps/rabbitmq_management/priv/www/js/tmpl/feature-flags.ejs +++ b/deps/rabbitmq_management/priv/www/js/tmpl/feature-flags.ejs @@ -1,145 +1,316 @@ +

Feature Flags

<% - var needs_enabling = false; + var nonreq_feature_flags = []; for (var i = 0; i < feature_flags.length; i++) { - var feature_flag = feature_flags[i]; - if (feature_flag.state == "disabled" && feature_flag.stability != "experimental") { - needs_enabling = true; - } + if (feature_flags[i].stability == 'required') + continue; + nonreq_feature_flags.push(feature_flags[i]); } - if (needs_enabling) { %> -

- All stable feature flags must be enabled after completing an upgrade. Without enabling all flags, upgrading to future minor or major versions of RabbitMQ may not be possible. [Learn more] -

- <% } %> + %> +

Feature Flags

-<%= filter_ui(feature_flags) %> -
-<% if (feature_flags.length > 0) { %> - - - - - - - - - - <% - for (var i = 0; i < feature_flags.length; i++) { - var feature_flag = feature_flags[i]; - if (feature_flag.stability == "required") { - /* Hide required feature flags. There is nothing the user can do - * about them and they just add noise to the UI. */ - continue; - } - if (feature_flag.stability == "experimental") { - continue; - } - var state_color = "grey"; - if (feature_flag.state == "enabled") { - state_color = "green"; - } else if (feature_flag.state == "disabled") { - state_color = "yellow"; - } else if (feature_flag.state == "unsupported") { - state_color = "red"; - } - %> - > - - - - - <% } %> - -
<%= fmt_sort('Name', 'name') %><%= fmt_sort('State', 'state') %>Description
<%= fmt_string(feature_flag.name) %> - <% if (feature_flag.stability == "experimental") { %> - Experimental - <% } else if (feature_flag.stability == "stable" && feature_flag.state == "disabled") { %> -

Disabled!

- <% } %> - <% if (feature_flag.state == "disabled") { %> -
- - -
- <% } else { %> - - <%= fmt_string(feature_flag.state) %> - - <% } %> -
-

<%= fmt_string(feature_flag.desc) %>

- <% if (feature_flag.doc_url) { %> -

[Learn more]

- <% } %> -
-<% } else { %> -

... no feature_flags ...

-<% } %> -
-
-
+<%= filter_ui(nonreq_feature_flags) %> +
+<% if (nonreq_feature_flags.length > 0) { %> + + + -These flags can be enabled in production deployments after an appropriate amount of testing in non-production environments. -

+ <% - for (var i = 0; i < feature_flags.length; i++) { - var feature_flag = feature_flags[i]; - if (feature_flag.stability != "experimental") { - continue; - } - var state_color = "grey"; - if (feature_flag.state == "enabled") { - state_color = "green"; - } else if (feature_flag.state == "disabled") { - state_color = "yellow"; - } else if (feature_flag.state == "unsupported") { - state_color = "red"; - } + for (var i = 0; i < nonreq_feature_flags.length; i++) { + var feature_flag = nonreq_feature_flags[i]; %> > - +
<%= fmt_sort('Name', 'name') %>Specificities <%= fmt_sort('State', 'state') %> Description
<%= fmt_string(feature_flag.name) %> - <% if (feature_flag.state == "disabled") { %> -
- -
-
-
- - -
- - <% } else { %> - - <%= fmt_string(feature_flag.state) %> - +
+ <% if (feature_flag.callbacks.includes('enable')) { %> + + This feature flags has a migration function which might take some time and consume resources. + + + <% } %> + <% if (feature_flag.stability == 'experimental') { %> + + This is an experimental feature flag + + <% } %> + <% if (feature_flag.experiment_level == 'unsupported') { %> + + This experimental feature flag is not yet supported at this stage and an upgrade path is not guaranteed + + + <% } %> + + + checked disabled + <% } %> + <% if (feature_flag.state == 'state_changing') { %> + disabled + <% } %> + onchange='handle_feature_flag(this, "<%= feature_flag.name %>");'/> +

<%= fmt_string(feature_flag.desc) %>

@@ -157,3 +328,59 @@ These flags can be enabled in production deployments after an appropriate amount + + + + +

Enabling an experimental feature flag

+

+ The feature flag is experimental. + This means the functionality behind it is still a work in progress. Here + are a few important things to keep in mind: +

+
    +
  1. +

    + Before enabling it, make sure to try it in a test environment + first before enabling it in production. +

    +

    + The feature flag is supported even though it is still experimental. + Therefore, upgrades to a later version of RabbitMQ with this feature flag + enabled are supported. +

    +

    + + +

    +
  2. +
  3. +

    + This development of this feature is at an early stage. Support is not + provided and enabling it in production is not recommended. +

    +

    + Once it is enabled, upgrades to a future version of RabbitMQ is not + guaranteed! If there is no upgrade path, you will have to use a + blue-green migration + to upgrade RabbitMQ. +

    +

    + + +

    +

    + + +

    +
  4. +
  5. + If you enable it, + please give feedback, + this will help the RabbitMQ team polish it and make it stable as soon as + possible. +
  6. +
+ + +