You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by da...@apache.org on 2017/06/16 21:40:26 UTC

[couchdb] branch optimize-ddoc-cache updated (cc82a13 -> db1bef3)

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

davisp pushed a change to branch optimize-ddoc-cache
in repository https://gitbox.apache.org/repos/asf/couchdb.git.


 discard cc82a13  Refresh cache entries periodically
 discard 1c39eb6  ss - remove lru
 discard b2d6d46  Generalize cache entry specific logic
 discard 022fc60  ss - remove lru
 discard 242dc46  ss - remove lru
 discard 59f786b  Remove LRU from ddoc_cache
     new 8348446  Remove LRU from ddoc_cache
     new 3615fa6  Generalize cache entry specific logic
     new db1bef3  Refresh cache entries periodically

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (cc82a13)
            \
             N -- N -- N   refs/heads/optimize-ddoc-cache (db1bef3)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:

-- 
To stop receiving notification emails like this one, please contact
['"commits@couchdb.apache.org" <co...@couchdb.apache.org>'].

[couchdb] 03/03: Refresh cache entries periodically

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davisp pushed a commit to branch optimize-ddoc-cache
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit db1bef3b3a7b53ee255e061413c4ab58c554b81b
Author: Paul J. Davis <pa...@gmail.com>
AuthorDate: Fri Jun 16 16:35:15 2017 -0500

    Refresh cache entries periodically
    
    This patch restores the general runtime correctness of ddoc_cache. The
    refresher is responsible for updating cache entries either when they
    change or when they need to be removed. This clears up the previous
    possible race condition between ddoc_cache_opener and ddoc_cache_lru
    that could have left a stale entry in the cache. This same exact race
    condition existed with ets_lru but the max_age of ets_lru would cover up
    the issue if it ever happened (which would be fairly rare).
    
    This change also finally addresses the major issue in ddoc_cache where
    entries were forcibly evicted after max_age. This lead to situations
    where we would have a thundering herd of clients trying to all insert
    the same cache item just after it expired. In some extreme cases this
    would lead to a backup of the ddoc_cache message queue that was
    unrecoverable. Hopefully this change addresses that behavior.
---
 src/ddoc_cache/src/ddoc_cache.hrl           |   3 +-
 src/ddoc_cache/src/ddoc_cache_lru.erl       |  87 +++++++++++++++++----
 src/ddoc_cache/src/ddoc_cache_refresher.erl | 117 ++++++++++++++++++++++++++++
 3 files changed, 192 insertions(+), 15 deletions(-)

diff --git a/src/ddoc_cache/src/ddoc_cache.hrl b/src/ddoc_cache/src/ddoc_cache.hrl
index 8545914..6986211 100644
--- a/src/ddoc_cache/src/ddoc_cache.hrl
+++ b/src/ddoc_cache/src/ddoc_cache.hrl
@@ -22,7 +22,8 @@
 
 -record(entry, {
     key,
-    val
+    val,
+    pid
 }).
 
 -record(opener, {
diff --git a/src/ddoc_cache/src/ddoc_cache_lru.erl b/src/ddoc_cache/src/ddoc_cache_lru.erl
index ce53d1d..dbcaa28 100644
--- a/src/ddoc_cache/src/ddoc_cache_lru.erl
+++ b/src/ddoc_cache/src/ddoc_cache_lru.erl
@@ -20,6 +20,8 @@
 
     insert/2,
     accessed/1,
+    refresh/2,
+    remove/1,
     evict/2
 ]).
 
@@ -61,6 +63,14 @@ accessed(Key) ->
     gen_server:cast(?MODULE, {accessed, Key}).
 
 
+refresh(Key, Val) ->
+    gen_server:call(?MODULE, {refresh, Key, Val}).
+
+
+remove(Key) ->
+    gen_server:call(?MODULE, {remove, Key}).
+
+
 -spec evict(dbname(), [docid()]) -> ok.
 evict(DbName, DDocIds) ->
     gen_server:cast(?MODULE, {evict, DbName, DDocIds}).
@@ -97,12 +107,51 @@ handle_call({insert, Key, Val}, _From, St) ->
         time = Time
     } = St,
     NewTime = Time + 1,
