You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by va...@apache.org on 2024/08/05 20:07:44 UTC

(couchdb) branch main updated: Remove unique_integer bottleneck from couch_lru

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

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


The following commit(s) were added to refs/heads/main by this push:
     new 6531b8621 Remove unique_integer bottleneck from couch_lru
6531b8621 is described below

commit 6531b862185fd4c77128715c2f52dcd2e8c62360
Author: Nick Vatamaniuc <va...@gmail.com>
AuthorDate: Mon Aug 5 14:36:33 2024 -0400

    Remove unique_integer bottleneck from couch_lru
    
    Previously, couch_lru used `unique_integer([monotonic,positive])` to generate
    incrementing integer keys. That call take a global VM lock [1]:
    
    > Strictly monotonically increasing values are inherently quite expensive to
    generate and scales poorly.
    
    This is especially bad now that we have sharded couch_server instances, each
    with it's own LRU. We don't want them all to bottleneck waiting on each other
    to generate their integers.
    
    [1] https://www.erlang.org/doc/apps/erts/erlang.html#unique_integer/1
---
 src/couch/src/couch_lru.erl | 78 +++++++++++++++++++++++++++------------------
 1 file changed, 47 insertions(+), 31 deletions(-)

diff --git a/src/couch/src/couch_lru.erl b/src/couch/src/couch_lru.erl
index bf3f63a11..a062d5a3a 100644
--- a/src/couch/src/couch_lru.erl
+++ b/src/couch/src/couch_lru.erl
@@ -15,33 +15,35 @@
 
 -include("couch_server_int.hrl").
 
