From b6ea325ccb1e3de45f8541ca4fde225ac75f7d54 Mon Sep 17 00:00:00 2001 From: Ronny Berndt Date: Fri, 22 Sep 2023 20:44:50 +0200 Subject: [PATCH] Add new HTTP endpoint `/_node/_local/_smoosh/status`. (#4766) Introduce a new HTTP endpoint `/_node/_local/_smoosh/status` to get status information from the CouchDB auto-compaction daemon. Previously, this was only possible by starting a `remsh` session and manually calling the `smoosh:status/1` function. The internal data structures of `smoosh:status/1` are migrated into Erlang maps to send them directly as json to the client. To add more status information to smoosh in the future, the available information will be stored under the json key `channels`. Example: { "channels": { "ratio_dbs": { ... }, "slack_dbs": { ... }, ... } --- src/chttpd/src/chttpd_node.erl | 6 ++ src/docs/src/api/server/common.rst | 110 +++++++++++++++++++++++ src/smoosh/src/smoosh_channel.erl | 14 +-- src/smoosh/src/smoosh_persist.erl | 10 +-- src/smoosh/src/smoosh_priority_queue.erl | 31 +++---- src/smoosh/src/smoosh_server.erl | 16 ++-- src/smoosh/test/smoosh_tests.erl | 43 ++++----- 7 files changed, 174 insertions(+), 56 deletions(-) diff --git a/src/chttpd/src/chttpd_node.erl b/src/chttpd/src/chttpd_node.erl index 46850fc4e64..165b85a42d1 100644 --- a/src/chttpd/src/chttpd_node.erl +++ b/src/chttpd/src/chttpd_node.erl @@ -39,6 +39,12 @@ handle_node_req(#httpd{path_parts = [_, <<"_local">>]} = Req) -> send_json(Req, 200, {[{name, node()}]}); handle_node_req(#httpd{path_parts = [A, <<"_local">> | Rest]} = Req) -> handle_node_req(Req#httpd{path_parts = [A, node()] ++ Rest}); +% GET /_node/$node/_smoosh/status +handle_node_req(#httpd{method = 'GET', path_parts = [_, _Node, <<"_smoosh">>, <<"status">>]} = Req) -> + {ok, Status} = smoosh:status(), + send_json(Req, 200, Status); +handle_node_req(#httpd{path_parts = [_, _Node, <<"_smoosh">>, <<"status">>]} = Req) -> + send_method_not_allowed(Req, "GET"); % GET /_node/$node/_versions handle_node_req(#httpd{method = 'GET', path_parts = [_, _Node, <<"_versions">>]} = Req) -> IcuVer = couch_ejson_compare:get_icu_version(), diff --git a/src/docs/src/api/server/common.rst b/src/docs/src/api/server/common.rst index 9e645f64901..783b8120aa6 100644 --- a/src/docs/src/api/server/common.rst +++ b/src/docs/src/api/server/common.rst @@ -1902,6 +1902,116 @@ See :ref:`Configuration of Prometheus Endpoint ` for details. Accept: text/plain Host: localhost:17986 +.. _api/server/smoosh/status: + +===================================== +``/_node/{node-name}/_smoosh/status`` +===================================== + +.. versionadded:: 3.4 + +.. http:get:: /_node/{node-name}/_smoosh/status + :synopsis: Returns metrics of the CouchDB's auto-compaction daemon + + This prints the state of each channel, how many jobs they are + currently running and how many jobs are enqueued (as well as the + lowest and highest priority of those enqueued items). The idea is to + provide, at a glance, sufficient insight into ``smoosh`` that an operator + can assess whether ``smoosh`` is adequately targeting the reclaimable + space in the cluster. + + In general, a healthy status output will have + items in the ``ratio_dbs`` and ``ratio_views`` channels. Owing to the default + settings, the ``slack_dbs`` and ``slack_views`` will almost certainly have + items in them. Historically, we've not found that the slack channels, + on their own, are particularly adept at keeping things well compacted. + + :code 200: Request completed successfully + :code 401: CouchDB Server Administrator privileges required + + **Request**: + + .. code-block:: http + + GET /_node/_local/_smoosh/status HTTP/1.1 + Host: 127.0.0.1:5984 + Accept: */* + + **Response**: + + .. code-block:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "channels": { + "slack_dbs": { + "starting": 0, + "waiting": { + "size": 0, + "min": 0, + "max": 0 + }, + "active": 0 + }, + "ratio_dbs": { + "starting": 0, + "waiting": { + "size": 56, + "min": 1.125, + "max": 11.0625 + }, + "active": 0 + }, + "ratio_views": { + "starting": 0, + "waiting": { + "size": 0, + "min": 0, + "max": 0 + }, + "active": 0 + }, + "upgrade_dbs": { + "starting": 0, + "waiting": { + "size": 0, + "min": 0, + "max": 0 + }, + "active": 0 + }, + "slack_views": { + "starting": 0, + "waiting": { + "size": 0, + "min": 0, + "max": 0 + }, + "active": 0 + }, + "upgrade_views": { + "starting": 0, + "waiting": { + "size": 0, + "min": 0, + "max": 0 + }, + "active": 0 + }, + "index_cleanup": { + "starting": 0, + "waiting": { + "size": 0, + "min": 0, + "max": 0 + }, + "active": 0 + } + } + } + .. _api/server/system: ============================== diff --git a/src/smoosh/src/smoosh_channel.erl b/src/smoosh/src/smoosh_channel.erl index 92fd3413b7b..3cfbcdec69c 100644 --- a/src/smoosh/src/smoosh_channel.erl +++ b/src/smoosh/src/smoosh_channel.erl @@ -77,10 +77,10 @@ enqueue(ServerRef, Object, Priority) -> get_status(StatusTab) when is_reference(StatusTab) -> try ets:lookup(StatusTab, status) of [{status, Status}] -> Status; - [] -> [] + [] -> #{} catch error:badarg -> - [] + #{} end. close(ServerRef) -> @@ -235,11 +235,11 @@ unpersist(Name) -> % set_status(#state{} = State) -> #state{active = Active, starting = Starting, waiting = Waiting} = State, - Status = [ - {active, map_size(Active)}, - {starting, map_size(Starting)}, - {waiting, smoosh_priority_queue:info(Waiting)} - ], + Status = #{ + active => map_size(Active), + starting => map_size(Starting), + waiting => smoosh_priority_queue:info(Waiting) + }, true = ets:insert(State#state.stab, {status, Status}), State. diff --git a/src/smoosh/src/smoosh_persist.erl b/src/smoosh/src/smoosh_persist.erl index 2feab7e6901..c1519f65fa4 100644 --- a/src/smoosh/src/smoosh_persist.erl +++ b/src/smoosh/src/smoosh_persist.erl @@ -225,7 +225,7 @@ t_persist_unpersist_disabled(_) -> Q2 = unpersist(Name), ?assertEqual(Name, smoosh_priority_queue:name(Q2)), - ?assertEqual([{size, 0}], smoosh_priority_queue:info(Q2)). + ?assertEqual(#{max => 0, min => 0, size => 0}, smoosh_priority_queue:info(Q2)). t_persist_unpersist_enabled(_) -> Name = "chan2", @@ -241,7 +241,7 @@ t_persist_unpersist_enabled(_) -> Q2 = unpersist(Name), ?assertEqual(Name, smoosh_priority_queue:name(Q2)), Info2 = smoosh_priority_queue:info(Q2), - ?assertEqual([{size, 3}, {min, 1.0}, {max, infinity}], Info2), + ?assertEqual(#{max => infinity, min => 1.0, size => 3}, Info2), ?assertEqual(Keys, drain_q(Q2)), % Try to persist the already unpersisted queue @@ -249,7 +249,7 @@ t_persist_unpersist_enabled(_) -> Q3 = unpersist(Name), ?assertEqual(Name, smoosh_priority_queue:name(Q3)), Info3 = smoosh_priority_queue:info(Q2), - ?assertEqual([{size, 3}, {min, 1.0}, {max, infinity}], Info3), + ?assertEqual(#{max => infinity, min => 1.0, size => 3}, Info3), ?assertEqual(Keys, drain_q(Q3)). t_persist_unpersist_errors(_) -> @@ -267,7 +267,7 @@ t_persist_unpersist_errors(_) -> Q2 = unpersist(Name), ?assertEqual(Name, smoosh_priority_queue:name(Q2)), - ?assertEqual([{size, 0}], smoosh_priority_queue:info(Q2)), + ?assertEqual(#{max => 0, min => 0, size => 0}, smoosh_priority_queue:info(Q2)), Dir = state_dir(), ok = file:make_dir(Dir), @@ -278,7 +278,7 @@ t_persist_unpersist_errors(_) -> Q3 = unpersist(Name), ?assertEqual(Name, smoosh_priority_queue:name(Q3)), - ?assertEqual([{size, 0}], smoosh_priority_queue:info(Q3)), + ?assertEqual(#{max => 0, min => 0, size => 0}, smoosh_priority_queue:info(Q3)), ok = file:del_dir_r(Dir). diff --git a/src/smoosh/src/smoosh_priority_queue.erl b/src/smoosh/src/smoosh_priority_queue.erl index b2ef4393dbd..2f2fba687a0 100644 --- a/src/smoosh/src/smoosh_priority_queue.erl +++ b/src/smoosh/src/smoosh_priority_queue.erl @@ -64,17 +64,14 @@ qsize(#priority_queue{tree = Tree}) -> gb_trees:size(Tree). info(#priority_queue{tree = Tree} = Q) -> - [ - {size, qsize(Q)} - | case gb_trees:is_empty(Tree) of - true -> - []; - false -> - {{Min, _}, _} = gb_trees:smallest(Tree), - {{Max, _}, _} = gb_trees:largest(Tree), - [{min, Min}, {max, Max}] - end - ]. + case gb_trees:is_empty(Tree) of + true -> + #{size => qsize(Q), min => 0, max => 0}; + false -> + {{Min, _}, _} = gb_trees:smallest(Tree), + {{Max, _}, _} = gb_trees:largest(Tree), + #{size => qsize(Q), min => Min, max => Max} + end. insert(Key, Priority, Capacity, #priority_queue{tree = Tree, map = Map} = Q) -> TreeKey = {Priority, make_ref()}, @@ -122,7 +119,7 @@ basics_test() -> Q = new("foo"), ?assertMatch(#priority_queue{}, Q), ?assertEqual("foo", name(Q)), - ?assertEqual([{size, 0}], info(Q)). + ?assertEqual(0, maps:get(size, info(Q))). empty_test() -> Q = new("foo"), @@ -136,7 +133,7 @@ one_element_test() -> Q0 = new("foo"), Q = in(?K1, ?P1, 1, Q0), ?assertMatch(#priority_queue{}, Q), - ?assertEqual([{size, 1}, {min, 1}, {max, 1}], info(Q)), + ?assertEqual(#{max => 1, min => 1, size => 1}, info(Q)), ?assertEqual(Q, truncate(1, Q)), ?assertMatch({?K1, #priority_queue{}}, out(Q)), {?K1, Q2} = out(Q), @@ -144,7 +141,7 @@ one_element_test() -> ?assertEqual(#{?K1 => ?P1}, to_map(Q)), Q3 = from_map("foo", 1, to_map(Q)), ?assertEqual("foo", name(Q3)), - ?assertEqual([{size, 1}, {min, ?P1}, {max, ?P1}], info(Q3)), + ?assertEqual(#{max => ?P1, min => ?P1, size => 1}, info(Q3)), ?assertEqual(to_map(Q), to_map(Q3)), ?assertEqual(Q0, flush(Q)). @@ -153,7 +150,7 @@ multiple_elements_basics_test() -> Q1 = in(?K1, ?P1, 10, Q0), Q2 = in(?K2, ?P2, 10, Q1), Q3 = in(?K3, ?P3, 10, Q2), - ?assertEqual([{size, 3}, {min, ?P1}, {max, ?P3}], info(Q3)), + ?assertEqual(#{max => ?P3, min => ?P1, size => 3}, info(Q3)), ?assertEqual([?K3, ?K2, ?K1], drain(Q3)). update_element_same_priority_test() -> @@ -166,7 +163,7 @@ update_element_new_priority_test() -> Q1 = in(?K1, ?P1, 10, Q0), Q2 = in(?K2, ?P2, 10, Q1), Q3 = in(?K1, ?P3, 10, Q2), - ?assertEqual([{size, 2}, {min, ?P2}, {max, ?P3}], info(Q3)), + ?assertEqual(#{max => ?P3, min => ?P2, size => 2}, info(Q3)), ?assertEqual([?K1, ?K2], drain(Q3)). capacity_test() -> @@ -189,7 +186,7 @@ a_lot_of_elements_test() -> lists:seq(1, N) ), Q = from_map("foo", N, maps:from_list(KVs)), - ?assertMatch([{size, N} | _], info(Q)), + ?assertMatch(N, maps:get(size, info(Q))), {_, Priorities} = lists:unzip(drain(Q)), ?assertEqual(lists:reverse(lists:sort(Priorities)), Priorities). diff --git a/src/smoosh/src/smoosh_server.erl b/src/smoosh/src/smoosh_server.erl index 10368a5494d..3b0b86808c6 100644 --- a/src/smoosh/src/smoosh_server.erl +++ b/src/smoosh/src/smoosh_server.erl @@ -96,12 +96,14 @@ flush() -> gen_server:call(?MODULE, flush, infinity). status() -> - try ets:foldl(fun get_channel_status/2, [], ?MODULE) of - Res -> {ok, Res} - catch - error:badarg -> - {ok, []} - end. + ChannelsStatus = + try ets:foldl(fun get_channel_status/2, #{}, ?MODULE) of + Res -> Res + catch + error:badarg -> + #{} + end, + {ok, #{channels => ChannelsStatus}}. enqueue(Object0) -> Object = smoosh_utils:validate_arg(Object0), @@ -286,7 +288,7 @@ remove_enqueue_ref(Ref, #state{} = State) when is_reference(Ref) -> get_channel_status(#channel{name = Name, stab = Tab}, Acc) -> Status = smoosh_channel:get_status(Tab), - [{Name, Status} | Acc]; + Acc#{list_to_atom(Name) => Status}; get_channel_status(_, Acc) -> Acc. diff --git a/src/smoosh/test/smoosh_tests.erl b/src/smoosh/test/smoosh_tests.erl index 622cabc8e70..6861db5e188 100644 --- a/src/smoosh/test/smoosh_tests.erl +++ b/src/smoosh/test/smoosh_tests.erl @@ -82,21 +82,22 @@ teardown(DbName) -> config:delete("smoosh", "cleanup_index_files", false). t_default_channels(_) -> + ChannelStatus = maps:get(channels, status()), ?assertMatch( [ - {"index_cleanup", _}, - {"ratio_dbs", _}, - {"ratio_views", _}, - {"slack_dbs", _}, - {"slack_views", _}, - {"upgrade_dbs", _}, - {"upgrade_views", _} + index_cleanup, + ratio_dbs, + ratio_views, + slack_dbs, + slack_views, + upgrade_dbs, + upgrade_views ], - status() + lists:sort(maps:keys(ChannelStatus)) ), % If app hasn't started status won't crash application:stop(smoosh), - ?assertEqual([], status()). + ?assertEqual(#{channels => #{}}, status()). t_channels_recreated_on_crash(_) -> RatioDbsPid = get_channel_pid("ratio_dbs"), @@ -104,7 +105,8 @@ t_channels_recreated_on_crash(_) -> exit(RatioDbsPid, kill), meck:wait(1, smoosh_channel, start_link, 1, 3000), wait_for_channels(7), - ?assertMatch([_, {"ratio_dbs", _} | _], status()), + ChannelStatus = maps:get(channels, status()), + ?assertMatch(true, maps:is_key(ratio_dbs, ChannelStatus)), ?assertNotEqual(RatioDbsPid, get_channel_pid("ratio_dbs")). t_can_create_and_delete_channels(_) -> @@ -402,17 +404,17 @@ delete_doc(DbName, DDocId) -> status() -> {ok, Props} = smoosh:status(), - lists:keysort(1, Props). + Props. status(Channel) -> - case lists:keyfind(Channel, 1, status()) of - {_, Val} -> - Val, - Active = proplists:get_value(active, Val), - Starting = proplists:get_value(starting, Val), - WaitingInfo = proplists:get_value(waiting, Val), - Waiting = proplists:get_value(size, WaitingInfo), - {Active, Starting, Waiting}; + ChannelStatus = maps:get(channels, status()), + ChannelAtom = list_to_atom(Channel), + case maps:is_key(ChannelAtom, ChannelStatus) of + true -> + #{active := Active, starting := Starting, waiting := Waiting} = maps:get( + ChannelAtom, ChannelStatus + ), + {Active, Starting, maps:get(size, Waiting)}; false -> false end. @@ -443,7 +445,8 @@ wait_for_channels() -> wait_for_channels(N) when is_integer(N), N >= 0 -> WaitFun = fun() -> - case length(status()) of + ChannelStatus = maps:get(channels, status()), + case length(maps:keys(ChannelStatus)) of N -> ok; _ -> wait end