-    true = ets:insert(?CACHE, #entry{key = Key, val = Val}),
+    Pid = ddoc_cache_refresher:spawn(Key),
+    true = ets:insert(?CACHE, #entry{key = Key, val = Val, pid = Pid}),
     true = ets:insert(?ATIMES, {NewTime, Key}),
     ok = khash:put(Keys, NewTime),
     store_key(Dbs, Key),
     {reply, ok, trim(St#st{time = NewTime})};
 
+handle_call({refresh, Key, Val}, _From, St) ->
+    #st{
+        keys = Keys
+    } = St,
+    case khash:lookup(Keys, Key) of
+        {value, _} ->
+            ets:update_element(?CACHE, Key, {#entry.val, Val}),
+            {reply, ok, St};
+        not_found ->
+            {reply, evicted, St}
+    end;
+
+handle_call({remove, Key}, _From, St) ->
+    #st{
+        keys = TimeKeys,
+        dbs = Dbs
+    } = St,
+    case khash:lookup(TimeKeys, Key) of
+        {value, ATime} ->
+            remove_entry(St, Key, ATime),
+            DbName = ddoc_cache_entry:dbname(Key),
+            DDocId = ddoc_cache_entry:ddocid(Key),
+            {value, DDocIds} = khash:lookup(Dbs, DbName),
+            {value, Keys} = khash:lookup(DDocIds, DDocId),
+            ok = khash:del(Keys, Key),
+            case khash:size(Keys) of
+                0 -> khash:del(DDocIds, DDocId);
+                _ -> ok
+            end,
+            case khash:size(DDocIds) of
+                0 -> khash:del(Dbs, DDocId);
+                _ -> ok
+            end;
+        not_found ->
+            ok
+    end,
+    {reply, ok, St};
+
 handle_call(Msg, _From, St) ->
     {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.
 
@@ -115,6 +164,8 @@ handle_cast({accessed, Key}, St) ->
     NewTime = Time + 1,
     case khash:lookup(Keys, Key) of
         {value, OldTime} ->
+            [#entry{pid = Pid}] = ets:lookup(?CACHE, Key),
+            true = is_process_alive(Pid),
             true = ets:delete(?ATIMES, OldTime),
             true = ets:insert(?ATIMES, {NewTime, Key}),
             ok = khash:put(Keys, NewTime);
@@ -135,15 +186,15 @@ handle_cast({evict, _, _} = Msg, St) ->
 
 handle_cast({do_evict, DbName}, St) ->
     #st{
-        keys = KeyTimes,
         dbs = Dbs
     } = St,
     case khash:lookup(Dbs, DbName) of
         {value, DDocIds} ->
             khash:fold(DDocIds, fun(_, Keys, _) ->
                 khash:fold(Keys, fun(Key, _, _) ->
-                    {value, Time} = khash:lookup(KeyTimes, Key),
-                    remove(St, Time)
+                    [#entry{pid = Pid}] = ets:lookup(?CACHE, Key),
+                    ddoc_cache_refresher:stop(Pid),
+                    remove_entry(St, Key)
                 end, nil)
             end, nil),
             khash:del(Dbs, DbName);
@@ -154,7 +205,6 @@ handle_cast({do_evict, DbName}, St) ->
 
 handle_cast({do_evict, DbName, DDocIds}, St) ->
     #st{
-        keys = KeyTimes,
         dbs = Dbs
     } = St,
     case khash:lookup(Dbs, DbName) of
@@ -163,8 +213,9 @@ handle_cast({do_evict, DbName, DDocIds}, St) ->
                 case khash:lookup(DDocIds, DDocId) of
                     {value, Keys} ->
                         khash:fold(Keys, fun(Key, _, _) ->
-                            {value, Time} = khash:lookup(KeyTimes, Key),
-                            remove(St, Time)
+                            [#entry{pid = Pid}] = ets:lookup(?CACHE, Key),
+                            ddoc_cache_refresher:stop(Pid),
+                            remove_entry(St, Key)
                         end, nil);
                     not_found ->
                         ok
@@ -239,21 +290,29 @@ trim(St) ->
         true ->
             case ets:first(?ATIMES) of
                 '$end_of_table' ->
-                    St;
+                    ok;
                 ATime ->
-                    trim(remove(St, ATime))
+                    [{ATime, Key}] = ets:lookup(?ATIMES, ATime),
+                    remove_entry(St, Key, ATime),
+                    trim(St)
             end;
         false ->
-            St
+            ok
     end.
 
 
-remove(St, ATime) ->
+remove_entry(St, Key) ->
+    #st{
+        keys = Keys
+    } = St,
+    {value, ATime} = khash:lookup(Keys, Key),
+    remove_entry(St, Key, ATime).
+
+
+remove_entry(St, Key, ATime) ->
     #st{
         keys = Keys
     } = St,
-    {value, Key} = khash:lookup(Keys, ATime),
     true = ets:delete(?CACHE, Key),
     true = ets:delete(?ATIMES, ATime),
-    ok = khash:del(Keys, Key),
-    St.
+    ok = khash:del(Keys, Key).
diff --git a/src/ddoc_cache/src/ddoc_cache_refresher.erl b/src/ddoc_cache/src/ddoc_cache_refresher.erl
new file mode 100644
index 0000000..8e8a6ef
--- /dev/null
+++ b/src/ddoc_cache/src/ddoc_cache_refresher.erl
@@ -0,0 +1,117 @@
+% 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(ddoc_cache_refresher).
+-behaviour(gen_server).
+-vsn(1).
+
+
+-export([
+    spawn/1,
+    stop/1
+]).
+
+-export([
+    init/1,
+    terminate/2,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3
+]).
+
+
+-include("ddoc_cache.hrl").
+
+
+-record(st, {
+    key
+}).
+
+
+-define(REFRESH_TIMEOUT, 67000).
+
+
+spawn(Key) ->
+    proc_lib:spawn(?MODULE, init, [{self(), Key}]).
+
+
+stop(Pid) ->
+    gen_server:cast(Pid, stop).
+
+
+init({Parent, Key}) ->
+    process_flag(trap_exit, true),
+    erlang:monitor(process, Parent),
+    gen_server:enter_loop(?MODULE, [], #st{key = Key}, ?REFRESH_TIMEOUT).
+
+
+terminate(_Reason, _St) ->
+    ok.
+
+
+handle_call(Msg, _From, St) ->
+    {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.
+
+
+handle_cast(stop, St) ->
+    {stop, normal, St};
+
+handle_cast(Msg, St) ->
+    {stop, {invalid_cast, Msg}, St}.
+
+
+handle_info(timeout, St) ->
+    ddoc_cache_entry:spawn_link(St#st.key),
+    {noreply, St};
+
+handle_info({'EXIT', _, {open_ok, Key, Resp}}, #st{key = Key} = St) ->
+    Self = self(),
+    case Resp of
+        {ok, Val} ->
+            case ets:lookup(?CACHE, Key) of
+                [] ->
+                    % We were evicted
+                    {stop, normal, St};
+                [#entry{key = Key, val = Val, pid = Self}] ->
+                    % Value hasn't changed, do nothing
+                    {noreply, St};
+                [#entry{key = Key, pid = Self}] ->
+                    % Value changed, update cache
+                    case ddoc_cache_lru:refresh(Key, Val) of
+                        ok ->
+                            {noreply, St};
+                        evicted ->
+                            {stop, normal, St}
+                    end
+            end;
+        _Else ->
+            ddoc_cache_lru:remove(Key),
+            {stop, normal, St}
+    end;
+
+handle_info({'EXIT', _, _}, #st{key = Key} = St) ->
+    % Somethign went wrong trying to refresh the cache
+    % so bail in the interest of safety.
+    ddoc_cache_lru:remove(Key),
+    {stop, normal, St};
+
+handle_info({'DOWN', _, _, _, _}, St) ->
+    % ddoc_cache_lru died, so we will as well
+    {stop, normal, St};
+
+handle_info(Msg, St) ->
+    {stop, {invalid_info, Msg}, St}.
+
+
+code_change(_OldVsn, St, _Extra) ->
+    {ok, St}.

-- 
To stop receiving notification emails like this one, please contact
"commits@couchdb.apache.org" <co...@couchdb.apache.org>.

[couchdb] 02/03: Generalize cache entry specific logic

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davisp pushed a commit to branch optimize-ddoc-cache
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 3615fa68f6ed7af834481a0b982b2dad0b81eeea
Author: Paul J. Davis <pa...@gmail.com>
AuthorDate: Fri Jun 16 15:18:28 2017 -0500

    Generalize cache entry specific logic
    
    There was a lot of guff going on with all of the various special cased
    function clauses to separate out different behavior. This makes things a
    lot more straight forward in terms of isolating the different logic.
    
    The one slight downside is that there's a somewhat complex change to how
    we manage evictions now that the ets table keys are opaque. Though on
    the plus side this changed turned an ets table scan into a set of O(1)
    khash lookups, so there's that at least.
    
    This will still not work in production as we're not kicking out entries
    when their existence changes. Patch three in this series will address
    that last detail.
---
 src/ddoc_cache/src/ddoc_cache.app.src              |   2 +-
 src/ddoc_cache/src/ddoc_cache.erl                  |  62 ++-------
 src/ddoc_cache/src/ddoc_cache_entry.erl            |  82 +++++++++++
 ...c_cache.app.src => ddoc_cache_entry_custom.erl} |  47 +++----
 ...c_cache.app.src => ddoc_cache_entry_ddocid.erl} |  50 ++++---
 ...che.app.src => ddoc_cache_entry_ddocid_rev.erl} |  52 ++++---
 ...pp.src => ddoc_cache_entry_validation_funs.erl} |  54 ++++----
 src/ddoc_cache/src/ddoc_cache_lru.erl              | 102 +++++++++-----
 src/ddoc_cache/src/ddoc_cache_opener.erl           | 150 +++------------------
 src/ddoc_cache/src/ddoc_cache_tables.erl           |   2 +-
 10 files changed, 280 insertions(+), 323 deletions(-)

diff --git a/src/ddoc_cache/src/ddoc_cache.app.src b/src/ddoc_cache/src/ddoc_cache.app.src
index 084895e..3b6617b 100644
--- a/src/ddoc_cache/src/ddoc_cache.app.src
+++ b/src/ddoc_cache/src/ddoc_cache.app.src
@@ -35,5 +35,5 @@
         couch_log,
         couch_stats
     ]},
-    {mod, {ddoc_cache_app, []}},
+    {mod, {ddoc_cache_app, []}}
 ]}.
diff --git a/src/ddoc_cache/src/ddoc_cache.erl b/src/ddoc_cache/src/ddoc_cache.erl
index 07d89ac..f9eea9f 100644
--- a/src/ddoc_cache/src/ddoc_cache.erl
+++ b/src/ddoc_cache/src/ddoc_cache.erl
@@ -33,66 +33,32 @@ start() ->
 stop() ->
     application:stop(ddoc_cache).
 
+
 open_doc(DbName, DocId) ->
-    Key = {DbName, DocId, '_'},
-    case ddoc_cache_opener:match_newest(Key) of
-        {ok, _} = Resp ->
-            couch_stats:increment_counter([ddoc_cache, hit]),
-            Resp;
-        missing ->
-            couch_stats:increment_counter([ddoc_cache, miss]),
-            ddoc_cache_opener:open_doc(DbName, DocId);
-        recover ->
-            couch_stats:increment_counter([ddoc_cache, recovery]),
-            ddoc_cache_opener:recover_doc(DbName, DocId)
-    end.
+    Key = {ddoc_cache_entry_ddocid, {DbName, DocId}},
+    ddoc_cache_opener:open(Key).
+
 
 open_doc(DbName, DocId, RevId) ->
-    Key = {DbName, DocId, RevId},
-    case ddoc_cache_opener:lookup(Key) of
-        {ok, _} = Resp ->
-            couch_stats:increment_counter([ddoc_cache, hit]),
-            Resp;
-        missing ->
-            couch_stats:increment_counter([ddoc_cache, miss]),
-            ddoc_cache_opener:open_doc(DbName, DocId, RevId);
-        recover ->
-            couch_stats:increment_counter([ddoc_cache, recovery]),
-            ddoc_cache_opener:recover_doc(DbName, DocId, RevId)
-    end.
+    Key = {ddoc_cache_entry_ddocid_rev, {DbName, DocId, RevId}},
+    ddoc_cache_opener:open(Key).
+
 
 open_validation_funs(DbName) ->
-    Key = {DbName, validation_funs},
-    case ddoc_cache_opener:lookup(Key) of
-        {ok, _} = Resp ->
-            couch_stats:increment_counter([ddoc_cache, hit]),
-            Resp;
-        missing ->
-            couch_stats:increment_counter([ddoc_cache, miss]),
-            ddoc_cache_opener:open_validation_funs(DbName);
-        recover ->
-            couch_stats:increment_counter([ddoc_cache, recovery]),
-            ddoc_cache_opener:recover_validation_funs(DbName)
-    end.
+    Key = {ddoc_cache_entry_validation_funs, DbName},
+    ddoc_cache_opener:open(Key).
+
 
 open_custom(DbName, Mod) ->
-    Key = {DbName, Mod},
-    case ddoc_cache_opener:lookup(Key) of
-        {ok, _} = Resp ->
-            couch_stats:increment_counter([ddoc_cache, hit]),
-            Resp;
-        missing ->
-            couch_stats:increment_counter([ddoc_cache, miss]),
-            ddoc_cache_opener:open_doc(DbName, Mod);
-        recover ->
-            couch_stats:increment_counter([ddoc_cache, recovery]),
-            Mod:recover(DbName)
-    end.
+    Key = {ddoc_cache_entry_custom, {DbName, Mod}},
+    ddoc_cache_opener:open(Key).
+
 
 evict(ShardDbName, DDocIds) ->
     DbName = mem3:dbname(ShardDbName),
     ddoc_cache_lru:evict(DbName, DDocIds).
 
+
 open(DbName, validation_funs) ->
     open_validation_funs(DbName);
 open(DbName, Module) when is_atom(Module) ->
diff --git a/src/ddoc_cache/src/ddoc_cache_entry.erl b/src/ddoc_cache/src/ddoc_cache_entry.erl
new file mode 100644
index 0000000..a1bcb3a
--- /dev/null
+++ b/src/ddoc_cache/src/ddoc_cache_entry.erl
@@ -0,0 +1,82 @@
+% 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(ddoc_cache_entry).
+
+
+-export([
+    dbname/1,
+    ddocid/1,
+    spawn_link/1,
+    handle_resp/1,
+    open/1,
+    recover/1
+]).
+
+-export([
+    do_open/1,
+    do_open/2
+]).
+
+dbname({Mod, Arg}) ->
+    Mod:dbname(Arg).
+
+
+ddocid({Mod, Arg}) ->
+    Mod:ddocid(Arg).
+
+
+spawn_link(Key) ->
+    erlang:spawn_link(?MODULE, do_open, [Key]).
+
+
+handle_resp({open_ok, _Key, Resp}) ->
+    Resp;
+
+handle_resp({open_error, _Key, Type, Reason, Stack}) ->
+    erlang:raise(Type, Reason, Stack);
+
+handle_resp(Else) ->
+    erlang:error({ddoc_cache_entry, Else}).
+
+
+open(Key) ->
+    {_Pid, Ref} = erlang:spawn_monitor(?MODULE, do_open, [Key]),
+    receive
+        {'DOWN', Ref, _, _, {open_ok, Key, Resp}} ->
+            Resp;
+        {'DOWN', Ref, _, _, {open_error, Key, Class, Reason, Stack}} ->
+            erlang:raise(Class, Reason, Stack);
+        {'DOWN', Ref, _, _, Other} ->
+            erlang:error({ddoc_cache_entry, Other})
+    end.
+
+
+recover({Mod, Arg}) ->
+    Mod:recover(Arg).
+
+
+do_open(Key) ->
+    do_open(Key, false).
+
+
+do_open({Mod, Arg} = Key, DoInsert) ->
+    try Mod:recover(Arg) of
+        {ok, Resp} when DoInsert ->
+            ddoc_cache_lru:insert(Key, Resp),
+            erlang:exit({open_ok, Key, {ok, Resp}});
+        Resp ->
+            erlang:exit({open_ok, Key, Resp})
+    catch T:R ->
+        S = erlang:get_stacktrace(),
+        erlang:exit({open_error, Key, T, R, S})
+    end.
diff --git a/src/ddoc_cache/src/ddoc_cache.app.src b/src/ddoc_cache/src/ddoc_cache_entry_custom.erl
similarity index 50%
copy from src/ddoc_cache/src/ddoc_cache.app.src
copy to src/ddoc_cache/src/ddoc_cache_entry_custom.erl
index 084895e..d858ad6 100644
--- a/src/ddoc_cache/src/ddoc_cache.app.src
+++ b/src/ddoc_cache/src/ddoc_cache_entry_custom.erl
@@ -10,30 +10,23 @@
 % License for the specific language governing permissions and limitations under
 % the License.
 
-{application, ddoc_cache, [
-    {description, "Design Document Cache"},
-    {vsn, git},
-    {modules, [
-        ddoc_cache,
-        ddoc_cache_app,
-        ddoc_cache_opener,
-        ddoc_cache_sup,
-        ddoc_cache_util
-    ]},
-    {registered, [
-        ddoc_cache_tables,
-        ddoc_cache_lru,
-        ddoc_cache_opener
-    ]},
-    {applications, [
-        kernel,
-        stdlib,
-        crypto,
-        couch_event,
-        mem3,
-        fabric,
-        couch_log,
-        couch_stats
-    ]},
-    {mod, {ddoc_cache_app, []}},
-]}.
+-module(ddoc_cache_entry_custom).
+
+
+-export([
+    dbname/1,
+    ddocid/1,
+    recover/1
+]).
+
+
+dbname({DbName, _}) ->
+    DbName.
+
+
+ddocid(_) ->
+    no_ddocid.
+
+
+recover({DbName, Mod}) ->
+    Mod:recover(DbName).
diff --git a/src/ddoc_cache/src/ddoc_cache.app.src b/src/ddoc_cache/src/ddoc_cache_entry_ddocid.erl
similarity index 50%
copy from src/ddoc_cache/src/ddoc_cache.app.src
copy to src/ddoc_cache/src/ddoc_cache_entry_ddocid.erl
index 084895e..cac9abc 100644
--- a/src/ddoc_cache/src/ddoc_cache.app.src
+++ b/src/ddoc_cache/src/ddoc_cache_entry_ddocid.erl
@@ -10,30 +10,26 @@
 % License for the specific language governing permissions and limitations under
 % the License.
 
-{application, ddoc_cache, [
-    {description, "Design Document Cache"},
-    {vsn, git},
-    {modules, [
-        ddoc_cache,
-        ddoc_cache_app,
-        ddoc_cache_opener,
-        ddoc_cache_sup,
-        ddoc_cache_util
-    ]},
-    {registered, [
-        ddoc_cache_tables,
-        ddoc_cache_lru,
-        ddoc_cache_opener
-    ]},
-    {applications, [
-        kernel,
-        stdlib,
-        crypto,
-        couch_event,
-        mem3,
-        fabric,
-        couch_log,
-        couch_stats
-    ]},
-    {mod, {ddoc_cache_app, []}},
-]}.
+-module(ddoc_cache_entry_ddocid).
+
+
+-export([
+    dbname/1,
+    ddocid/1,
+    recover/1
+]).
+
+
+-include_lib("couch/include/couch_db.hrl").
+
+
+dbname({DbName, _}) ->
+    DbName.
+
+
+ddocid({_, DDocId}) ->
+    DDocId.
+
+
+recover({DbName, DDocId}) ->
+    fabric:open_doc(DbName, DDocId, [ejson_body, ?ADMIN_CTX]).
diff --git a/src/ddoc_cache/src/ddoc_cache.app.src b/src/ddoc_cache/src/ddoc_cache_entry_ddocid_rev.erl
similarity index 50%
copy from src/ddoc_cache/src/ddoc_cache.app.src
copy to src/ddoc_cache/src/ddoc_cache_entry_ddocid_rev.erl
index 084895e..012abab 100644
--- a/src/ddoc_cache/src/ddoc_cache.app.src
+++ b/src/ddoc_cache/src/ddoc_cache_entry_ddocid_rev.erl
@@ -10,30 +10,28 @@
 % License for the specific language governing permissions and limitations under
 % the License.
 
-{application, ddoc_cache, [
-    {description, "Design Document Cache"},
-    {vsn, git},
-    {modules, [
-        ddoc_cache,
-        ddoc_cache_app,
-        ddoc_cache_opener,
-        ddoc_cache_sup,
-        ddoc_cache_util
-    ]},
-    {registered, [
-        ddoc_cache_tables,
-        ddoc_cache_lru,
-        ddoc_cache_opener
-    ]},
-    {applications, [
-        kernel,
-        stdlib,
-        crypto,
-        couch_event,
-        mem3,
-        fabric,
-        couch_log,
-        couch_stats
-    ]},
-    {mod, {ddoc_cache_app, []}},
-]}.
+-module(ddoc_cache_entry_ddocid_rev).
+
+
+-export([
+    dbname/1,
+    ddocid/1,
+    recover/1
+]).
+
+
+-include_lib("couch/include/couch_db.hrl").
+
+
+dbname({DbName, _, _}) ->
+    DbName.
+
+
+ddocid({_, DDocId, _}) ->
+    DDocId.
+
+
+recover({DbName, DDocId, Rev}) ->
+    Opts = [ejson_body, ?ADMIN_CTX],
+    {ok, [Resp]} = fabric:open_revs(DbName, DDocId, [Rev], Opts),
+    Resp.
diff --git a/src/ddoc_cache/src/ddoc_cache.app.src b/src/ddoc_cache/src/ddoc_cache_entry_validation_funs.erl
similarity index 50%
copy from src/ddoc_cache/src/ddoc_cache.app.src
copy to src/ddoc_cache/src/ddoc_cache_entry_validation_funs.erl
index 084895e..3d43f7a 100644
--- a/src/ddoc_cache/src/ddoc_cache.app.src
+++ b/src/ddoc_cache/src/ddoc_cache_entry_validation_funs.erl
@@ -10,30 +10,30 @@
 % License for the specific language governing permissions and limitations under
 % the License.
 
-{application, ddoc_cache, [
-    {description, "Design Document Cache"},
-    {vsn, git},
-    {modules, [
-        ddoc_cache,
-        ddoc_cache_app,
-        ddoc_cache_opener,
-        ddoc_cache_sup,
-        ddoc_cache_util
-    ]},
-    {registered, [
-        ddoc_cache_tables,
-        ddoc_cache_lru,
-        ddoc_cache_opener
-    ]},
-    {applications, [
-        kernel,
-        stdlib,
-        crypto,
-        couch_event,
-        mem3,
-        fabric,
-        couch_log,
-        couch_stats
-    ]},
-    {mod, {ddoc_cache_app, []}},
-]}.
+-module(ddoc_cache_entry_validation_funs).
+
+
+-export([
+    dbname/1,
+    ddocid/1,
+    recover/1
+]).
+
+
+dbname(DbName) ->
+    DbName.
+
+
+ddocid(_) ->
+    no_ddocid.
+
+
+recover(DbName) ->
+    {ok, DDocs} = fabric:design_docs(mem3:dbname(DbName)),
+    Funs = lists:flatmap(fun(DDoc) ->
+        case couch_doc:get_validate_doc_fun(DDoc) of
+            nil -> [];
+            Fun -> [Fun]
+        end
+    end, DDocs),
+    {ok, Funs}.
diff --git a/src/ddoc_cache/src/ddoc_cache_lru.erl b/src/ddoc_cache/src/ddoc_cache_lru.erl
index 9d8c397..ce53d1d 100644
--- a/src/ddoc_cache/src/ddoc_cache_lru.erl
+++ b/src/ddoc_cache/src/ddoc_cache_lru.erl
@@ -20,7 +20,6 @@
 
     insert/2,
     accessed/1,
-    evict/1,
     evict/2
 ]).
 
@@ -42,7 +41,8 @@
 
 
 -record(st, {
-    keys,
+    keys, % key -> time
+    dbs, % dbname -> docid -> key -> []
     time,
     max_size,
     evictor
@@ -54,7 +54,7 @@ start_link() ->
 
 
 insert(Key, Val) ->
-    gen_server:call(?MODULE, {insert, Key}).
+    gen_server:call(?MODULE, {insert, Key, Val}).
 
 
 accessed(Key) ->
@@ -68,12 +68,14 @@ evict(DbName, DDocIds) ->
 
 init(_) ->
     {ok, Keys} = khash:new(),
+    {ok, Dbs} = khash:new(),
     {ok, Evictor} = couch_event:link_listener(
             ?MODULE, handle_db_event, nil, [all_dbs]
         ),
     MaxSize = config:get_integer("ddoc_cache", "max_size", 1000),
     {ok, #st{
         keys = Keys,
+        dbs = Dbs,
         time = 0,
         max_size = MaxSize,
         evictor = Evictor
@@ -91,19 +93,21 @@ terminate(_Reason, St) ->
 handle_call({insert, Key, Val}, _From, St) ->
     #st{
         keys = Keys,
+        dbs = Dbs,
         time = Time
     } = St,
     NewTime = Time + 1,
     true = ets:insert(?CACHE, #entry{key = Key, val = Val}),
     true = ets:insert(?ATIMES, {NewTime, Key}),
     ok = khash:put(Keys, NewTime),
+    store_key(Dbs, Key),
     {reply, ok, trim(St#st{time = NewTime})};
 
 handle_call(Msg, _From, St) ->
     {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.
 
 
-handle_cast({accessed, Key}, _St) ->
+handle_cast({accessed, Key}, St) ->
     #st{
         keys = Keys,
         time = Time
@@ -129,36 +133,51 @@ handle_cast({evict, _, _} = Msg, St) ->
     gen_server:abcast(mem3:nodes(), ?MODULE, Msg),
     {noreply, St};
 
-handle_cast({do_evict, DbName} = Msg, St) ->
-    Pattern = #entry{
-        key = {DbName, '$1', '_'},
-        val = '_',
-        _ = '_'
-    },
-    DDocIds = lists:flatten(ets:match(?CACHE, Pattern)),
-    handle_cast({do_evict, DbName, DDocIds});
+handle_cast({do_evict, DbName}, St) ->
+    #st{
+        keys = KeyTimes,
+        dbs = Dbs
+    } = St,
+    case khash:lookup(Dbs, DbName) of
+        {value, DDocIds} ->
+            khash:fold(DDocIds, fun(_, Keys, _) ->
+                khash:fold(Keys, fun(Key, _, _) ->
+                    {value, Time} = khash:lookup(KeyTimes, Key),
+                    remove(St, Time)
+                end, nil)
+            end, nil),
+            khash:del(Dbs, DbName);
+        not_found ->
+            ok
+    end,
+    {noreply, St};
 
 handle_cast({do_evict, DbName, DDocIds}, St) ->
-    Pattern = #entry{
-        key = {DbName, '$1'},
-        val = '_',
-        _ = '_'
-    },
-    CustomKeys = lists:flatten(ets:match(?CACHE, Pattern)),
-    lists:foreach(fun(Mod) ->
-        ets:delete(?CACHE, {DbName, Mod})
-    end, CustomKeys),
-    lists:foreach(fun(DDocId) ->
-        RevPattern = #entry{
-            key = {DbName, DDocId, '$1'},
-            val = '_',
-            _ = '_'
-        },
-        Revs = lists:flatten(ets:match(?CACHE, RevPattern)),
-        lists:foreach(fun(Rev) ->
-            ets:delete(?CACHE, {DbName, DDocId, Rev})
-        end, Revs)
-    end, DDocIds),
+    #st{
+        keys = KeyTimes,
+        dbs = Dbs
+    } = St,
+    case khash:lookup(Dbs, DbName) of
+        {value, DDocIds} ->
+            lists:foreach(fun(DDocId) ->
+                case khash:lookup(DDocIds, DDocId) of
+                    {value, Keys} ->
+                        khash:fold(Keys, fun(Key, _, _) ->
+                            {value, Time} = khash:lookup(KeyTimes, Key),
+                            remove(St, Time)
+                        end, nil);
+                    not_found ->
+                        ok
+                end,
+                khash:del(DDocIds, DDocId)
+            end, [no_ddocid | DDocIds]),
+            case khash:size(DDocIds) of
+                0 -> khash:del(Dbs, DbName);
+                _ -> ok
+            end;
+        not_found ->
+            ok
+    end,
     {noreply, St};
 
 handle_cast(Msg, St) ->
@@ -192,6 +211,25 @@ handle_db_event(_DbName, _Event, St) ->
     {ok, St}.
 
 
+store_key(Dbs, Key) ->
+    DbName = ddoc_cache_entry:dbname(Key),
+    DDocId = ddoc_cache_entry:ddocid(Key),
+    case khash:lookup(Dbs, DbName) of
+        {value, DDocIds} ->
+            case khash:lookup(DDocIds, DDocId) of
+                {value, Keys} ->
+                    khash:put(Keys, Key, []);
+                not_found ->
+                    {ok, Keys} = khash:from_list([{Key, []}]),
+                    khash:put(DDocIds, DDocId, Keys)
+            end;
+        not_found ->
+            {ok, Keys} = khash:from_list([{Key, []}]),
+            {ok, DDocIds} = khash:from_list([{DDocId, Keys}]),
+            khash:put(Dbs, DDocId, DDocIds)
+    end.
+
+
 trim(St) ->
     #st{
         keys = Keys,
diff --git a/src/ddoc_cache/src/ddoc_cache_opener.erl b/src/ddoc_cache/src/ddoc_cache_opener.erl
index a4adffc..654a98a 100644
--- a/src/ddoc_cache/src/ddoc_cache_opener.erl
+++ b/src/ddoc_cache/src/ddoc_cache_opener.erl
@@ -32,17 +32,7 @@
 ]).
 
 -export([
-    open_doc/2,
-    open_doc/3,
-    open_validation_funs/1,
-    lookup/1,
-    match_newest/1,
-    recover_doc/2,
-    recover_doc/3,
-    recover_validation_funs/1
-]).
--export([
-    fetch_doc_data/1
+    open/1
 ]).
 
 -include("ddoc_cache.hrl").
