This is an automated email from the ASF dual-hosted git repository.

nickva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/couchdb-ioq.git


The following commit(s) were added to refs/heads/main by this push:
     new 75e4a92  Replace khash with erlang maps
75e4a92 is described below

commit 75e4a92b713e58cad19f240012bae8e6a56120f6
Author: Artúr Poór <[email protected]>
AuthorDate: Thu Jul 9 15:44:39 2026 +0200

    Replace khash with erlang maps
    
    Cloudant's version IOQ is still using the khash NIF-based library to 
efficiently manage dictionaries. With the modernization of Erlang maps, this is 
not needed any more and as such, it is now time to replace that.
---
 IOQ2.md             |   6 +--
 include/ioq.hrl     |   3 +-
 src/ioq_config.erl  |  62 +++++++++-------------
 src/ioq_server2.erl | 147 +++++++++++++++++++++++++---------------------------
 4 files changed, 99 insertions(+), 119 deletions(-)

diff --git a/IOQ2.md b/IOQ2.md
index 849fbe2..d2b43c2 100644
--- a/IOQ2.md
+++ b/IOQ2.md
@@ -373,7 +373,7 @@ module and are directly usable for easy testing. You can 
also see the full list
 of priority values from those priority data structures like so:
 
 ```erlang
-([email protected])14> khash:to_list(ShardP).
+([email protected])14> maps:to_list(ShardP).
 [{{<<"shards/00000000-1fffffff/foo">>,interactive},1.0e3},
  {{<<"shards/00000000-1fffffff/foo/pizza_db">>,db_update},
    1.5}]
@@ -472,7 +472,7 @@ ioq_config:set_max_priority(55555.0, "Expand priority 
space").
 
 Both IOQ1 and IOQ2 have a dedupe feature that will avoid performing the same
 read multiple times in parallel. In IOQ1 this operation scanned through lists
-and could become a considerable resource hog. In IOQ2 this is a simple khash
+and could become a considerable resource hog. In IOQ2 this is a simple map
 lookup and should not be a problem. You should *not* need to ever disable this.
 For whatever reason if you need to, you can do so with:
 
@@ -624,7 +624,7 @@ Afterwards, you'll be able to utilize the setters as 
expected.
 ## ioq_server2:get_state
 
 You can see a human readable representation of the IOQ2 server state with the
-following block of code. The output is "human readable" in that the khash and
+following block of code. The output is "human readable" in that the map and
 hqueue data structures have been transformed into lists so the contents can be
 viewed. This fetches the state of the `ioq_server_1` pid. If you want a
 different pid you'll need to manually `gen_server:call` into it.
diff --git a/include/ioq.hrl b/include/ioq.hrl
index 7c4a4a4..2628f61 100644
--- a/include/ioq.hrl
+++ b/include/ioq.hrl
@@ -87,4 +87,5 @@
     | {view_io_priority(), dbname(), group_id()}
     | {search, dbname(), group_id()}.
 -type ioq_request() :: #ioq_request{}.
-
+-type priority() :: float(). %% should be non_negative_float().
+-type ioq_priority_map() :: #{any() => priority()}.
diff --git a/src/ioq_config.erl b/src/ioq_config.erl
index 3dfe5e4..6e52188 100644
--- a/src/ioq_config.erl
+++ b/src/ioq_config.erl
@@ -174,7 +174,7 @@ set_config(Section, Key, Value, Reason) ->
     ok = config:set(Section, Key, Value, Reason).
 
 
--spec build_shard_priorities() -> {ok, khash:khash()}.
+-spec build_shard_priorities() -> {ok, ioq_priority_map()}.
 build_shard_priorities() ->
     Configs = lists:foldl(
         fun({Key0, Val}, Acc) ->
@@ -195,31 +195,30 @@ build_shard_priorities() ->
     build_shard_priorities(Configs).
 
 
--spec build_shard_priorities([{any(), float()}]) -> {ok, khash:khash()}.
+-spec build_shard_priorities([{any(), float()}]) -> {ok, ioq_priority_map()}.
 build_shard_priorities(Configs) ->
     init_config_priorities(Configs).
 
 
--spec build_user_priorities() -> {ok, khash:khash()}.
+-spec build_user_priorities() -> {ok, ioq_priority_map()}.
 build_user_priorities() ->
     build_user_priorities(config:get("ioq2.users")).
 
 
--spec build_user_priorities([{any(), float()}]) -> {ok, khash:khash()}.
+-spec build_user_priorities([{any(), float()}]) -> {ok, ioq_priority_map()}.
 build_user_priorities(Configs0) ->
     Configs = [{list_to_binary(K), to_float(V)} || {K,V} <- Configs0],
     init_config_priorities(Configs).
 
 
--spec build_class_priorities() -> {ok, khash:khash()}.
+-spec build_class_priorities() -> {ok, ioq_priority_map()}.
 build_class_priorities() ->
     build_class_priorities(config:get("ioq2.classes")).
 
 
--spec build_class_priorities([{any(), float()}]) -> {ok, khash:khash()}.
+-spec build_class_priorities([{any(), float()}]) -> {ok, ioq_priority_map()}.
 build_class_priorities(Configs0) ->
-    {ok, ClassP} = khash:new(),
-    ok = add_default_class_priorities(ClassP),
+    ClassP = add_default_class_priorities(#{}),
     Configs = [{list_to_existing_atom(K), to_float(V)} || {K,V} <- Configs0],
     init_config_priorities(Configs, ClassP).
 
@@ -235,14 +234,9 @@ parse_shard_string(ShardString) ->
     end.
 
 
--spec add_default_class_priorities(khash:khash()) -> ok.
+-spec add_default_class_priorities(ioq_priority_map()) -> ioq_priority_map().
 add_default_class_priorities(ClassP) ->
-    ok = lists:foreach(
-        fun({Class, Priority}) ->
-            ok = khash:put(ClassP, Class, Priority)
-        end,
-        ?DEFAULT_CLASS_PRIORITIES
-    ).
+    maps:merge(maps:from_list(?DEFAULT_CLASS_PRIORITIES), ClassP).
 
 
 -spec to_float(any()) -> float().
@@ -269,7 +263,7 @@ to_float(_, Default) ->
     Default.
 
 
--spec prioritize(ioq_request(), khash:khash(), khash:khash(), khash:khash()) ->
+-spec prioritize(ioq_request(), ioq_priority_map(), ioq_priority_map(), 
ioq_priority_map()) ->
     float().
 prioritize(#ioq_request{} = Req, ClassP, UserP, ShardP) ->
     #ioq_request{
@@ -277,28 +271,20 @@ prioritize(#ioq_request{} = Req, ClassP, UserP, ShardP) ->
         shard=Shard,
         class=Class
     } = Req,
-    UP = get_priority(UserP, User),
-    CP = get_priority(ClassP, Class),
-    SP = get_priority(ShardP, {Shard, Class}),
+    UP = get_priority(User, UserP),
+    CP = get_priority(Class, ClassP),
+    SP = get_priority({Shard, Class}, ShardP),
     UP * CP * SP.
 
 
--spec init_config_priorities([{any(), float()}]) -> {ok, khash:khash()}.
+-spec init_config_priorities([{any(), float()}]) -> {ok, ioq_priority_map()}.
 init_config_priorities(Configs) ->
-    {ok, Hash} = khash:new(),
-    init_config_priorities(Configs, Hash).
-
+    init_config_priorities(Configs, #{}).
 
--spec init_config_priorities([{any(), float()}], khash:khash()) ->
-    {ok, khash:khash()}.
-init_config_priorities(Configs, Hash) ->
-    ok = lists:foreach(
-        fun({Key, Val}) ->
-            ok = khash:put(Hash, Key, Val)
-        end,
-        Configs
-    ),
-    {ok, Hash}.
+-spec init_config_priorities([{any(), float()}], ioq_priority_map()) ->
+    {ok, ioq_priority_map()}.
+init_config_priorities(Configs, Map) ->
+    {ok, maps:merge(Map, maps:from_list(Configs))}.
 
 
 -spec check_priority(atom(), binary(), binary()) -> float().
@@ -317,11 +303,11 @@ check_priority(Class, User, Shard0) ->
     prioritize(Req, ClassP, UserP, ShardP).
 
 
-get_priority(KH, Key) ->
-    get_priority(KH, Key, ?DEFAULT_PRIORITY).
+get_priority(Key, KH) ->
+    get_priority(Key, KH, ?DEFAULT_PRIORITY).
 
 
-get_priority(_KH, undefined, Default) ->
+get_priority(undefined, _KH, Default) ->
     Default;
-get_priority(KH, Key, Default) ->
-    khash:get(KH, Key, Default).
+get_priority(Key, KH, Default) ->
+    maps:get(Key, KH, Default).
diff --git a/src/ioq_server2.erl b/src/ioq_server2.erl
index c2fc215..ac9cd0c 100644
--- a/src/ioq_server2.erl
+++ b/src/ioq_server2.erl
@@ -51,14 +51,14 @@
 
 
 -record(state, {
-    reqs :: khash:khash(),
-    waiters :: khash:khash(),
+    reqs = #{} :: ioq_requests(),
+    waiters = #{} :: ioq_waiters(),
     queue :: hqueue:hqueue(),
     concurrency = ?DEFAULT_IOQ2_CONCURRENCY :: pos_integer(),
     iterations = 0 :: non_neg_integer(),
-    class_p :: khash:khash(),  %% class priorities
-    user_p :: khash:khash(),   %% user priorities
-    shard_p :: khash:khash(),  %% shard priorities
+    class_p :: ioq_priority_map(),  %% class priorities
+    user_p :: ioq_priority_map(),   %% user priorities
+    shard_p :: ioq_priority_map(),  %% shard priorities
     scale_factor = ?DEFAULT_SCALE_FACTOR :: float(),
     dedupe = true :: boolean(),
     resize_limit = ?DEFAULT_RESIZE_LIMIT :: pos_integer(),
@@ -68,10 +68,10 @@
     max_priority = ?DEFAULT_MAX_PRIORITY :: float()
 }).
 
-
 -type state() :: #state{}.
 -type waiter_key() :: {pid(), integer()} | pos_integer().
--type priority() :: float(). %% should be non_negative_float().
+-type ioq_requests() :: #{reference() => ioq_request()}.
+-type ioq_waiters() :: #{waiter_key() => [gen_server:from()]}.
 
 %% Hacky queue_depth type due to existing fixed element lists for JSON in API
 %% Actual type is:
@@ -198,7 +198,7 @@ get_queue_depths() ->
 
 -spec get_queue_depths([ioq_request()]) -> queue_depths().
 get_queue_depths(Reqs) ->
-    {ok, Users0} = khash:new(),
+    Users0 = #{},
     {Compaction, Replication, Low, Users} = lists:foldl(
         fun
             (#ioq_request{class=db_compact}, {C, R, L, U}) ->
@@ -210,12 +210,7 @@ get_queue_depths(Reqs) ->
             (#ioq_request{class=low}, {C, R, L, U}) ->
                 {C, R, L+1, U};
             (#ioq_request{class=Class, user=User}, {C, R, L, U}) ->
-                [UI0, UDB0, UV0] = case khash:get(U, User) of
-                    undefined ->
-                        [0,0,0];
-                    UC0 ->
-                        UC0
-                end,
+                [UI0, UDB0, UV0] = maps:get(User, U, [0,0,0]),
                 UC = case Class of
                     db_update ->
                         [UI0, UDB0+1, UV0];
@@ -224,8 +219,7 @@ get_queue_depths(Reqs) ->
                     _Interactive ->
                         [UI0+1, UDB0, UV0]
                 end,
-                ok = khash:put(U, User, UC),
-                {C, R, L, U}
+                {C, R, L, maps:put(User, UC, U)}
         end,
         {0, 0, 0, Users0},
         Reqs
@@ -234,7 +228,7 @@ get_queue_depths(Reqs) ->
         {compaction, Compaction},
         {replication, Replication},
         {low, Low},
-        {channels, {khash:to_list(Users)}}
+        {channels, {maps:to_list(Users)}}
     ].
 
 
@@ -272,7 +266,7 @@ get_state() ->
     get_state(?SERVER_ID(1)).
 
 
-%% Returns a mutated #state{} with list representations of khash/hqueue objects
+%% Returns a mutated #state{} with list representations of map/hqueue objects
 -spec get_state(atom()) -> any().
 get_state(Server) ->
     gen_server:call(Server, get_state, infinity).
@@ -293,12 +287,8 @@ start_link(Name, SID, Bind) ->
 
 init([Name, SID]) ->
     {ok, HQ} = hqueue:new(),
-    {ok, Reqs} = khash:new(),
-    {ok, Waiters} = khash:new(),
     State = #state{
         queue = HQ,
-        reqs = Reqs,
-        waiters = Waiters,
         server_name = Name,
         scheduler_id = SID
     },
@@ -307,11 +297,11 @@ init([Name, SID]) ->
 
 handle_call(get_state, _From, State) ->
     Resp = State#state{
-        user_p = khash:to_list(State#state.user_p),
-        class_p = khash:to_list(State#state.class_p),
-        shard_p = khash:to_list(State#state.shard_p),
-        reqs = khash:to_list(State#state.reqs),
-        waiters = khash:to_list(State#state.waiters),
+        user_p = maps:to_list(State#state.user_p),
+        class_p = maps:to_list(State#state.class_p),
+        shard_p = maps:to_list(State#state.shard_p),
+        reqs = maps:to_list(State#state.reqs),
+        waiters = maps:to_list(State#state.waiters),
         queue = hqueue:to_list(State#state.queue)
     },
 
@@ -328,7 +318,7 @@ handle_call(get_concurrency, _From, State) ->
 handle_call({set_concurrency, C}, _From, State) when is_integer(C), C > 0 ->
     {reply, {ok, State#state.concurrency}, State#state{concurrency = C}, 0};
 handle_call(get_reqs, _From, #state{reqs=Reqs}=State) ->
-    {reply, khash:to_list(Reqs), State, 0};
+    {reply, maps:to_list(Reqs), State, 0};
 handle_call(get_pending_reqs, _From, #state{queue=HQ}=State) ->
     {reply, hqueue:to_list(HQ), State, 0};
 handle_call(get_counters, _From, State) ->
@@ -343,31 +333,27 @@ handle_cast(_Msg, State) ->
     {noreply, State, 0}.
 
 
-handle_info({Ref, Reply}, #state{reqs = Reqs} = State) ->
-    case khash:get(Reqs, Ref) of
-        undefined ->
-            ok;
-        #ioq_request{ref=Ref}=Req ->
-            ok = khash:del(Reqs, Ref),
-            TResponse = os:timestamp(),
-            ServiceTime = time_delta(TResponse, Req#ioq_request.tsub),
-            IOWait = time_delta(TResponse, Req#ioq_request.t0),
-            couch_stats:update_histogram(
-                [couchdb, io_queue2, svctm], ServiceTime),
-            couch_stats:update_histogram([couchdb, io_queue2, iowait], IOWait),
-            erlang:demonitor(Ref, [flush]),
-            send_response(State#state.waiters, Req, Reply)
-    end,
+handle_info({Ref, Reply}, #state{reqs = Reqs0} = State0) when is_map_key(Ref, 
Reqs0) ->
+    {#ioq_request{ref = Ref} = Req, Reqs} = maps:take(Ref, Reqs0),
+    TResponse = os:timestamp(),
+    ServiceTime = time_delta(TResponse, Req#ioq_request.tsub),
+    IOWait = time_delta(TResponse, Req#ioq_request.t0),
+    couch_stats:update_histogram(
+        [couchdb, io_queue2, svctm], ServiceTime),
+    couch_stats:update_histogram([couchdb, io_queue2, iowait], IOWait),
+    erlang:demonitor(Ref, [flush]),
+    State = send_response(State0, Req, Reply),
+    State1 = State#state{reqs = Reqs},
+    {noreply, State1, 0};
+handle_info({_Ref, _Reply}, #state{reqs = #{}} = State) ->
     {noreply, State, 0};
-handle_info({'DOWN', Ref, _, _, Reason}, #state{reqs = Reqs} = State) ->
-    case khash:get(Reqs, Ref) of
-        undefined ->
-            ok;
-        #ioq_request{ref=Ref}=Req ->
-            couch_stats:increment_counter([couchdb, io_queue2, io_errors]),
-            ok = khash:del(Reqs, Ref),
-            send_response(State#state.waiters, Req, {'EXIT', Reason})
-    end,
+handle_info({'DOWN', Ref, _, _, Reason}, #state{reqs = Reqs0} = State0) when 
is_map_key(Ref, Reqs0) ->
+    {#ioq_request{ref = Ref} = Req, Reqs} = maps:take(Ref, Reqs0),
+    couch_stats:increment_counter([couchdb, io_queue2, io_errors]),
+    State = send_response(State0, Req, {'EXIT', Reason}),
+    State1 = State#state{reqs = Reqs},
+    {noreply, State1, 0};
+handle_info({'DOWN', _Ref, _, _, _Reason}, #state{reqs = #{}} = State) ->
     {noreply, State, 0};
 handle_info(timeout, State) ->
     {noreply, maybe_submit_request(State)};
@@ -421,7 +407,7 @@ update_config_int(State) ->
 
 -spec maybe_submit_request(state()) -> state().
 maybe_submit_request(#state{reqs=Reqs, concurrency=C}=State) ->
-    NumReqs = khash:size(Reqs),
+    NumReqs = maps:size(Reqs),
     case NumReqs < C of
         true ->
             case make_next_request(State) of
@@ -451,14 +437,13 @@ make_next_request(#state{queue=HQ}=State) ->
 submit_request(Req, #state{iterations=I, resize_limit=RL}=State) when I >= RL 
->
     ok = hqueue:scale_by(State#state.queue, State#state.scale_factor),
     submit_request(Req, State#state{iterations=0});
-submit_request(Req, #state{iterations=Iterations}=State) ->
+submit_request(Req, #state{iterations=Iterations, reqs=Reqs0}=State) ->
     #ioq_request{
         fd = Fd,
         msg = Call,
         class = Class,
         t0 = T0
     } = Req,
-    #state{reqs = Reqs} = State,
 
     % make the request
     Ref = erlang:monitor(process, Fd),
@@ -472,15 +457,15 @@ submit_request(Req, #state{iterations=Iterations}=State) 
->
     couch_stats:increment_counter([couchdb, io_queue2, Class, count]),
     couch_stats:increment_counter([couchdb, io_queue2, RW, count]),
     couch_stats:update_histogram([couchdb, io_queue2, submit_delay], Latency),
-    khash:put(Reqs, Ref, Req#ioq_request{tsub=SubmitTime, ref=Ref}),
-    State#state{iterations=Iterations+1}.
+    Reqs = Reqs0#{Ref => Req#ioq_request{tsub=SubmitTime, ref=Ref}},
+    State#state{iterations = Iterations + 1, reqs = Reqs}.
 
 
--spec send_response(khash:khash(), ioq_request(), term()) -> [ok].
-send_response(Waiters, #ioq_request{key=Key}, Reply) ->
-    Waiting = khash:get(Waiters, Key),
-    khash:del(Waiters, Key),
-    [gen_server:reply(W, Reply) || W <- Waiting].
+-spec send_response(state(), ioq_request(), term()) -> state().
+send_response(#state{waiters = Waiters0} = State0, #ioq_request{key = Key}, 
Reply) ->
+    {Waiting, Waiters} = maps:take(Key, Waiters0),
+    [gen_server:reply(W, Reply) || W <- Waiting],
+    State0#state{waiters = Waiters}.
 
 
 -spec waiter_key(ioq_request(), state()) -> {waiter_key(), state()}.
@@ -495,7 +480,7 @@ waiter_key(Req, State) ->
 
 
 -spec enqueue_request(ioq_request(), state()) -> state().
-enqueue_request(Req, #state{queue=HQ, waiters=Waiters}=State0) ->
+enqueue_request(Req, #state{queue = HQ} = State0) ->
     #ioq_request{
         from = From,
         msg = Msg
@@ -506,20 +491,21 @@ enqueue_request(Req, #state{queue=HQ, 
waiters=Waiters}=State0) ->
     couch_stats:increment_counter([couchdb, io_queue2, queued]),
     couch_stats:increment_counter([couchdb, io_queue2, RW, queued]),
 
-    case khash:get(State#state.waiters, ReqKey, not_found) of
-        not_found ->
+    Waiters0 = State#state.waiters,
+    Waiters = case Waiters0 of
+        #{ReqKey := Pids} ->
+            couch_stats:increment_counter([couchdb, io_queue2, merged]),
+            Waiters0#{ReqKey := [From | Pids]};
+        #{} ->
             Priority = prioritize_request(Req, State),
             Req1 = Req#ioq_request{
                 key = ReqKey,
                 init_priority = Priority
             },
             hqueue:insert(HQ, Priority, Req1),
-            khash:put(State#state.waiters, ReqKey, [From]);
-        Pids ->
-            couch_stats:increment_counter([couchdb, io_queue2, merged]),
-            khash:put(Waiters, ReqKey, [From | Pids])
+            Waiters0#{ReqKey => [From]}
     end,
-    State.
+    State#state{waiters = Waiters}.
 
 bypass(_Msg, {Class, _Shard}) ->
     config:get_boolean("ioq2.bypass", atom_to_list(Class), false);
@@ -799,16 +785,21 @@ queue_depths_test_() ->
         {replication, 3},
         {low, 1},
         {channels, {[
-            {<<"foo">>, [3,1,4]},
-            {<<"bar">>, [1,3,1]}
+            {<<"bar">>, [1,3,1]},
+            {<<"foo">>, [3,1,4]}
         ]}}
     ],
 
+    Actual = get_queue_depths(Reqs),
+    {channels, {Channels}} = lists:keyfind(channels, 1, Actual),
+    SortedByUsers = lists:keysort(1, Channels),
+    ActualSorted = lists:keyreplace(channels, 1, Actual, {channels, 
{SortedByUsers}}),
+
     {
         "Test queue depth stats",
         ?_assertEqual(
             Expected,
-            get_queue_depths(Reqs)
+            ActualSorted
         )
     }.
 
@@ -1095,15 +1086,17 @@ random_server(Servers) ->
     lists:nth(rand:uniform(length(Servers)), Servers).
 
 
-test_io_error(#state{waiters=Waiters, reqs=Reqs}=State) ->
+test_io_error(#state{waiters=Waiters, reqs=Reqs}=State0) ->
     Key = asdf,
     Ref = make_ref(),
     RefTag = make_ref(),
     Req = #ioq_request{ref=Ref, key=Key},
-    khash:put(Waiters, Key, [{self(), RefTag}]),
-    khash:put(Reqs, Ref, Req),
+    State1 = State0#state{
+        waiters = Waiters#{Key => [{self(), RefTag}]},
+        reqs = Reqs#{Ref => Req}
+    },
     Error = {exit, foo},
-    {noreply, _State1, 0} = handle_info({'DOWN', Ref, baz, zab, Error}, State),
+    {noreply, _State2, 0} = handle_info({'DOWN', Ref, baz, zab, Error}, 
State1),
     Resp = receive
         {RefTag, {'EXIT', Error}} ->
             {ok, Error};

Reply via email to