Updated Branches: refs/heads/import [created] bd245ace5
Initial import Project: http://git-wip-us.apache.org/repos/asf/couchdb-ddoc-cache/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-ddoc-cache/commit/9911ea51 Tree: http://git-wip-us.apache.org/repos/asf/couchdb-ddoc-cache/tree/9911ea51 Diff: http://git-wip-us.apache.org/repos/asf/couchdb-ddoc-cache/diff/9911ea51 Branch: refs/heads/import Commit: 9911ea51e31fc43d182014f6fc05b25d2ef8cda2 Parents: Author: Paul J. Davis <[email protected]> Authored: Wed Dec 19 18:18:04 2012 -0600 Committer: Paul J. Davis <[email protected]> Committed: Wed Dec 19 18:18:04 2012 -0600 ---------------------------------------------------------------------- README.md | 4 + src/ddoc_cache.app.src | 22 ++++ src/ddoc_cache.erl | 17 +++ src/ddoc_cache_app.erl | 15 +++ src/ddoc_cache_server.erl | 231 +++++++++++++++++++++++++++++++++++++++++ src/ddoc_cache_sup.erl | 29 ++++++ src/ddoc_cache_util.erl | 22 ++++ 7 files changed, 340 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-ddoc-cache/blob/9911ea51/README.md ---------------------------------------------------------------------- diff --git a/README.md b/README.md new file mode 100644 index 0000000..81d600b --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +Design Doc Cache +================ + +Pretty much covers it. http://git-wip-us.apache.org/repos/asf/couchdb-ddoc-cache/blob/9911ea51/src/ddoc_cache.app.src ---------------------------------------------------------------------- diff --git a/src/ddoc_cache.app.src b/src/ddoc_cache.app.src new file mode 100644 index 0000000..184d0a9 --- /dev/null +++ b/src/ddoc_cache.app.src @@ -0,0 +1,22 @@ +% Copyright 2012 Cloudant. All rights reserved. + +{application, ddoc_cache, [ + {description, "Design Document Cache"}, + {vsn, git}, + {registered, [ + ddoc_cache_server + ]}, + {applications, [ + kernel, + stdlib, + crypto, + mem3, + fabric, + twig + ]}, + {mod, {ddoc_cache_app, []}}, + {env, [ + {cache_size, 104857600}, % 100M + {cache_expiry, 3600} % 1h + ]} +]}. http://git-wip-us.apache.org/repos/asf/couchdb-ddoc-cache/blob/9911ea51/src/ddoc_cache.erl ---------------------------------------------------------------------- diff --git a/src/ddoc_cache.erl b/src/ddoc_cache.erl new file mode 100644 index 0000000..42a4999 --- /dev/null +++ b/src/ddoc_cache.erl @@ -0,0 +1,17 @@ +% Copyright 2012 Cloudant. All rights reserved. + +-module(ddoc_cache). + + +-export([ + start/0, + stop/0 +]). + + +start() -> + application:start(ddoc_cache). + + +stop() -> + application:stop(ddoc_cache). http://git-wip-us.apache.org/repos/asf/couchdb-ddoc-cache/blob/9911ea51/src/ddoc_cache_app.erl ---------------------------------------------------------------------- diff --git a/src/ddoc_cache_app.erl b/src/ddoc_cache_app.erl new file mode 100644 index 0000000..922aab6 --- /dev/null +++ b/src/ddoc_cache_app.erl @@ -0,0 +1,15 @@ +% Copyright 2012 Cloudant. All rights reserved. + +-module(ddoc_cache_app). +-behaviour(application). + + +-export([start/2, stop/1]). + + +start(_StartType, _StartArgs) -> + ddoc_cache_sup:start_link(). + + +stop(_State) -> + ok. http://git-wip-us.apache.org/repos/asf/couchdb-ddoc-cache/blob/9911ea51/src/ddoc_cache_server.erl ---------------------------------------------------------------------- diff --git a/src/ddoc_cache_server.erl b/src/ddoc_cache_server.erl new file mode 100644 index 0000000..ba176fd --- /dev/null +++ b/src/ddoc_cache_server.erl @@ -0,0 +1,231 @@ +% Copyright 2012 Cloudant. All rights reserved. + +-module(ddoc_cache_server). +-behaviour(gen_server). + + +-export([ + start_link/0, + open/2, + evict/2 +]). + +-export([ + open_ddoc/1 +]). + +-export([ + init/1, + terminate/2, + handle_call/3, + handle_cast/2, + handle_info/2, + code_change/3 +]). + + +-define(CACHE, ddoc_cache_docs). +-define(ATIMES, ddoc_cache_atimes). +-define(OPENING, ddoc_cache_opening). + + +-record(ddoc, { + key, + dbname, + atime, + doc, + lease +}). + +-record(opener, { + key, + pid, + clients +}). + +-record(st, { + uuid, + max_size, + expiry +}). + + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + + +open(DbName, <<"_design/", _/binary>>=DDocId) -> + case ets:lookup(?CACHE, {DbName, DDocId}) of + [#ddoc{doc=Doc}] -> + gen_server:cast(?MODULE, {cache_hit, {DbName, DDocId}}), + {ok, Doc}; + _ -> + gen_server:call(?MODULE, {open, {DbName, DDocId}}, infinity) + end; +open(DbName, DDocId) -> + open(DbName, <<"_design/", DDocId/binary>>). + + +evict(DbName, DDocId) -> + gen_server:abcast(?MODULE, {evict, {DbName, DDocId}}). + + +init(_) -> + process_flag(trap_exit, true), + ets:new(?CACHE, [protected, named_table, set, {keypos, #ddoc.key}]), + ets:new(?ATIMES, [protected, named_table, sorted_set]), + ets:new(?OPENING, [protected, named_table, set, {keypos, #opener.key}]), + {ok, #st{ + uuid = ddoc_cache_util:new_uuid(), + max_size = get_cache_size(), + expiry = get_cache_expiry(), + in_progress = [] + }}. + + +terminate(_Reason, _State) -> + ok. + + +handle_call({cache_hit, Key}, _From, St) -> + cache_hit(Key), + {ok, St}; + +handle_call({open, Key}, From, #st{in_progress=IP}=St) -> + case ets:lookup(?OPENING, Key) of + [#opener{clients=Clients}=O] -> + ets:insert(?OPENING, O#opening{clients=[From | Clients]}), + {noreply, St}; + [] -> + Pid = spawn_link(?MODULE, open_ddoc, [Key]), + ets:insert(?OPENING, #opener{key=Key, pid=Pid, clients=[From]}) + {noreply, St} + end; + +handle_call(Msg, _From, St) -> + {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}. + + +handle_cast({evict, DbName, DDocId}, St) -> + cache_remove({DbName, DDocId}), + ets:insert(?LOG, [{erlang:now(), {DbName, DDocId}}]), + {noreply, St}; + +handle_cast(Msg, St) -> + {stop, {invalid_cast, Msg}, St}. + + +handle_info({'EXIT', _Pid, {ddoc_ok, {DbName, _}=Key, Doc}}, St) -> + cache_insert(#ddoc{key=Key, dbname=DbName, doc=Doc}, St), + respond(Key, {ok, Doc}), + {noreply, St}; + +handle_info({'EXIT', _Pid, {ddoc_error, Key, Error}}, St) -> + respond(Key, Error), + {noreply, St}; + +handle_info({'EXIT', Pid, Reason}, St) -> + Pattern = #opener{pid=Pid, _='_'}, + case ets:match_object(?OPENING, Pattern) of + [#opener{key=Key, clients=Clients}] -> + respond(Key, Reason), + {noreply, St}; + [] -> + {stop, {unknown_pid_died, {Pid, Reason}}, St} + end; + +handle_info(Msg, St) -> + {stop, {invalid_info, Msg}, St}. + + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + +open_ddoc({DbName, DDocId}=Key) -> + Resp = fabric:open_doc(DbName, DDocId) of + {ok, Doc} -> + exit({ddoc_ok, Key, Doc}); + Else -> + exit({ddoc_error, Key, Error}) + end. + + +respond(Key, Resp) -> + [#opener{clients=Clients}] = ets:lookup(?OPENING, Key), + [gen_server:reply(C, Resp) || C <- Clients], + ets:delete(?OPENING, Key). + + +cache_hit(Key) -> + % Using a different pattern than the usual ets:lookup/2 + % method so that we can avoid needlessly copying the large + % #doc{} record in and out of ets. + case ets:match(?CACHE, #ddoc{key=Key, atime='$1', _='_'}) of + [[ATime]] -> + NewATime = erlang:now(), + ets:delete(?ATIMES, ATime), + ets:insert(?ATIMES, {NewATime, Key}), + ets:update_element(?CACHE, Key, {#ddoc.atime, NewATime}); + [] -> + ok + end. + + +cache_insert(#ddoc{key=Key}=DDoc, St) -> + % Same logic as cache_hit to avoid ets:lookup/2 + case ets:match(?CACHE, #ddoc{key=Key, atime='$1', _='_'}) of + [[ATime]] -> + ets:delete(?ATIMES, ATime); + [] -> + ok + end, + NewATime = erlang:now(), + ets:insert(?CACHE, DDoc#ddoc{atime=ATime}), + ets:insert(?ATIMES, {ATime, DDoc#ddoc.key}), + cache_free_space(St). + + +cache_free_space(St) -> + case ets:info(?CACHE, memory) > St#st.cache_size of + true -> + case ets:first(?ATIMES) of + {ATime, Key} -> + ets:delete(?ATIMES, ATime), + ets:delete(?CACHE, Key), + cache_free_space(St) + '$end_of_table' -> + ok + end; + false -> + ok + end. + + +cache_remove(Key) -> + % Same logic as cache_hit/1 to avoid ets:lookup/2 + case ets:match(?CACHE, #ddoc{key=Key, atime=ATime, _='_'}) of + [[ATme]] -> + ets:delete(?CACHE, Key), + ets:delete(?ATIMES, ATime); + [] -> + ok + end. + + +get_cache_size() -> + case application:get_env(ddoc_cache, cache_size) of + {ok, Value} when is_integer(Value), Value > 0 -> + Value; + _ -> + 104857600 % Default 100M + end. + + +get_cache_expiry() -> + case application:get_env(ddoc_cache, cache_expiry) of + {ok, Value} when is_integer(Value), Value > 0 -> + Value; + _ -> + 3600 % Default 1h + end. http://git-wip-us.apache.org/repos/asf/couchdb-ddoc-cache/blob/9911ea51/src/ddoc_cache_sup.erl ---------------------------------------------------------------------- diff --git a/src/ddoc_cache_sup.erl b/src/ddoc_cache_sup.erl new file mode 100644 index 0000000..2d8cd8c --- /dev/null +++ b/src/ddoc_cache_sup.erl @@ -0,0 +1,29 @@ +% Copyright 2012 Cloudant. All rights reserved. + +-module(ddoc_cache_sup). +-behaviour(supervisor). + + +-export([ + start_link/0, + init/1 +]). + + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + + +init([]) -> + Children = [ + { + ddoc_cache_server, + {ddoc_cache_server, start_link, []}, + permanent, + 5000, + worker, + [ddoc_cache_server] + } + ], + {ok, {{one_for_one, 5, 10}, Children}}. + http://git-wip-us.apache.org/repos/asf/couchdb-ddoc-cache/blob/9911ea51/src/ddoc_cache_util.erl ---------------------------------------------------------------------- diff --git a/src/ddoc_cache_util.erl b/src/ddoc_cache_util.erl new file mode 100644 index 0000000..a725674 --- /dev/null +++ b/src/ddoc_cache_util.erl @@ -0,0 +1,22 @@ +-module(ddoc_cache_util). + + +-export([ + new_uuid/0 +]). + + +new_uuid() -> + to_hex(crypto:rand_bytes(16), []). + + +to_hex(<<>>, Acc) -> + list_to_binary(lists:reverse(Acc)); +to_hex(<<C1:4, C2:4, Rest/binary>>, Acc) -> + to_hex(Rest, [hexdig(C1), hexdig(C2) | Acc]). + + +hexdig(C) when C >= 0, C =< 9 -> + C + $0; +hexdig(C) when C >= 10, C =< 15 -> + C + $A - 10.
