From 724705ca3b8ec38e72ca18eb7f7fff0ad9834ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Wed, 30 Oct 2024 15:20:16 +0100 Subject: [PATCH 1/4] rabbit_feature_flags: Declare if an experimental feature flag is supported or not [Why] Durint the development of Khepri, it was difficult to communicate that it was unsupported in RabbitMQ 3.13.x but was then supported in 4.0.x even though it was still experimental. [How] The feature flag definition now exposes that support level in a now attribute called `experiment_level`. It can be `unsupported` or `supported`. We can use this now attribute in the CLI or the web UI to convey the level of support to the end user. In the future, we could imagine that an experimental feature flag becomes abandoned, where upgraded from a node that has it enabled to a version that marks the feature flag as abandoned is not possible. --- deps/rabbit/src/rabbit_core_ff.erl | 3 +- deps/rabbit/src/rabbit_feature_flags.erl | 72 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) 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..aaa8d582df85 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. @@ -809,6 +830,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 +1028,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 +1040,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 From f90cb869ccdd198e2624e09c60aea0974a993c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Thu, 31 Oct 2024 17:42:40 +0100 Subject: [PATCH 2/4] rabbit_feature_flags: Expose more feature flag properties to the management API [Why] It allows to better communicate each feature flag specificities and make a better more user-friendly management UI. --- deps/rabbit/src/rabbit_feature_flags.erl | 2 ++ deps/rabbit/src/rabbit_ff_extra.erl | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/deps/rabbit/src/rabbit_feature_flags.erl b/deps/rabbit/src/rabbit_feature_flags.erl index aaa8d582df85..c550df82d313 100644 --- a/deps/rabbit/src/rabbit_feature_flags.erl +++ b/deps/rabbit/src/rabbit_feature_flags.erl @@ -348,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, diff --git a/deps/rabbit/src/rabbit_ff_extra.erl b/deps/rabbit/src/rabbit_ff_extra.erl index 0171c4200856..e20a002fc1a3 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))). From d2d608211a4ae1444ac9246f38e671710fef09d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Mon, 4 Nov 2024 13:00:11 +0100 Subject: [PATCH 3/4] rabbit_feature_flags: Use non-blocking call in `get_state/1` [Why] The previous implementation was using the blocking `is_enabled/1` API. This meant that if a feature flag was being enabled and the enable callback took time, the CLI's `list_feature_flag` command or any use of the management UI would block until the feature flag was enabled. [How] `get_state/1` now uses the non-blocking API. However it returns a now possible value: `state_changing`. --- deps/rabbit/src/rabbit_feature_flags.erl | 24 ++++++++++++++++-------- deps/rabbit/src/rabbit_ff_extra.erl | 2 ++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/deps/rabbit/src/rabbit_feature_flags.erl b/deps/rabbit/src/rabbit_feature_flags.erl index c550df82d313..8425dafa4cef 100644 --- a/deps/rabbit/src/rabbit_feature_flags.erl +++ b/deps/rabbit/src/rabbit_feature_flags.erl @@ -719,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: %%
    %%
  • `enabled': the feature flag is enabled.
  • +%%
  • `state_changing': the feature flag is being enabled.
  • %%
  • `disabled': the feature flag is supported by all nodes in the %% cluster but currently disabled.
  • %%
  • `unavailable': the feature flag is unsupported by at least one @@ -733,16 +737,20 @@ info(Options) when is_map(Options) -> %%
%% %% @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 diff --git a/deps/rabbit/src/rabbit_ff_extra.erl b/deps/rabbit/src/rabbit_ff_extra.erl index e20a002fc1a3..79c445e3aab3 100644 --- a/deps/rabbit/src/rabbit_ff_extra.erl +++ b/deps/rabbit/src/rabbit_ff_extra.erl @@ -174,6 +174,8 @@ info(FeatureFlags, Options) -> {State, Color} = case State0 of enabled -> {"Enabled", Green}; + state_changing -> + {"(Changing)", Yellow}; disabled -> {"Disabled", Yellow}; unavailable -> From f7a740cd8f477807cf79bb9da2f5e470f81ff96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Thu, 31 Oct 2024 09:26:45 +0100 Subject: [PATCH 4/4] rabbit_feature_flags: Rework the management UI page [Why] The "Feature flags" admin section had several issues: * It was not designed for experimental feature flags. What was done for RabbitMQ 4.0.0 was still unclear as to what a user should expect for experimental feature flags. * The UI uses synchronous requests from the browser main thread. It means that for a feature flag that has a long running migration callback, the browser tab could freeze for a very long time. [How] The feature flags table is reworked and now displays: * a series of icons to highlight the following: * a feature flag that has a migration function and thus that can take time to be enabled * a feature flag that is experimental * whether this experimental feature flag is supported or not * a toggle to quickly show if a feature flag is enabled or not and let the user enable it at the same time. For stable feature flags, when a user click on the toggle, the toggle goes into an intermediate state while waiting for the response from the broker. If the response is successful, the toggle is green. Otherwise it goes back to red and the error is displayed in a popup as before. For experimental feature flags, when a user click on the toggle, a popup is displayed to let the user know of the possible constraints and consequences, with one or two required checkboxes to tick so the user confirms they understand the message. The feature flag is enabled only after the user validates the popup. The displayed message and the checkboxes depend on if the experimental feature flag is supported or not (it is a new attribute of experimental feature flags). The request to enable feature flags now uses the modern `fetch()` API. Therefore it uses Javascript promises and does not block the main thread: the UI remains responsive while a migration callback runs. Finally, an "Enable all stable feature flags" button has been added to the warning that tells the user some stable feature flags are still disabled. V2: Pause auto-refresh while a feature flag is being handled. This fixes some display inconsistencies. --- .../rabbitmq_management/priv/www/css/main.css | 57 ++- deps/rabbitmq_management/priv/www/js/main.js | 17 + .../priv/www/js/tmpl/feature-flags.ejs | 461 +++++++++++++----- 3 files changed, 416 insertions(+), 119 deletions(-) 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. +
+ + +