Jelajahi Sumber

chore: add more test for emqx_config_handler

Zhongwen Deng 4 tahun lalu
induk
melakukan
a862eb0252

+ 4 - 4
apps/emqx/src/emqx_config.erl

@@ -256,7 +256,7 @@ init_load(SchemaMod, Conf) when is_list(Conf) orelse is_binary(Conf) ->
     init_load(SchemaMod, parse_hocon(Conf));
 init_load(SchemaMod, RawConf) when is_map(RawConf) ->
     ok = save_schema_mod_and_names(SchemaMod),
-    %% Merge environment varialbe overrides on top
+    %% Merge environment variable overrides on top
     RawConfWithEnvs = merge_envs(SchemaMod, RawConf),
     ClusterOverrides = read_override_conf(#{override_to => cluster}),
     LocalOverrides = read_override_conf(#{override_to => local}),
@@ -267,7 +267,7 @@ init_load(SchemaMod, RawConf) when is_map(RawConf) ->
         check_config(SchemaMod, RawConfWithOverrides , #{}),
     RootNames = get_root_names(),
     ok = save_to_config_map(maps:with(get_atom_root_names(), CheckedConf),
-                            maps:with(RootNames, RawConfWithEnvs)).
+                            maps:with(RootNames, RawConfWithOverrides)).
 
 parse_hocon(Conf) ->
     IncDirs = include_dirs(),
@@ -301,7 +301,7 @@ merge_envs(SchemaMod, RawConf) ->
             },
     hocon_tconf:merge_env_overrides(SchemaMod, RawConf, all, Opts).
 
--spec check_config(module(), raw_config()) -> {AppEnvs, CheckedConf}
+-spec check_config(hocon_schema:shema(), raw_config()) -> {AppEnvs, CheckedConf}
     when AppEnvs :: app_envs(), CheckedConf :: config().
 check_config(SchemaMod, RawConf) ->
     check_config(SchemaMod, RawConf, #{}).
@@ -454,7 +454,7 @@ do_get(Type, [], Default) ->
                 AccIn#{conf_key(Type0, RootName) => Conf};
             (_, AccIn) -> AccIn
         end, #{}, persistent_term:get()),
-    case map_size(AllConf) == 0 of
+    case AllConf =:= #{} of
         true -> Default;
         false -> AllConf
     end;

+ 93 - 61
apps/emqx/src/emqx_config_handler.erl

@@ -28,6 +28,7 @@
         , remove_handler/1
         , update_config/3
         , get_raw_cluster_override_conf/0
+        , info/0
         , merge_to_old_config/2
         ]).
 
@@ -76,7 +77,8 @@ update_config(SchemaModule, ConfKeyPath, UpdateArgs) ->
 
 -spec add_handler(emqx_config:config_key_path(), handler_name()) -> ok.
 add_handler(ConfKeyPath, HandlerName) ->
-    gen_server:call(?MODULE, {add_handler, ConfKeyPath, HandlerName}, infinity).
+    assert_callback_function(HandlerName),
+    gen_server:call(?MODULE, {add_handler, ConfKeyPath, HandlerName}).
 
 %% @doc Remove handler asynchronously
 -spec remove_handler(emqx_config:config_key_path()) -> ok.
@@ -86,20 +88,20 @@ remove_handler(ConfKeyPath) ->
 get_raw_cluster_override_conf() ->
     gen_server:call(?MODULE, get_raw_cluster_override_conf).
 
+info() ->
+    gen_server:call(?MODULE, info).
+
 %%============================================================================
 
 -spec init(term()) -> {ok, state()}.
 init(_) ->
     process_flag(trap_exit, true),
