Packages

Simple memory cache with expiration.

Current section

Files

Jump to
simple_cache src simple_cache_server.erl
Raw

src/simple_cache_server.erl

-module(simple_cache_server).
-behaviour(gen_server).
-define(SERVER, ?MODULE).
-define(ETS_OPTIONS, [{read_concurrency, true},
{write_concurrency, true},
named_table,
protected
]).
-record(state, {table :: ets:tid() | atom(),
options :: list()}).
%%=============================================================================
%% API Function Exports
%%=============================================================================
-export([start_link/0,
start_link/1,
ops_info/0,
ops_list/0,
sync_set/2,
set/2,
sync_set/3,
set/3,
cond_set/4,
lookup/1,
lookup/2,
flush/1,
sync_flush/1,
flush/0,
sync_flush/0
]).
%%=============================================================================
%% gen_server Function Exports
%%=============================================================================
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
%%=============================================================================
%% API Function Definitions
%%=============================================================================
-spec start_link() -> 'ignore' | {'error', term()} | {'ok', pid()}.
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
-spec start_link(list({atom(), any()}))
-> 'ignore' | {'error', term()} | {'ok', pid()}.
start_link(CustomOptions) ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [CustomOptions], []).
-spec ops_info() -> list().
ops_info() ->
gen_server:call(?MODULE, ops_info).
-spec ops_list() -> list().
ops_list() ->
gen_server:call(?MODULE, ops_list).
-spec set(any(), any()) -> 'ok'.
set(Key, Value) ->
gen_server:cast(?SERVER, {set, Key, Value, infinity}).
-spec sync_set(any(), any()) -> any().
sync_set(Key, Value) ->
gen_server:call(?SERVER, {set, Key, Value, infinity}).
-spec set(any(), any(), simple_cache:expire()) -> 'ok'.
set(Key, Value, Expires) ->
gen_server:cast(?SERVER, {set, Key, Value, Expires}).
-spec sync_set(any(), any(), simple_cache:expire()) -> any().
sync_set(Key, Value, Expires) ->
gen_server:call(?SERVER, {set, Key, Value, Expires}).
-spec cond_set(any(), any(), simple_cache:conditional(),
simple_cache:expire()) -> {ok, boolean()}.
cond_set(Key, Value, Conditional, Expires) ->
gen_server:call(?SERVER, {set, Key, Value, Conditional, Expires}).
-spec lookup(any()) -> {'error','not_found'} | {'ok', any()}.
lookup(Key) -> get_by_key(?SERVER, Key).
-spec lookup(any(), any()) -> {'ok',_}.
lookup(Key, Default) ->
case lookup(Key) of
{ok, Value} ->
{ok, Value};
{error, not_found} ->
{ok, Default}
end.
-spec flush(any()) -> 'ok'.
flush(Key) ->
gen_server:cast(?SERVER, {flush, Key}),
ok.
-spec sync_flush(any()) -> 'ok'.
sync_flush(Key) ->
gen_server:call(?SERVER, {flush, Key}),
ok.
-spec flush() -> 'ok'.
flush() ->
gen_server:cast(?SERVER, flush),
ok.
-spec sync_flush() -> 'ok'.
sync_flush() ->
gen_server:call(?SERVER, flush).
%%=============================================================================
%% gen_server Function Definitions
%%=============================================================================
init([]) ->
{ok, #state{table = ets:new(?SERVER, ?ETS_OPTIONS),
options = ?ETS_OPTIONS}};
init(CustomOptions) ->
Options = merge_options(?ETS_OPTIONS, CustomOptions),
{ok, #state{table = ets:new(?SERVER, Options),
options = Options}}.
handle_call(ops_info, _From, #state{table = Table} = State) ->
{reply, ets:info(Table), State};
handle_call(ops_list, _From, #state{table = Table} = State) ->
{reply, ets:tab2list(Table), State};
handle_call({set, Key, Value, infinity}, _From, #state{table = Table} = State) ->
insert(Table, Key, Value, infinity),
{reply, ok, State};
handle_call({set, Key, Value, Expires}, _From, #state{table = Table} = State) ->
insert(Table, Key, Value, Expires),
{reply, ok, State};
handle_call({set, Key, Value, Conditional, Expires}, _From, #state{table = Table} = State) ->
Test = case lookup(Key) of
{ok, OldValue} -> Conditional(OldValue);
{error, not_found} -> true
end,
Test andalso insert(Table, Key, Value, Expires),
{reply, {ok, Test}, State};
handle_call({flush, Key}, _From, #state{table = Table} = State) ->
ets:delete(Table, Key),
{reply, ok, State};
handle_call(flush, _From, #state{table = Table} = State) ->
true = ets:delete_all_objects(Table),
{reply, ok, State}.
handle_cast({set, Key, Value, infinity}, #state{table = Table} = State) ->
insert(Table, Key, Value, infinity),
{noreply, State};
handle_cast({set, Key, Value, Expires}, #state{table = Table} = State) ->
insert(Table, Key, Value, Expires),
{noreply, State};
handle_cast({flush, Key}, #state{table = Table} = State) ->
ets:delete(Table, Key),
{noreply, State};
handle_cast(flush, #state{table = Table} = State) ->
ets:delete_all_objects(Table),
{noreply, State}.
handle_info({expire, Key}, #state{table = Table} = State) ->
ets:delete(Table, Key),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%=============================================================================
%% Internal functionality
%%=============================================================================
insert(Table, Key, Value, infinity) ->
ets:insert(Table, {Key, Value, infinity});
insert(Table, Key, Value, Expires) ->
ets:insert(Table, {Key, Value, Expires}),
erlang:send_after(1000 * Expires, ?SERVER, {expire, Key}).
get_by_key(Table, Key) ->
case ets:lookup(Table, Key) of
[{Key, Value, _Expires}] ->
{ok, Value};
[] ->
{error, not_found}
end.
merge_options(ExistingOptions, NewOptions) ->
orddict:merge(fun (_, X, Y) -> X + Y end,
orddict:from_list(ExistingOptions),
orddict:from_list(NewOptions)).