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>.