-    {ok, #{handlers => #{?MOD => ?MODULE}}}.
+    Handlers = load_prev_handlers(),
+    {ok, #{handlers => Handlers#{?MOD => ?MODULE}}}.
 
 handle_call({add_handler, ConfKeyPath, HandlerName}, _From, State = #{handlers := Handlers}) ->
-    case deep_put_handler(ConfKeyPath, Handlers, HandlerName) of
-        {ok, NewHandlers} ->
-            {reply, ok, State#{handlers => NewHandlers}};
-        Error ->
-            {reply, Error, State}
-    end;
+    {ok, NewHandlers} = deep_put_handler(ConfKeyPath, Handlers, HandlerName),
+    {reply, ok, State#{handlers => NewHandlers}};
 
 handle_call({change_config, SchemaModule, ConfKeyPath, UpdateArgs}, _From,
             #{handlers := Handlers} = State) ->
@@ -108,20 +110,22 @@ handle_call({change_config, SchemaModule, ConfKeyPath, UpdateArgs}, _From,
 handle_call(get_raw_cluster_override_conf, _From, State) ->
     Reply = emqx_config:read_override_conf(#{override_to => cluster}),
     {reply, Reply, State};
+handle_call(info, _From, State) ->
+    {reply, State, State};
 handle_call(_Request, _From, State) ->
-    Reply = ok,
-    {reply, Reply, State}.
+    {reply, ok, State}.
 
-handle_cast({remove_handler, ConfKeyPath},
-            State = #{handlers := Handlers}) ->
-    {noreply, State#{handlers => emqx_map_lib:deep_remove(ConfKeyPath ++ [?MOD], Handlers)}};
+handle_cast({remove_handler, ConfKeyPath}, State = #{handlers := Handlers}) ->
+    NewHandlers = do_remove_handler(ConfKeyPath, Handlers),
+    {noreply, State#{handlers => NewHandlers}};
 handle_cast(_Msg, State) ->
     {noreply, State}.
 
 handle_info(_Info, State) ->
     {noreply, State}.
 
-terminate(_Reason, _State) ->
+terminate(_Reason, #{handlers := Handlers}) ->
+    save_handlers(Handlers),
     ok.
 
 code_change(_OldVsn, State, _Extra) ->
@@ -129,26 +133,10 @@ code_change(_OldVsn, State, _Extra) ->
 
 deep_put_handler([], Handlers, Mod) when is_map(Handlers) ->
     {ok, Handlers#{?MOD => Mod}};
-deep_put_handler([], _Handlers, Mod) ->
-    {ok, #{?MOD => Mod}};
-deep_put_handler([?WKEY | KeyPath], Handlers, Mod) ->
-    deep_put_handler2(?WKEY, KeyPath, Handlers, Mod);
 deep_put_handler([Key | KeyPath], Handlers, Mod) ->
-    case maps:find(?WKEY, Handlers) of
-        error ->
-            deep_put_handler2(Key, KeyPath, Handlers, Mod);
-        {ok, _SubHandlers} ->
-            {error, {cannot_override_a_wildcard_path, [?WKEY | KeyPath]}}
-    end.
-
-deep_put_handler2(Key, KeyPath, Handlers, Mod) ->
     SubHandlers = maps:get(Key, Handlers, #{}),
-    case deep_put_handler(KeyPath, SubHandlers, Mod) of
-        {ok, SubHandlers1} ->
-            {ok, Handlers#{Key => SubHandlers1}};
-        Error ->
-            Error
-    end.
+    {ok, SubHandlers1} = deep_put_handler(KeyPath, SubHandlers, Mod),
+    {ok, Handlers#{Key => SubHandlers1}}.
 
 handle_update_request(SchemaModule, ConfKeyPath, Handlers, UpdateArgs) ->
     try
@@ -157,9 +145,12 @@ handle_update_request(SchemaModule, ConfKeyPath, Handlers, UpdateArgs) ->
         throw : Reason ->
             {error, Reason};
         Error : Reason : ST ->
-            ?SLOG(error, #{msg => "change_config_failed",
+            ?SLOG(error, #{msg => "change_config_crashed",
                            exception => Error,
                            reason => Reason,
+                           update_req => UpdateArgs,
+                           module => SchemaModule,
+                           key_path => ConfKeyPath,
                            stacktrace => ST
                           }),
             {error, config_update_crashed}
@@ -174,11 +165,12 @@ do_handle_update_request(SchemaModule, ConfKeyPath, Handlers, UpdateArgs) ->
             {error, Result}
     end.
 
+process_update_request([_], _Handlers, {remove, _Opts}) ->
+    {error, "remove_root_is_forbidden"};
 process_update_request(ConfKeyPath, _Handlers, {remove, Opts}) ->
     OldRawConf = emqx_config:get_root_raw(ConfKeyPath),
     BinKeyPath = bin_path(ConfKeyPath),
     NewRawConf = emqx_map_lib:deep_remove(BinKeyPath, OldRawConf),
-    _ = remove_from_local_if_cluster_change(BinKeyPath, Opts),
     OverrideConf = remove_from_override_config(BinKeyPath, Opts),
     {ok, NewRawConf, OverrideConf, Opts};
 process_update_request(ConfKeyPath, Handlers, {{update, UpdateReq}, Opts}) ->
@@ -198,11 +190,12 @@ do_update_config([], Handlers, OldRawConf, UpdateReq, ConfKeyPath) ->
 do_update_config([ConfKey | SubConfKeyPath], Handlers, OldRawConf,
         UpdateReq, ConfKeyPath0) ->
     ConfKeyPath = ConfKeyPath0 ++ [ConfKey],
-    SubOldRawConf = get_sub_config(bin(ConfKey), OldRawConf),
+    ConfKeyBin = bin(ConfKey),
+    SubOldRawConf = get_sub_config(ConfKeyBin, OldRawConf),
     SubHandlers = get_sub_handlers(ConfKey, Handlers),
     case do_update_config(SubConfKeyPath, SubHandlers, SubOldRawConf, UpdateReq, ConfKeyPath) of
         {ok, NewUpdateReq} ->
-            call_pre_config_update(Handlers, OldRawConf, #{bin(ConfKey) => NewUpdateReq},
+            call_pre_config_update(Handlers, OldRawConf, #{ConfKeyBin => NewUpdateReq},
                 ConfKeyPath);
         Error ->
             Error
@@ -211,12 +204,11 @@ do_update_config([ConfKey | SubConfKeyPath], Handlers, OldRawConf,
 check_and_save_configs(SchemaModule, ConfKeyPath, Handlers, NewRawConf, OverrideConf,
         UpdateArgs, Opts) ->
     OldConf = emqx_config:get_root(ConfKeyPath),
-    FullRawConf = with_full_raw_confs(NewRawConf),
-    {AppEnvs, CheckedConf} = emqx_config:check_config(SchemaModule, FullRawConf),
-    NewConf = maps:with(maps:keys(OldConf), CheckedConf),
-    _ = remove_from_local_if_cluster_change(ConfKeyPath, Opts),
+    Schema = schema(SchemaModule, ConfKeyPath),
+    {AppEnvs, #{root := NewConf}} = emqx_config:check_config(Schema, #{<<"root">> => NewRawConf}),
     case do_post_config_update(ConfKeyPath, Handlers, OldConf, NewConf, AppEnvs, UpdateArgs, #{}) of
         {ok, Result0} ->
+            remove_from_local_if_cluster_change(ConfKeyPath, Opts),
             case save_configs(ConfKeyPath, AppEnvs, NewConf, NewRawConf, OverrideConf,
                     UpdateArgs, Opts) of
                 {ok, Result1} ->
@@ -259,8 +251,7 @@ get_sub_config(ConfKey, Conf) when is_map(Conf) ->
 get_sub_config(_, _Conf) -> %% the Conf is a primitive
     undefined.
 
-call_pre_config_update(Handlers, OldRawConf, UpdateReq, ConfKeyPath) ->
-    HandlerName = maps:get(?MOD, Handlers, undefined),
+call_pre_config_update(#{?MOD := HandlerName}, OldRawConf, UpdateReq, ConfKeyPath) ->
     case erlang:function_exported(HandlerName, pre_config_update, 3) of
         true ->
             case HandlerName:pre_config_update(ConfKeyPath, UpdateReq, OldRawConf) of
@@ -268,21 +259,22 @@ call_pre_config_update(Handlers, OldRawConf, UpdateReq, ConfKeyPath) ->
                 {error, Reason} -> {error, {pre_config_update, HandlerName, Reason}}
             end;
         false -> merge_to_old_config(UpdateReq, OldRawConf)
-    end.
+    end;
+call_pre_config_update(_Handlers, OldRawConf, UpdateReq, _ConfKeyPath) ->
+    merge_to_old_config(UpdateReq, OldRawConf).
 
-call_post_config_update(Handlers, OldConf, NewConf, AppEnvs, UpdateReq, Result, ConfKeyPath) ->
-    HandlerName = maps:get(?MOD, Handlers, undefined),
+call_post_config_update(#{?MOD := HandlerName}, OldConf, NewConf, AppEnvs, UpdateReq, Result, ConfKeyPath) ->
     case erlang:function_exported(HandlerName, post_config_update, 5) of
         true ->
-            case HandlerName:post_config_update(ConfKeyPath, UpdateReq, NewConf, OldConf,
-                    AppEnvs) of
+            case HandlerName:post_config_update(ConfKeyPath, UpdateReq, NewConf, OldConf, AppEnvs) of
                 ok -> {ok, Result};
-                {ok, Result1} ->
-                    {ok, Result#{HandlerName => Result1}};
+                {ok, Result1} -> {ok, Result#{HandlerName => Result1}};
                 {error, Reason} -> {error, {post_config_update, HandlerName, Reason}}
             end;
         false -> {ok, Result}
-    end.
+    end;
+call_post_config_update(_Handlers, _OldConf, _NewConf, _AppEnvs, _UpdateReq, Result, _ConfKeyPath) ->
+    {ok, Result}.
 
 save_configs(ConfKeyPath, AppEnvs, CheckedConf, NewRawConf, OverrideConf, UpdateArgs, Opts) ->
     case emqx_config:save_configs(AppEnvs, CheckedConf, NewRawConf, OverrideConf, Opts) of
@@ -296,19 +288,18 @@ save_configs(ConfKeyPath, AppEnvs, CheckedConf, NewRawConf, OverrideConf, Update
 %%   2. either the old or the new config is not of map type
 %% the behaviour is merging the new the config to the old config if they are maps.
 merge_to_old_config(UpdateReq, RawConf) when is_map(UpdateReq), is_map(RawConf) ->
-    {ok, maps:merge(RawConf, UpdateReq)};
+    {ok, emqx_map_lib:deep_merge(RawConf, UpdateReq)};
 merge_to_old_config(UpdateReq, _RawConf) ->
     {ok, UpdateReq}.
 
 %% local-override.conf priority is higher than cluster-override.conf
 %% If we want cluster to take effect, we must remove the local.
-remove_from_local_if_cluster_change(BinKeyPath, Opts) ->
-    case maps:get(override, Opts, local) of
-        local -> ok;
-        cluster ->
-            Local = remove_from_override_config(BinKeyPath, Opts#{override_to => local}),
-            emqx_config:save_to_override_conf(Local, Opts)
-    end.
+remove_from_local_if_cluster_change(BinKeyPath, #{override_to := cluster} = Opts) ->
+    Local = remove_from_override_config(BinKeyPath, Opts#{override_to => local}),
+    _ = emqx_config:save_to_override_conf(Local, Opts),
+    ok;
+remove_from_local_if_cluster_change(_BinKeyPath, _Opts) ->
+    ok.
 
 remove_from_override_config(_BinKeyPath, #{persistent := false}) ->
     undefined;
@@ -337,17 +328,58 @@ return_rawconf(ConfKeyPath, #{rawconf_with_defaults := true}) ->
 return_rawconf(ConfKeyPath, _) ->
     emqx_config:get_raw(ConfKeyPath).
 
-with_full_raw_confs(PartialConf) ->
-    maps:merge(emqx_config:get_raw([]), PartialConf).
-
 bin_path(ConfKeyPath) -> [bin(Key) || Key <- ConfKeyPath].
 
 bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
 bin(B) when is_binary(B) -> B.
 
+list(Atom) when is_atom(Atom) -> atom_to_list(Atom);
+list(Bin) when is_binary(Bin) -> binary_to_list(Bin);
+list(List) when is_list(List) -> List.
+
 atom(Bin) when is_binary(Bin) ->
     binary_to_atom(Bin, utf8);
 atom(Str) when is_list(Str) ->
     list_to_atom(Str);
 atom(Atom) when is_atom(Atom) ->
     Atom.
+
+-dialyzer({nowarn_function, do_remove_handler/2}).
+do_remove_handler(ConfKeyPath, Handlers) ->
+    NewHandlers = emqx_map_lib:deep_remove(ConfKeyPath ++ [?MOD], Handlers),
+    remove_empty_leaf(ConfKeyPath, NewHandlers).
+
+remove_empty_leaf([], Handlers) -> Handlers;
+remove_empty_leaf(KeyPath, Handlers) ->
+    case emqx_map_lib:deep_find(KeyPath, Handlers) =:= {ok, #{}} of
+        true -> %% empty leaf
+            Handlers1 = emqx_map_lib:deep_remove(KeyPath, Handlers),
+            SubKeyPath = lists:sublist(KeyPath, length(KeyPath) - 1),
+            remove_empty_leaf(SubKeyPath, Handlers1);
+        false -> Handlers
+    end.
+
+assert_callback_function(Mod) ->
+    case erlang:function_exported(Mod, pre_config_update, 3) orelse
+        erlang:function_exported(Mod, post_config_update, 5) of
+        true -> ok;
+        false -> error(#{msg => "bad_emqx_config_handler_callback", module => Mod})
+    end,
+    ok.
+
+schema(SchemaModule, [RootKey | _]) ->
+    Roots = SchemaModule:roots(),
+    Fields =
+        case lists:keyfind(list(RootKey), 1, Roots) of
+            false -> lists:keyfind(RootKey, 1, Roots);
+            Fields1 -> Fields1
+        end,
+    #{roots => [root], fields => #{root => [Fields]}}.
+
+load_prev_handlers() ->
+    Handlers = application:get_env(emqx, ?MODULE, #{}),
+    application:unset_env(emqx, ?MODULE),
+    Handlers.
+
+save_handlers(Handlers) ->
+    application:set_env(emqx, ?MODULE, Handlers).

+ 259 - 0
apps/emqx/test/emqx_config_handler_SUITE.erl

@@ -0,0 +1,259 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2019-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%%     http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%--------------------------------------------------------------------
+
+-module(emqx_config_handler_SUITE).
+
+-compile(export_all).
+-compile(nowarn_export_all).
+
+-define(MOD, {mod}).
+-define(WKEY, '?').
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("common_test/include/ct.hrl").
+
+all() -> emqx_common_test_helpers:all(?MODULE).
+
+init_per_suite(Config) ->
+    emqx_common_test_helpers:boot_modules(all),
+    emqx_common_test_helpers:start_apps([]),
+    Config.
+
+end_per_suite(_Config) ->
+    emqx_common_test_helpers:stop_apps([]).
+
+init_per_testcase(_Case, Config) ->
+    Config.
+
+end_per_testcase(_Case, _Config) ->
+    ok.
+
+t_handler(_Config) ->
+    BadCallBackMod = emqx,
+    RootKey = sysmon,
+    %% bad
+    ?assertError(#{msg := "bad_emqx_config_handler_callback", module := BadCallBackMod},
+        emqx_config_handler:add_handler([RootKey], BadCallBackMod)),
+    %% simple
+    ok = emqx_config_handler:add_handler([RootKey], ?MODULE),
+    #{handlers := Handlers0} = emqx_config_handler:info(),
+    ?assertMatch(#{RootKey := #{?MOD := ?MODULE}}, Handlers0),
+    ok = emqx_config_handler:remove_handler([RootKey]),
+    #{handlers := Handlers1} = emqx_config_handler:info(),
+    ct:pal("Key:~p simple: ~p~n", [RootKey, Handlers1]),
+    ?assertEqual(false, maps:is_key(RootKey, Handlers1)),
+    %% wildcard 1
+    Wildcard1 = [RootKey, '?', cpu_check_interval],
+    ok = emqx_config_handler:add_handler(Wildcard1, ?MODULE),
+    #{handlers := Handlers2} = emqx_config_handler:info(),
+    ?assertMatch(#{RootKey := #{?WKEY := #{cpu_check_interval := #{?MOD := ?MODULE}}}}, Handlers2),
+    ok = emqx_config_handler:remove_handler(Wildcard1),
+    #{handlers := Handlers3} = emqx_config_handler:info(),
+    ct:pal("Key:~p wildcard1: ~p~n", [Wildcard1, Handlers3]),
+    ?assertEqual(false, maps:is_key(RootKey, Handlers3)),
+
+    %% can_override_a_wildcard_path
+    ok = emqx_config_handler:add_handler(Wildcard1, ?MODULE),
+    ?assertEqual(ok, emqx_config_handler:add_handler([RootKey, os, cpu_check_interval], ?MODULE)),
+    ok = emqx_config_handler:remove_handler(Wildcard1),
+    ok = emqx_config_handler:remove_handler([RootKey, os, cpu_check_interval]),
+
+    ok = emqx_config_handler:add_handler([RootKey, os, cpu_check_interval], ?MODULE),
+    ok = emqx_config_handler:add_handler(Wildcard1, ?MODULE),
+    ok = emqx_config_handler:remove_handler([RootKey, os, cpu_check_interval]),
+    ok = emqx_config_handler:remove_handler(Wildcard1),
+    ok.
+
+t_root_key_update(_Config) ->
+    PathKey = [sysmon],
+    Opts = #{rawconf_with_defaults => true},
+    ok = emqx_config_handler:add_handler(PathKey, ?MODULE),
+    %% update
+    Old = #{<<"os">> := OS} = emqx:get_raw_config(PathKey),
+    {ok, Res} = emqx:update_config(PathKey,
+        Old#{<<"os">> => OS#{<<"cpu_check_interval">> => <<"12s">>}}, Opts),
+    ?assertMatch(#{config := #{os := #{cpu_check_interval := 12000}},
+        post_config_update := #{?MODULE := ok},
+        raw_config := #{<<"os">> := #{<<"cpu_check_interval">> := <<"12s">>}}},
+        Res),
+    ?assertMatch(#{os := #{cpu_check_interval := 12000}}, emqx:get_config(PathKey)),
+
+    %% update sub key
+    SubKey = PathKey ++ [os, cpu_high_watermark],
+    ?assertEqual({ok,#{config => 0.81,
+        post_config_update => #{?MODULE => ok},
+        raw_config => <<"81%">>}},
+        emqx:update_config(SubKey, "81%", Opts)),
+    ?assertEqual(0.81, emqx:get_config(SubKey)),
+    ?assertEqual("81%", emqx:get_raw_config(SubKey)),
+    %% remove
+    ?assertEqual({error, "remove_root_is_forbidden"}, emqx:remove_config(PathKey)),
+    ?assertMatch(#{<<"os">> := _, <<"vm">> := _ }, emqx:get_raw_config(PathKey)),
+
+    ok = emqx_config_handler:remove_handler(PathKey),
+    ok.
+
+t_sub_key_update_remove(_Config) ->
+    KeyPath = [sysmon, os, cpu_check_interval],
+    Opts = #{},
+    ok = emqx_config_handler:add_handler(KeyPath, ?MODULE),
+    {ok, Res} = emqx:update_config(KeyPath, <<"60s">>, Opts),
+    ?assertMatch(#{config := 60000,
+        post_config_update := #{?MODULE := ok},
+        raw_config :=  <<"60s">>},
+        Res),
+    ?assertMatch(60000, emqx:get_config(KeyPath)),
+
+    KeyPath2 = [sysmon, os, cpu_low_watermark],
+    ok = emqx_config_handler:add_handler(KeyPath2, ?MODULE),
+    {ok, Res1} = emqx:update_config(KeyPath2, <<"40%">>, Opts),
+    ?assertMatch(#{config := 0.4,
+        post_config_update := #{},
+        raw_config :=  <<"40%">>},
+        Res1),
+    ?assertMatch(0.4, emqx:get_config(KeyPath2)),
+
+    %% remove
+    ?assertEqual({ok,#{post_config_update => #{emqx_config_handler_SUITE => ok}}},
+        emqx:remove_config(KeyPath)),
+    ?assertError({config_not_found, [sysmon,os, cpu_check_interval]},
+        emqx:get_raw_config(KeyPath)),
+    ok = emqx_config_handler:remove_handler(KeyPath),
+    ok = emqx_config_handler:remove_handler(KeyPath2),
+    ok.
+
+t_check_failed(_Config) ->
+    KeyPath = [sysmon, os, cpu_check_interval],
+    Opts = #{rawconf_with_defaults => true},
+    Origin = emqx:get_raw_config(KeyPath),
+    ok = emqx_config_handler:add_handler(KeyPath, ?MODULE),
+    %% It should be a duration("1h"), but we set it as a percent.
+    ?assertMatch({error, _Res}, emqx:update_config(KeyPath, <<"80%">>, Opts)),
+    New = emqx:get_raw_config(KeyPath),
+    ?assertEqual(Origin, New),
+    ok = emqx_config_handler:remove_handler(KeyPath),
+    ok.
+
+t_stop(_Config) ->
+    OldPid = erlang:whereis(emqx_config_handler),
+    OldInfo = emqx_config_handler:info(),
+    emqx_config_handler:stop(),
+    NewPid = wait_for_new_pid(),
+    NewInfo = emqx_config_handler:info(),
+    ?assertNotEqual(OldPid, NewPid),
+    ?assertEqual(OldInfo, NewInfo),
+    ok.
+
+t_callback_crash(_Config) ->
+    CrashPath = [sysmon, os, cpu_high_watermark],
+    Opts = #{rawconf_with_defaults => true},
+    ok = emqx_config_handler:add_handler(CrashPath, ?MODULE),
+    Old = emqx:get_raw_config(CrashPath),
+    ?assertEqual({error, config_update_crashed}, emqx:update_config(CrashPath, <<"89%">>, Opts)),
+    New = emqx:get_raw_config(CrashPath),
+    ?assertEqual(Old, New),
+    ok = emqx_config_handler:remove_handler(CrashPath),
+    ok.
+
+t_pre_callback_error(_Config) ->
+    callback_error([sysmon, os, mem_check_interval], <<"100s">>,
+        {error, {pre_config_update, ?MODULE, pre_config_update_error}}),
+    ok.
+
+t_post_update_error(_Config)  ->
+    callback_error([sysmon, os, sysmem_high_watermark], <<"60%">>,
+        {error, {post_config_update, ?MODULE, post_config_update_error}}),
+    ok.
+
+t_handler_root() ->
+    %% Don't rely on default emqx_config_handler's merge behaviour.
+    RootKey = [],
+    Opts = #{rawconf_with_defaults => true},
+    ok = emqx_config_handler:add_handler(RootKey, ?MODULE),
+    %% update
+    Old = #{<<"sysmon">> := #{<<"os">> := OS}} = emqx:get_raw_config(RootKey),
+    {ok, Res} = emqx:update_config(RootKey,
+        Old#{<<"sysmon">> => #{<<"os">> => OS#{<<"cpu_check_interval">> => <<"12s">>}}},
+        Opts),
+    ?assertMatch(#{config := #{os := #{cpu_check_interval := 12000}},
+        post_config_update := #{?MODULE := ok},
+        raw_config := #{<<"os">> := #{<<"cpu_check_interval">> := <<"12s">>}}},
+        Res),
+    ?assertMatch(#{sysmon := #{os := #{cpu_check_interval := 12000}}}, emqx:get_config(RootKey)),
+    ok.
+
+t_get_raw_cluster_override_conf(_Config) ->
+    Raw0 = emqx_config:read_override_conf(#{override_to => cluster}),
+    Raw1 = emqx_config_handler:get_raw_cluster_override_conf(),
+    ?assertEqual(Raw0, Raw1),
+    OldPid = erlang:whereis(emqx_config_handler),
+    OldInfo = emqx_config_handler:info(),
+
+    ?assertEqual(ok, gen_server:call(emqx_config_handler, bad_call_msg)),
+    gen_server:cast(emqx_config_handler, bad_cast_msg),
+    erlang:send(emqx_config_handler, bad_info_msg),
+
+    NewPid = erlang:whereis(emqx_config_handler),
+    NewInfo = emqx_config_handler:info(),
+    ?assertEqual(OldPid, NewPid),
+    ?assertEqual(OldInfo, NewInfo),
+    ok.
+
+t_save_config_failed(_Config) ->
+
+    ok.
+
+pre_config_update([sysmon], UpdateReq, RawConf) ->
+    {ok, emqx_map_lib:deep_merge(RawConf, UpdateReq)};
+pre_config_update([sysmon, os], UpdateReq, _RawConf) ->
+    {ok, UpdateReq};
+pre_config_update([sysmon, os, cpu_check_interval], UpdateReq, _RawConf) ->
+    {ok, UpdateReq};
+pre_config_update([sysmon, os, cpu_low_watermark], UpdateReq, _RawConf) ->
+    {ok, UpdateReq};
+pre_config_update([sysmon, os, sysmem_high_watermark], UpdateReq, _RawConf) ->
+    {ok, UpdateReq};
+pre_config_update([sysmon, os, mem_check_interval], _UpdateReq, _RawConf) ->
+    {error, pre_config_update_error}.
+
+post_config_update([sysmon], _UpdateReq, _NewConf, _OldConf, _AppEnvs) ->
+    {ok, ok};
+post_config_update([sysmon, os], _UpdateReq, _NewConf, _OldConf, _AppEnvs) ->
+    {ok, ok};
+post_config_update([sysmon, os, cpu_check_interval], _UpdateReq, _NewConf, _OldConf, _AppEnvs) ->
+    {ok, ok};
+post_config_update([sysmon, os, cpu_low_watermark], _UpdateReq, _NewConf, _OldConf, _AppEnvs) ->
+    ok;
+post_config_update([sysmon, os, sysmem_high_watermark], _UpdateReq, _NewConf, _OldConf, _AppEnvs) ->
+    {error, post_config_update_error}.
+
+wait_for_new_pid() ->
+    case erlang:whereis(emqx_config_handler) of
+        undefined ->
+            ct:sleep(10),
+            wait_for_new_pid();
+        Pid -> Pid
+    end.
+
+callback_error(FailedPath, Update, Error)  ->
+    Opts = #{rawconf_with_defaults => true},
+    ok = emqx_config_handler:add_handler(FailedPath, ?MODULE),
+    Old = emqx:get_raw_config(FailedPath),
+    ?assertEqual(Error, emqx:update_config(FailedPath, Update, Opts)),
+    New = emqx:get_raw_config(FailedPath),
+    ?assertEqual(Old, New),
+    ok = emqx_config_handler:remove_handler(FailedPath),
+    ok.

+ 1 - 2
apps/emqx_slow_subs/src/emqx_slow_subs.erl

@@ -140,8 +140,7 @@ init([]) ->
     Enable = emqx:get_config([slow_subs, enable]),
     {ok, check_enable(Enable, InitState)}.
 
-handle_call({update_settings, #{enable := Enable} = Conf}, _From, State) ->
-    emqx_config:put([slow_subs], Conf),
+handle_call({update_settings, #{enable := Enable}}, _From, State) ->
     State2 = check_enable(Enable, State),
     {reply, ok, State2};