@@ -58,75 +48,22 @@
 start_link() ->
     gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
 
--spec open_doc(dbname(), docid()) -> {ok, #doc{}}.
-open_doc(DbName, DocId) ->
-    Resp = gen_server:call(?MODULE, {open, {DbName, DocId}}, infinity),
-    handle_open_response(Resp).
-
--spec open_doc(dbname(), docid(), revision()) -> {ok, #doc{}}.
-open_doc(DbName, DocId, Rev) ->
-    Resp = gen_server:call(?MODULE, {open, {DbName, DocId, Rev}}, infinity),
-    handle_open_response(Resp).
-
--spec open_validation_funs(dbname()) -> {ok, [fun()]}.
-open_validation_funs(DbName) ->
-    Resp = gen_server:call(?MODULE, {open, {DbName, validation_funs}}, infinity),
-    handle_open_response(Resp).
 
-
-lookup(Key) ->
-    try ets:lookup(?CACHE, Key) of
-        [#entry{key = Key, val = Val}] ->
+open(Key) ->
+    case ets:lookup(?CACHE, Key) of
+        [#entry{val = Val}] ->
+            couch_stats:increment_counter([ddoc_cache, hit]),
             ddoc_cache_lru:accessed(Key),
             {ok, Val};
-        _ ->
-            missing
-    catch
-        error:badarg ->
-            recover
-    end.
-
-match_newest(Key) ->
-    Pattern = #entry{
-        key = Key,
-        val = '_',
-        _ = '_'
-    },
-    try ets:match_object(?CACHE, Pattern) of
         [] ->
-            missing;
-        Entries ->
-            Docs = lists:map(fun(#entry{key = K, val = V}) ->
-                ddoc_cache_lru:accessed(K),
-                V
-            end, Entries),
-            Sorted = lists:sort(
-                fun (#doc{deleted=DelL, revs=L}, #doc{deleted=DelR, revs=R}) ->
-                    {not DelL, L} > {not DelR, R}
-                end, Docs),
-            {ok, hd(Sorted)}
-    catch
-        error:badarg ->
-            recover
+            couch_stats:increment_counter([ddoc_cache, miss]),
+            Resp = gen_server:call(?MODULE, {open, Key}, infinity),
+            ddoc_cache_entry:handle_resp(Resp);
+        recover ->
+            couch_stats:increment_counter([ddoc_cache, recovery]),
+            ddoc_cache_entry:open(Key)
     end.
 
-recover_doc(DbName, DDocId) ->
-    fabric:open_doc(DbName, DDocId, [ejson_body, ?ADMIN_CTX]).
-
-recover_doc(DbName, DDocId, Rev) ->
-    {ok, [Resp]} = fabric:open_revs(DbName, DDocId, [Rev], [ejson_body, ?ADMIN_CTX]),
-    Resp.
-
-recover_validation_funs(DbName) ->
-    {ok, DDocs} = fabric:design_docs(mem3:dbname(DbName)),
-    Funs = lists:flatmap(fun(DDoc) ->
-        case couch_doc:get_validate_doc_fun(DDoc) of
-            nil -> [];
-            Fun -> [Fun]
-        end
-    end, DDocs),
-    {ok, Funs}.
-
 
 init(_) ->
     process_flag(trap_exit, true),
@@ -141,7 +78,7 @@ handle_call({open, OpenerKey}, From, St) ->
             ets:insert(?OPENERS, O#opener{clients=[From | Clients]}),
             {noreply, St};
         [] ->
-            Pid = spawn_link(?MODULE, fetch_doc_data, [OpenerKey]),
+            Pid = ddoc_cache_entry:spawn_link(OpenerKey),
             ets:insert(?OPENERS, #opener{key=OpenerKey, pid=Pid, clients=[From]}),
             {noreply, St}
     end;
@@ -156,8 +93,8 @@ handle_cast({do_evict, _} = Msg, St) ->
     gen_server:cast(?LRU, Msg),
     {noreply, St};
 
-handle_cast({do_evict, _, _}, St) ->
-    gen_server:cast(?LRU, Msg)
+handle_cast({do_evict, _, _} = Msg, St) ->
+    gen_server:cast(?LRU, Msg),
     {noreply, St};
 
 handle_cast(Msg, St) ->
@@ -168,8 +105,8 @@ handle_info({'EXIT', _Pid, {open_ok, OpenerKey, Resp}}, St) ->
     respond(OpenerKey, {open_ok, Resp}),
     {noreply, St};
 
-handle_info({'EXIT', _Pid, {open_error, OpenerKey, Type, Error}}, St) ->
-    respond(OpenerKey, {open_error, Type, Error}),
+handle_info({'EXIT', _Pid, {open_error, OpenerKey, Type, Reason, Stack}}, St) ->
+    respond(OpenerKey, {open_error, Type, Reason, Stack}),
     {noreply, St};
 
 handle_info({'EXIT', Pid, Reason}, St) ->
@@ -189,61 +126,8 @@ handle_info(Msg, St) ->
 code_change(_OldVsn, State, _Extra) ->
     {ok, State}.
 
--spec fetch_doc_data({dbname(), validation_funs}) -> no_return();
-                    ({dbname(), atom()}) -> no_return();
-                    ({dbname(), docid()}) -> no_return();
-                    ({dbname(), docid(), revision()}) -> no_return().
-fetch_doc_data({DbName, validation_funs}=OpenerKey) ->
-    {ok, Funs} = recover_validation_funs(DbName),
-    ok = ddoc_cache_lru:insert(OpenerKey, Funs),
-    exit({open_ok, OpenerKey, {ok, Funs}});
-fetch_doc_data({DbName, Mod}=OpenerKey) when is_atom(Mod) ->
-    % This is not actually a docid but rather a custom cache key.
-    % Treat the argument as a code module and invoke its recover function.
-    try Mod:recover(DbName) of
-        {ok, Result} ->
-            ok = ddoc_cache_lru:insert(OpenerKey, Result),
-            exit({open_ok, OpenerKey, {ok, Result}});
-        Else ->
-            exit({open_ok, OpenerKey, Else})
-    catch
-        Type:Reason ->
-            exit({open_error, OpenerKey, Type, Reason})
-    end;
-fetch_doc_data({DbName, DocId}=OpenerKey) ->
-    try recover_doc(DbName, DocId) of
-        {ok, Doc} ->
-            {RevDepth, [RevHash| _]} = Doc#doc.revs,
-            Rev = {RevDepth, RevHash},
-            ok = ddoc_cache_lru:insert({DbName, DocId, Rev}, Doc),
-            exit({open_ok, OpenerKey, {ok, Doc}});
-        Else ->
-            exit({open_ok, OpenerKey, Else})
-    catch
-        Type:Reason ->
-            exit({open_error, OpenerKey, Type, Reason})
-    end;
-fetch_doc_data({DbName, DocId, Rev}=OpenerKey) ->
-    try recover_doc(DbName, DocId, Rev) of
-        {ok, Doc} ->
-            ok = ddoc_cache_lru:insert({DbName, DocId, Rev}, Doc),
-            exit({open_ok, OpenerKey, {ok, Doc}});
-        Else ->
-            exit({open_ok, OpenerKey, Else})
-    catch
-        Type:Reason ->
-            exit({open_error, OpenerKey, Type, Reason})
-    end.
-
-handle_open_response(Resp) ->
-    case Resp of
-        {open_ok, Value} -> Value;
-        {open_error, throw, Error} -> throw(Error);
-        {open_error, error, Error} -> erlang:error(Error);
-        {open_error, exit, Error} -> exit(Error)
-    end.
 
 respond(OpenerKey, Resp) ->
     [#opener{clients=Clients}] = ets:lookup(?OPENERS, OpenerKey),
-    _ = [gen_server:reply(C, Resp) || C <- Clients],
+    [gen_server:reply(C, Resp) || C <- Clients],
     ets:delete(?OPENERS, OpenerKey).
diff --git a/src/ddoc_cache/src/ddoc_cache_tables.erl b/src/ddoc_cache/src/ddoc_cache_tables.erl
index 9b35943..86cc9b3 100644
--- a/src/ddoc_cache/src/ddoc_cache_tables.erl
+++ b/src/ddoc_cache/src/ddoc_cache_tables.erl
@@ -40,7 +40,7 @@ init(_) ->
     BaseOpts = [public, named_table],
     ets:new(?CACHE, [set, {read_concurrency, true}] ++ BaseOpts),
     ets:new(?ATIMES, [sorted_set] ++ BaseOpts),
-    ets:new(?OPENING, [set, {keypos, #opener.key}] ++ BaseOpts),
+    ets:new(?OPENERS, [set, {keypos, #opener.key}] ++ BaseOpts),
     {ok, nil}.
 
 

-- 
To stop receiving notification emails like this one, please contact
"commits@couchdb.apache.org" <co...@couchdb.apache.org>.

[couchdb] 01/03: Remove LRU from ddoc_cache

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davisp pushed a commit to branch optimize-ddoc-cache
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 834844601b131716beb343fc504e1d166e8348c2
Author: Paul J. Davis <pa...@gmail.com>
AuthorDate: Fri Jun 16 13:29:47 2017 -0500

    Remove LRU from ddoc_cache
    
    This is only a part of the necessary work to optimize the ddoc cache.
    This will not work in production because the ets_lru max_age is not
    implemented so once a ddoc is in the cache it won't be removed unless
    everything stops using it (which for a busy database will be never
    because of the match_newest code).
---
 src/ddoc_cache/src/ddoc_cache.app.src    |   7 +-
 src/ddoc_cache/src/ddoc_cache.erl        |   2 +-
 src/ddoc_cache/src/ddoc_cache.hrl        |  32 +++++
 src/ddoc_cache/src/ddoc_cache_lru.erl    | 221 +++++++++++++++++++++++++++++++
 src/ddoc_cache/src/ddoc_cache_opener.erl | 123 ++++++-----------
 src/ddoc_cache/src/ddoc_cache_sup.erl    |  35 ++---
 src/ddoc_cache/src/ddoc_cache_tables.erl |  64 +++++++++
 7 files changed, 370 insertions(+), 114 deletions(-)

diff --git a/src/ddoc_cache/src/ddoc_cache.app.src b/src/ddoc_cache/src/ddoc_cache.app.src
index a64b2f5..084895e 100644
--- a/src/ddoc_cache/src/ddoc_cache.app.src
+++ b/src/ddoc_cache/src/ddoc_cache.app.src
@@ -21,6 +21,7 @@
         ddoc_cache_util
     ]},
     {registered, [
+        ddoc_cache_tables,
         ddoc_cache_lru,
         ddoc_cache_opener
     ]},
@@ -29,16 +30,10 @@
         stdlib,
         crypto,
         couch_event,
-        ets_lru,
         mem3,
         fabric,
         couch_log,
         couch_stats
     ]},
     {mod, {ddoc_cache_app, []}},
-    {env, [
-        {max_objects, unlimited},
-        {max_size, 104857600}, % 100M
-        {max_lifetime, 60000} % 1m
-    ]}
 ]}.
diff --git a/src/ddoc_cache/src/ddoc_cache.erl b/src/ddoc_cache/src/ddoc_cache.erl
index ed93309..07d89ac 100644
--- a/src/ddoc_cache/src/ddoc_cache.erl
+++ b/src/ddoc_cache/src/ddoc_cache.erl
@@ -91,7 +91,7 @@ open_custom(DbName, Mod) ->
 
 evict(ShardDbName, DDocIds) ->
     DbName = mem3:dbname(ShardDbName),
-    ddoc_cache_opener:evict_docs(DbName, DDocIds).
+    ddoc_cache_lru:evict(DbName, DDocIds).
 
 open(DbName, validation_funs) ->
     open_validation_funs(DbName);
diff --git a/src/ddoc_cache/src/ddoc_cache.hrl b/src/ddoc_cache/src/ddoc_cache.hrl
new file mode 100644
index 0000000..8545914
--- /dev/null
+++ b/src/ddoc_cache/src/ddoc_cache.hrl
@@ -0,0 +1,32 @@
+% 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.
+
+-type dbname() :: iodata().
+-type docid() :: iodata().
+-type doc_hash() :: <<_:128>>.
+-type revision() :: {pos_integer(), doc_hash()}.
+
+-define(CACHE, ddoc_cache_entries).
+-define(ATIMES, ddoc_cache_atimes).
+-define(OPENERS, ddoc_cache_openers).
+
+
+-record(entry, {
+    key,
+    val
+}).
+
+-record(opener, {
+    key,
+    pid,
+    clients
+}).
diff --git a/src/ddoc_cache/src/ddoc_cache_lru.erl b/src/ddoc_cache/src/ddoc_cache_lru.erl
new file mode 100644
index 0000000..9d8c397
--- /dev/null
+++ b/src/ddoc_cache/src/ddoc_cache_lru.erl
@@ -0,0 +1,221 @@
+% 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(ddoc_cache_lru).
+-behaviour(gen_server).
+-vsn(1).
+
+
+-export([
+    start_link/0,
+
+    insert/2,
+    accessed/1,
+    evict/1,
+    evict/2
+]).
+
+-export([
+    init/1,
+    terminate/2,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3
+]).
+
+-export([
+    handle_db_event/3
+]).
+
+
+-include("ddoc_cache.hrl").
+
+
+-record(st, {
+    keys,
+    time,
+    max_size,
+    evictor
+}).
+
+
+start_link() ->
+    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+
+insert(Key, Val) ->
+    gen_server:call(?MODULE, {insert, Key}).
+
+
+accessed(Key) ->
+    gen_server:cast(?MODULE, {accessed, Key}).
+
+
+-spec evict(dbname(), [docid()]) -> ok.
+evict(DbName, DDocIds) ->
+    gen_server:cast(?MODULE, {evict, DbName, DDocIds}).
+
+
+init(_) ->
+    {ok, Keys} = khash:new(),
+    {ok, Evictor} = couch_event:link_listener(
+            ?MODULE, handle_db_event, nil, [all_dbs]
+        ),
+    MaxSize = config:get_integer("ddoc_cache", "max_size", 1000),
+    {ok, #st{
+        keys = Keys,
+        time = 0,
+        max_size = MaxSize,
+        evictor = Evictor
+    }}.
+
+
+terminate(_Reason, St) ->
+    case is_pid(St#st.evictor) of
+        true -> exit(St#st.evictor, kill);
+        false -> ok
+    end,
+    ok.
+
+
+handle_call({insert, Key, Val}, _From, St) ->
+    #st{
+        keys = Keys,
+        time = Time
+    } = St,
+    NewTime = Time + 1,
+    true = ets:insert(?CACHE, #entry{key = Key, val = Val}),
+    true = ets:insert(?ATIMES, {NewTime, Key}),
+    ok = khash:put(Keys, NewTime),
+    {reply, ok, trim(St#st{time = NewTime})};
+
+handle_call(Msg, _From, St) ->
+    {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.
+
+
+handle_cast({accessed, Key}, _St) ->
+    #st{
+        keys = Keys,
+        time = Time
+    } = St,
+    NewTime = Time + 1,
+    case khash:lookup(Keys, Key) of
+        {value, OldTime} ->
+            true = ets:delete(?ATIMES, OldTime),
+            true = ets:insert(?ATIMES, {NewTime, Key}),
+            ok = khash:put(Keys, NewTime);
+        not_found ->
+            % Likely a client read from the cache while an
+            % eviction message was in our mailbox
+            ok
+    end,
+    {noreply, St};
+
+handle_cast({evict, _} = Msg, St) ->
+    gen_server:abcast(mem3:nodes(), ?MODULE, Msg),
+    {noreply, St};
+
+handle_cast({evict, _, _} = Msg, St) ->
+    gen_server:abcast(mem3:nodes(), ?MODULE, Msg),
+    {noreply, St};
+
+handle_cast({do_evict, DbName} = Msg, St) ->
+    Pattern = #entry{
+        key = {DbName, '$1', '_'},
+        val = '_',
+        _ = '_'
+    },
+    DDocIds = lists:flatten(ets:match(?CACHE, Pattern)),
+    handle_cast({do_evict, DbName, DDocIds});
+
+handle_cast({do_evict, DbName, DDocIds}, St) ->
+    Pattern = #entry{
+        key = {DbName, '$1'},
+        val = '_',
+        _ = '_'
+    },
+    CustomKeys = lists:flatten(ets:match(?CACHE, Pattern)),
+    lists:foreach(fun(Mod) ->
+        ets:delete(?CACHE, {DbName, Mod})
+    end, CustomKeys),
+    lists:foreach(fun(DDocId) ->
+        RevPattern = #entry{
+            key = {DbName, DDocId, '$1'},
+            val = '_',
+            _ = '_'
+        },
+        Revs = lists:flatten(ets:match(?CACHE, RevPattern)),
+        lists:foreach(fun(Rev) ->
+            ets:delete(?CACHE, {DbName, DDocId, Rev})
+        end, Revs)
+    end, DDocIds),
+    {noreply, St};
+
+handle_cast(Msg, St) ->
+    {stop, {invalid_cast, Msg}, St}.
+
+
+handle_info({'EXIT', Pid, Reason}, #st{evictor=Pid}=St) ->
+    couch_log:error("ddoc_cache_opener evictor died ~w", [Reason]),
+    {ok, Evictor} = couch_event:link_listener(
+            ?MODULE, handle_db_event, nil, [all_dbs]
+        ),
+    {noreply, St#st{evictor=Evictor}};
+
+handle_info(Msg, St) ->
+    {stop, {invalid_info, Msg}, St}.
+
+
+code_change(_OldVsn, St, _Extra) ->
+    {ok, St}.
+
+
+handle_db_event(ShardDbName, created, St) ->
+    gen_server:cast(?MODULE, {evict, mem3:dbname(ShardDbName)}),
+    {ok, St};
+
+handle_db_event(ShardDbName, deleted, St) ->
+    gen_server:cast(?MODULE, {evict, mem3:dbname(ShardDbName)}),
+    {ok, St};
+
+handle_db_event(_DbName, _Event, St) ->
+    {ok, St}.
+
+
+trim(St) ->
+    #st{
+        keys = Keys,
+        max_size = MaxSize
+    } = St,
+    case khash:size(Keys) > MaxSize of
+        true ->
+            case ets:first(?ATIMES) of
+                '$end_of_table' ->
+                    St;
+                ATime ->
+                    trim(remove(St, ATime))
+            end;
+        false ->
+            St
+    end.
+
+
+remove(St, ATime) ->
+    #st{
+        keys = Keys
+    } = St,
+    {value, Key} = khash:lookup(Keys, ATime),
+    true = ets:delete(?CACHE, Key),
+    true = ets:delete(?ATIMES, ATime),
+    ok = khash:del(Keys, Key),
+    St.
diff --git a/src/ddoc_cache/src/ddoc_cache_opener.erl b/src/ddoc_cache/src/ddoc_cache_opener.erl
index b76a228..a4adffc 100644
--- a/src/ddoc_cache/src/ddoc_cache_opener.erl
+++ b/src/ddoc_cache/src/ddoc_cache_opener.erl
@@ -35,7 +35,6 @@
     open_doc/2,
     open_doc/3,
     open_validation_funs/1,
-    evict_docs/2,
     lookup/1,
     match_newest/1,
     recover_doc/2,
@@ -43,29 +42,17 @@
     recover_validation_funs/1
 ]).
 -export([
-    handle_db_event/3
-]).
--export([
     fetch_doc_data/1
 ]).
 
--define(CACHE, ddoc_cache_lru).
--define(OPENING, ddoc_cache_opening).
+-include("ddoc_cache.hrl").
 
--type dbname() :: iodata().
--type docid() :: iodata().
--type doc_hash() :: <<_:128>>.
--type revision() :: {pos_integer(), doc_hash()}.
 
--record(opener, {
-    key,
-    pid,
-    clients
-}).
+-define(LRU, ddoc_cache_lru).
+
 
 -record(st, {
-    db_ddocs,
-    evictor
+    db_ddocs
 }).
 
 start_link() ->
@@ -86,14 +73,12 @@ open_validation_funs(DbName) ->
     Resp = gen_server:call(?MODULE, {open, {DbName, validation_funs}}, infinity),
     handle_open_response(Resp).
 
--spec evict_docs(dbname(), [docid()]) -> ok.
-evict_docs(DbName, DocIds) ->
-    gen_server:cast(?MODULE, {evict, DbName, DocIds}).
 
 lookup(Key) ->
-    try ets_lru:lookup_d(?CACHE, Key) of
-        {ok, _} = Resp ->
-            Resp;
+    try ets:lookup(?CACHE, Key) of
+        [#entry{key = Key, val = Val}] ->
+            ddoc_cache_lru:accessed(Key),
+            {ok, Val};
         _ ->
             missing
     catch
@@ -102,10 +87,19 @@ lookup(Key) ->
     end.
 
 match_newest(Key) ->
-    try ets_lru:match_object(?CACHE, Key, '_') of
+    Pattern = #entry{
+        key = Key,
+        val = '_',
+        _ = '_'
+    },
+    try ets:match_object(?CACHE, Pattern) of
         [] ->
             missing;
-        Docs ->
+        Entries ->
+            Docs = lists:map(fun(#entry{key = K, val = V}) ->
+                ddoc_cache_lru:accessed(K),
+                V
+            end, Entries),
             Sorted = lists:sort(
                 fun (#doc{deleted=DelL, revs=L}, #doc{deleted=DelR, revs=R}) ->
                     {not DelL, L} > {not DelR, R}
@@ -133,40 +127,22 @@ recover_validation_funs(DbName) ->
     end, DDocs),
     {ok, Funs}.
 
-handle_db_event(ShardDbName, created, St) ->
-    gen_server:cast(?MODULE, {evict, mem3:dbname(ShardDbName)}),
-    {ok, St};
-handle_db_event(ShardDbName, deleted, St) ->
-    gen_server:cast(?MODULE, {evict, mem3:dbname(ShardDbName)}),
-    {ok, St};
-handle_db_event(_DbName, _Event, St) ->
-    {ok, St}.
 
 init(_) ->
     process_flag(trap_exit, true),
-    _ = ets:new(?OPENING, [set, protected, named_table, {keypos, #opener.key}]),
-    {ok, Evictor} = couch_event:link_listener(
-            ?MODULE, handle_db_event, nil, [all_dbs]
-        ),
-    {ok, #st{
-        evictor = Evictor
-    }}.
-
-terminate(_Reason, St) ->
-    case is_pid(St#st.evictor) of
-        true -> exit(St#st.evictor, kill);
-        false -> ok
-    end,
+    {ok, #st{}}.
+
+terminate(_Reason, _St) ->
     ok.
 
 handle_call({open, OpenerKey}, From, St) ->
-    case ets:lookup(?OPENING, OpenerKey) of
+    case ets:lookup(?OPENERS, OpenerKey) of
         [#opener{clients=Clients}=O] ->
-            ets:insert(?OPENING, O#opener{clients=[From | Clients]}),
+            ets:insert(?OPENERS, O#opener{clients=[From | Clients]}),
             {noreply, St};
         [] ->
             Pid = spawn_link(?MODULE, fetch_doc_data, [OpenerKey]),
-            ets:insert(?OPENING, #opener{key=OpenerKey, pid=Pid, clients=[From]}),
+            ets:insert(?OPENERS, #opener{key=OpenerKey, pid=Pid, clients=[From]}),
             {noreply, St}
     end;
 
@@ -174,38 +150,19 @@ handle_call(Msg, _From, St) ->
     {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.
 
 
-handle_cast({evict, DbName}, St) ->
-    gen_server:abcast(mem3:nodes(), ?MODULE, {do_evict, DbName}),
-    {noreply, St};
-
-handle_cast({evict, DbName, DDocIds}, St) ->
-    gen_server:abcast(mem3:nodes(), ?MODULE, {do_evict, DbName, DDocIds}),
+% The do_evict clauses are upgrades while we're
+% in a rolling reboot.
+handle_cast({do_evict, _} = Msg, St) ->
+    gen_server:cast(?LRU, Msg),
     {noreply, St};
 
-handle_cast({do_evict, DbName}, St) ->
-    DDocIds = lists:flatten(ets_lru:match(?CACHE, {DbName, '$1', '_'}, '_')),
-    handle_cast({do_evict, DbName, DDocIds}, St);
-
-handle_cast({do_evict, DbName, DDocIds}, St) ->
-    CustomKeys = lists:flatten(ets_lru:match(?CACHE, {DbName, '$1'}, '_')),
-    lists:foreach(fun(Mod) ->
-        ets_lru:remove(?CACHE, {DbName, Mod})
-    end, CustomKeys),
-    lists:foreach(fun(DDocId) ->
-        Revs = ets_lru:match(?CACHE, {DbName, DDocId, '$1'}, '_'),
-        lists:foreach(fun([Rev]) ->
-            ets_lru:remove(?CACHE, {DbName, DDocId, Rev})
-        end, Revs)
-    end, DDocIds),
+handle_cast({do_evict, _, _}, St) ->
+    gen_server:cast(?LRU, Msg)
     {noreply, St};
 
 handle_cast(Msg, St) ->
     {stop, {invalid_cast, Msg}, St}.
 
-handle_info({'EXIT', Pid, Reason}, #st{evictor=Pid}=St) ->
-    couch_log:error("ddoc_cache_opener evictor died ~w", [Reason]),
-    {ok, Evictor} = couch_event:link_listener(?MODULE, handle_db_event, nil, [all_dbs]),
-    {noreply, St#st{evictor=Evictor}};
 
 handle_info({'EXIT', _Pid, {open_ok, OpenerKey, Resp}}, St) ->
     respond(OpenerKey, {open_ok, Resp}),
@@ -217,10 +174,10 @@ handle_info({'EXIT', _Pid, {open_error, OpenerKey, Type, Error}}, St) ->
 
 handle_info({'EXIT', Pid, Reason}, St) ->
     Pattern = #opener{pid=Pid, _='_'},
-    case ets:match_object(?OPENING, Pattern) of
+    case ets:match_object(?OPENERS, Pattern) of
         [#opener{key=OpenerKey, clients=Clients}] ->
-            _ = [gen_server:reply(C, {error, Reason}) || C <- Clients],
-            ets:delete(?OPENING, OpenerKey),
+            [gen_server:reply(C, {error, Reason}) || C <- Clients],
+            ets:delete(?OPENERS, OpenerKey),
             {noreply, St};
         [] ->
             {stop, {unknown_pid_died, {Pid, Reason}}, St}
@@ -238,14 +195,14 @@ code_change(_OldVsn, State, _Extra) ->
                     ({dbname(), docid(), revision()}) -> no_return().
 fetch_doc_data({DbName, validation_funs}=OpenerKey) ->
     {ok, Funs} = recover_validation_funs(DbName),
-    ok = ets_lru:insert(?CACHE, OpenerKey, Funs),
+    ok = ddoc_cache_lru:insert(OpenerKey, Funs),
     exit({open_ok, OpenerKey, {ok, Funs}});
 fetch_doc_data({DbName, Mod}=OpenerKey) when is_atom(Mod) ->
     % This is not actually a docid but rather a custom cache key.
     % Treat the argument as a code module and invoke its recover function.
     try Mod:recover(DbName) of
         {ok, Result} ->
-            ok = ets_lru:insert(?CACHE, OpenerKey, Result),
+            ok = ddoc_cache_lru:insert(OpenerKey, Result),
             exit({open_ok, OpenerKey, {ok, Result}});
         Else ->
             exit({open_ok, OpenerKey, Else})
@@ -258,7 +215,7 @@ fetch_doc_data({DbName, DocId}=OpenerKey) ->
         {ok, Doc} ->
             {RevDepth, [RevHash| _]} = Doc#doc.revs,
             Rev = {RevDepth, RevHash},
-            ok = ets_lru:insert(?CACHE, {DbName, DocId, Rev}, Doc),
+            ok = ddoc_cache_lru:insert({DbName, DocId, Rev}, Doc),
             exit({open_ok, OpenerKey, {ok, Doc}});
         Else ->
             exit({open_ok, OpenerKey, Else})
@@ -269,7 +226,7 @@ fetch_doc_data({DbName, DocId}=OpenerKey) ->
 fetch_doc_data({DbName, DocId, Rev}=OpenerKey) ->
     try recover_doc(DbName, DocId, Rev) of
         {ok, Doc} ->
-            ok = ets_lru:insert(?CACHE, {DbName, DocId, Rev}, Doc),
+            ok = ddoc_cache_lru:insert({DbName, DocId, Rev}, Doc),
             exit({open_ok, OpenerKey, {ok, Doc}});
         Else ->
             exit({open_ok, OpenerKey, Else})
@@ -287,6 +244,6 @@ handle_open_response(Resp) ->
     end.
 
 respond(OpenerKey, Resp) ->
-    [#opener{clients=Clients}] = ets:lookup(?OPENING, OpenerKey),
+    [#opener{clients=Clients}] = ets:lookup(?OPENERS, OpenerKey),
     _ = [gen_server:reply(C, Resp) || C <- Clients],
-    ets:delete(?OPENING, OpenerKey).
+    ets:delete(?OPENERS, OpenerKey).
diff --git a/src/ddoc_cache/src/ddoc_cache_sup.erl b/src/ddoc_cache/src/ddoc_cache_sup.erl
index 85e90b3..ddb1232 100644
--- a/src/ddoc_cache/src/ddoc_cache_sup.erl
+++ b/src/ddoc_cache/src/ddoc_cache_sup.erl
@@ -27,12 +27,20 @@ start_link() ->
 init([]) ->
     Children = [
         {
+            ddoc_cache_tables,
+            {ddoc_cache_tables, start_link, []},
+            permanent,
+            5000,
+            worker,
+            [ddoc_cache_tables]
+        },
+        {
             ddoc_cache_lru,
-            {ets_lru, start_link, [ddoc_cache_lru, lru_opts()]},
+            {ddoc_cache_lru, start_link, []},
             permanent,
             5000,
             worker,
-            [ets_lru]
+            [ddoc_cache_lru]
         },
         {
             ddoc_cache_opener,
@@ -43,25 +51,4 @@ init([]) ->
             [ddoc_cache_opener]
         }
     ],
-    {ok, {{one_for_one, 5, 10}, Children}}.
-
-
-lru_opts() ->
-    case application:get_env(ddoc_cache, max_objects) of
-        {ok, MxObjs} when is_integer(MxObjs), MxObjs >= 0 ->
-            [{max_objects, MxObjs}];
-        _ ->
-            []
-    end ++
-    case application:get_env(ddoc_cache, max_size) of
-        {ok, MxSize} when is_integer(MxSize), MxSize >= 0 ->
-            [{max_size, MxSize}];
-        _ ->
-            []
-    end ++
-    case application:get_env(ddoc_cache, max_lifetime) of
-        {ok, MxLT} when is_integer(MxLT), MxLT >= 0 ->
-            [{max_lifetime, MxLT}];
-        _ ->
-            []
-    end.
+    {ok, {{one_for_all, 5, 10}, Children}}.
diff --git a/src/ddoc_cache/src/ddoc_cache_tables.erl b/src/ddoc_cache/src/ddoc_cache_tables.erl
new file mode 100644
index 0000000..9b35943
--- /dev/null
+++ b/src/ddoc_cache/src/ddoc_cache_tables.erl
@@ -0,0 +1,64 @@
+% 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(ddoc_cache_tables).
+-behaviour(gen_server).
+-vsn(1).
+
+
+-export([
+    start_link/0
+]).
+
+-export([
+    init/1,
+    terminate/2,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3
+]).
+
+
+-include("ddoc_cache.hrl").
+
+
+start_link() ->
+    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+
+init(_) ->
+    BaseOpts = [public, named_table],
+    ets:new(?CACHE, [set, {read_concurrency, true}] ++ BaseOpts),
+    ets:new(?ATIMES, [sorted_set] ++ BaseOpts),
+    ets:new(?OPENING, [set, {keypos, #opener.key}] ++ BaseOpts),
+    {ok, nil}.
+
+
+terminate(_Reason, _St) ->
+    ok.
+
+
+handle_call(Msg, _From, St) ->
+    {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.
+
+
+handle_cast(Msg, St) ->
+    {stop, {invalid_cast, Msg}, St}.
+
+
+handle_info(Msg, St) ->
+    {stop, {invalid_info, Msg}, St}.
+
+
+code_change(_OldVsn, St, _Extra) ->
+    {ok, St}.

-- 
To stop receiving notification emails like this one, please contact
"commits@couchdb.apache.org" <co...@couchdb.apache.org>.