Initial import
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.
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
+ ]}
+]}.
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).
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.
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.
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}}.
+
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.