--type cache() :: {gb_trees:tree(), #{binary() => pos_integer()}}.
+-type cache() :: {non_neg_integer(), gb_trees:tree(), #{binary() => non_neg_integer()}}.
 
 -spec new() -> cache().
 new() ->
-    {gb_trees:empty(), #{}}.
+    {0, gb_trees:empty(), #{}}.
 
 -spec insert(binary(), cache()) -> cache().
-insert(DbName, {Tree, #{} = Map} = Cache) ->
+insert(DbName, {Counter, Tree, #{} = Map} = Cache) ->
     case Map of
         #{DbName := Old} ->
             update_int(Old, DbName, Cache);
         #{} ->
-            New = couch_util:unique_monotonic_integer(),
-            {gb_trees:insert(New, DbName, Tree), Map#{DbName => New}}
+            Counter1 = Counter + 1,
+            Tree1 = gb_trees:insert(Counter1, DbName, Tree),
+            Map1 = Map#{DbName => Counter1},
+            {Counter1, Tree1, Map1}
     end.
 
 %% Update bumps the entry but only if it already exists. If it doesn't exist,
 %% it won't be inserted.
 
 -spec update(binary(), cache()) -> cache().
-update(DbName, {Tree, #{} = Map} = Cache) ->
+update(DbName, {_Counter, _Tree, Map} = Cache) ->
     case Map of
         #{DbName := Old} ->
             update_int(Old, DbName, Cache);
         #{} ->
             % We closed this database before processing the update.  Ignore
-            {Tree, Map}
+            Cache
     end.
 
 %% Attempt to close the oldest idle database. This function also cleans deleted
@@ -50,19 +52,20 @@ update(DbName, {Tree, #{} = Map} = Cache) ->
 %% all bumped entries are lost as heap garbage.
 
 -spec close(cache()) -> {true, cache()} | false.
-close({Tree, _} = Cache) ->
+close({_Counter, Tree, #{} = _Map} = Cache) ->
     close_int(gb_trees:next(gb_trees:iterator(Tree)), Cache).
 
 %% internals
 
-update_int(Old, DbName, {Tree, #{} = Map}) ->
-    New = couch_util:unique_monotonic_integer(),
-    Tree1 = gb_trees:insert(New, DbName, gb_trees:delete(Old, Tree)),
-    {Tree1, Map#{DbName := New}}.
+update_int(Old, DbName, {Counter, Tree, #{} = Map}) ->
+    Counter1 = Counter + 1,
+    Tree1 = gb_trees:insert(Counter1, DbName, gb_trees:delete(Old, Tree)),
+    Map1 = Map#{DbName := Counter1},
+    {Counter1, Tree1, Map1}.
 
-close_int(none, {_Tree, #{}}) ->
+close_int(none, {_Counter, _Tree, #{}}) ->
     false;
-close_int({Lru, DbName, Iter}, {Tree, #{} = Map} = Cache) ->
+close_int({Lru, DbName, Iter}, {Counter, Tree, #{} = Map} = Cache) ->
     CouchDbs = couch_server:couch_dbs(DbName),
     CouchDbsPidToName = couch_server:couch_dbs_pid_to_name(DbName),
 
@@ -73,16 +76,20 @@ close_int({Lru, DbName, Iter}, {Tree, #{} = Map} = Cache) ->
                     true = ets:delete(CouchDbs, DbName),
                     true = ets:delete(CouchDbsPidToName, Pid),
                     exit(Pid, kill),
-                    {true, {gb_trees:delete(Lru, Tree), maps:remove(DbName, Map)}};
+                    Tree1 = gb_trees:delete(Lru, Tree),
+                    Map1 = maps:remove(DbName, Map),
+                    {true, {Counter, Tree1, Map1}};
                 false ->
                     true = couch_server:unlock(CouchDbs, DbName),
                     couch_stats:increment_counter([couchdb, couch_server, lru_skip]),
-                    close_int(gb_trees:next(Iter), update(DbName, Cache))
+                    Cache1 = update(DbName, Cache),
+                    close_int(gb_trees:next(Iter), Cache1)
             end;
         false ->
-            NewTree = gb_trees:delete(Lru, Tree),
-            NewIter = gb_trees:iterator(NewTree),
-            close_int(gb_trees:next(NewIter), {NewTree, maps:remove(DbName, Map)})
+            Tree1 = gb_trees:delete(Lru, Tree),
+            Iter1 = gb_trees:iterator(Tree1),
+            Map1 = maps:remove(DbName, Map),
+            close_int(gb_trees:next(Iter1), {Counter, Tree1, Map1})
     end.
 
 -ifdef(TEST).
@@ -114,15 +121,16 @@ couch_lru_test_() ->
 
 t_new(_) ->
     Cache = new(),
-    ?assertMatch({_, _}, Cache),
-    {Tree, Map} = Cache,
+    ?assertMatch({_, _, _}, Cache),
+    {0, Tree, Map} = Cache,
     ?assert(gb_trees:is_empty(Tree)),
     ?assert(is_map(Map) andalso map_size(Map) == 0).
 
 t_insert(_) ->
-    {Tree, Map} = insert(?DB1, new()),
+    {Counter, Tree, Map} = insert(?DB1, new()),
     ?assertEqual(1, gb_trees:size(Tree)),
     ?assertEqual(1, map_size(Map)),
+    ?assertEqual(1, Counter),
     ?assertMatch(#{?DB1 := _}, Map),
     #{?DB1 := Int} = Map,
     ?assert(is_integer(Int)),
@@ -133,26 +141,29 @@ t_insert_duplicate(_) ->
     % We technically allow this, but is this right? Should we always use update
     % instead which would reap the old LRU entry
     %
-    {Tree, Map} = insert(?DB1, insert(?DB1, new())),
+    {Counter, Tree, Map} = insert(?DB1, insert(?DB1, new())),
     ?assertEqual(1, gb_trees:size(Tree)),
     ?assertEqual(1, map_size(Map)),
+    ?assertEqual(2, Counter),
     ?assertMatch(#{?DB1 := _}, Map),
     ?assertMatch([{_, ?DB1}], gb_trees:to_list(Tree)).
 
 t_update(_) ->
     % Insert followed by update.
-    {Tree, Map} = update(?DB1, insert(?DB1, new())),
+    {Counter, Tree, Map} = update(?DB1, insert(?DB1, new())),
     ?assertEqual(1, gb_trees:size(Tree)),
     ?assertEqual(1, map_size(Map)),
+    ?assertEqual(2, Counter),
     ?assertMatch(#{?DB1 := _}, Map),
     #{?DB1 := Int} = Map,
     ?assertEqual([{Int, ?DB1}], gb_trees:to_list(Tree)).
 
 t_update_non_existent(_) ->
     % Updating a non-existent item is a no-op
-    {Tree, Map} = update(?DB2, insert(?DB1, new())),
+    {Counter, Tree, Map} = update(?DB2, insert(?DB1, new())),
     ?assertEqual(1, gb_trees:size(Tree)),
     ?assertEqual(1, map_size(Map)),
+    ?assertEqual(1, Counter),
     ?assertMatch(#{?DB1 := _}, Map),
     #{?DB1 := Int} = Map,
     ?assertEqual([{Int, ?DB1}], gb_trees:to_list(Tree)).
@@ -164,10 +175,11 @@ t_close_unlocked_idle({Dbs, DbsPids, [Pid1, _]}) ->
     % There is one db handle and it's idle. It should get closed.
     Cache = insert(?DB1, new()),
     Res = close(Cache),
-    ?assertMatch({true, {_, #{}}}, Res),
-    {true, {Tree, Map}} = Res,
+    ?assertMatch({true, {_, _, #{}}}, Res),
+    {true, {Counter, Tree, Map}} = Res,
     ?assert(gb_trees:is_empty(Tree)),
     ?assert(is_map(Map) andalso map_size(Map) == 0),
+    ?assertEqual(1, Counter),
     ?assertNot(is_process_alive(Pid1)),
     ?assertEqual([], ets:lookup(Dbs, ?DB1)),
     ?assertEqual([], ets:lookup(DbsPids, Pid1)).
@@ -185,17 +197,20 @@ t_close_bump_busy_all({_Dbs, _DbsPids, [Pid1, _]}) ->
 
 t_close_bump_busy_one({Dbs, DbsPids, [Pid1, Pid2]}) ->
     % One busy handle gets bumped, the idle one closed.
-    {_, Map} = Cache = insert(?DB2, insert(?DB1, new())),
+    {Counter, _, Map} = Cache = insert(?DB2, insert(?DB1, new())),
     meck:expect(couch_db, is_idle, fun
         (?DB1) -> false;
         (?DB2) -> true
     end),
     meck:reset(couch_stats),
-    {true, {Tree1, Map1}} = close(Cache),
+    {true, {Counter1, Tree1, Map1}} = close(Cache),
     ?assert(is_process_alive(Pid1)),
     ?assertNot(is_process_alive(Pid2)),
     ?assertEqual(1, ets:info(Dbs, size)),
     ?assertEqual(1, ets:info(DbsPids, size)),
+    ?assertEqual(2, Counter),
+    % Counter1 is 3 because we bumped the busy one. So 2 inserts + 1 update.
+    ?assertEqual(3, Counter1),
     ?assertEqual(1, meck:num_calls(couch_stats, increment_counter, 1)),
     ?assert(is_process_alive(Pid1)),
     ?assertEqual(1, gb_trees:size(Tree1)),
@@ -211,12 +226,13 @@ t_close_entry_one_is_missing({Dbs, _, [_Pid1, Pid2]}) ->
     % it should be auto-removed from LRU
     Cache = insert(?DB2, insert(?DB1, new())),
     ets:delete(Dbs, ?DB1),
-    {true, {Tree1, Map1}} = close(Cache),
+    {true, {Counter1, Tree1, Map1}} = close(Cache),
     % One entry was removed, one was closed. There should be
     % nothing left in the LRU.
     ?assert(gb_trees:is_empty(Tree1)),
     ?assert(is_map(Map1)),
     ?assertEqual(0, map_size(Map1)),
+    ?assertEqual(2, Counter1),
     ?assertNot(is_process_alive(Pid2)).
 
 t_multiple_inserts_and_close(_) ->
@@ -225,7 +241,7 @@ t_multiple_inserts_and_close(_) ->
     % during the close traversal, if one instances was busy and the other idle,
     % we'd crash with function clause in a function clause in gb_trees:delete/2
     % (See issue #5166 for details)
-    {_, Map} = Cache = insert(?DB1, insert(?DB1, new())),
+    {_, _, _} = Cache = insert(?DB1, insert(?DB1, new())),
     meck:expect(couch_db, is_idle, 1, meck:seq([meck:val(false), meck:val(true)])),
     ?assertEqual(false, close(Cache)).