Current section
Files
Jump to
Current section
Files
src/g_cache.erl
%%
%% Copyright 2013-16 Joaquim Rocha
%%
%% 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(g_cache).
-behaviour(gen_server).
-include("gibreel.hrl").
-include("gibreel_db.hrl").
-include("g_storage.hrl").
-type options() :: [{atom(), any()}, ...].
-export_type([options/0]).
-define(NO_TASK, none).
-define(NO_COLUMBO, none).
-define(CLUSTER_TIMEOUT, 1000).
-define(SYNC_TIMEOUT, 5000).
-define(NO_VERSION, no_vs).
-define(NO_TOUCH, no_touch).
-define(USE_DEFAULT_EXPIRE, default_expire).
-define(NO_RECORD, no_record).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
%% ====================================================================
%% API functions
%% ====================================================================
-export([start_link/1]).
-export([get/2, get/3]).
-export([store/3, store/4]).
-export([remove/2, remove/3]).
-export([touch/2, touch/3]).
-export([size/1, get_all_keys/1, foldl/3, flush/1]).
-export([reload/1]).
start_link(CacheName) ->
gen_server:start_link(?MODULE, [CacheName], []).
-spec get(CacheName :: atom(), Key :: term()) -> {ok, Value :: term(), Version :: integer()} | not_found | no_cache | error.
get(CacheName, Key) ->
get(CacheName, Key, []).
-spec get(CacheName :: atom(), Key :: term(), Options :: options()) -> {ok, Value :: term(), Version :: integer()} | not_found | no_cache | error.
get(CacheName, Key, Options) ->
case gibreel_db:find(CacheName) of
{error, no_cache} -> no_cache;
{ok, Record} ->
Delay = get_option_value(?OPTION_DELAY, Options, ?NO_TOUCH),
run_get(Key, Delay, Record)
end.
-spec store(CacheName :: atom(), Key :: term(), Value :: term()) -> no_cache | {ok, Version :: integer()}.
store(CacheName, Key, Value) ->
store(CacheName, Key, Value, []).
-spec store(CacheName :: atom(), Key :: term(), Value :: term(), Options :: options()) -> no_cache | invalid_version | {ok, Version :: integer()}.
store(CacheName, Key, Value, Options) ->
case gibreel_db:find(CacheName) of
{error, no_cache} -> no_cache;
{ok, Record} ->
OldVersion = get_option_value(?OPTION_VERSION, Options, ?NO_VERSION),
Delay = get_option_value(?OPTION_DELAY, Options, ?USE_DEFAULT_EXPIRE),
run_store(Key, Value, OldVersion, Delay, Record)
end.
-spec remove(CacheName :: atom(), Key :: term()) -> no_cache | ok.
remove(CacheName, Key) ->
remove(CacheName, Key, []).
-spec remove(CacheName :: atom(), Key :: term(), Options :: options()) -> no_cache | invalid_version | ok.
remove(CacheName, Key, Options) ->
case gibreel_db:find(CacheName) of
{error, no_cache} -> no_cache;
{ok, Record} ->
Version = get_option_value(?OPTION_VERSION, Options, ?NO_VERSION),
run_remove(Key, Version, Record)
end.
-spec touch(CacheName :: atom(), Key :: term()) -> no_cache | ok.
touch(CacheName, Key) ->
touch(CacheName, Key, []).
-spec touch(CacheName :: atom(), Key :: term(), Options :: options()) -> no_cache | ok.
touch(CacheName, Key, Options) ->
case gibreel_db:find(CacheName) of
{error, no_cache} -> no_cache;
{ok, Record} ->
Delay = get_option_value(?OPTION_DELAY, Options, ?USE_DEFAULT_EXPIRE),
run_touch(Key, Delay, Record),
ok
end.
-spec size(CacheName :: atom()) -> no_cache | integer().
size(CacheName) ->
case gibreel_db:find(CacheName) of
{error, no_cache} -> no_cache;
{ok, Record} -> run_size(Record)
end.
-spec get_all_keys(CacheName :: atom()) -> no_cache | list().
get_all_keys(CacheName) ->
case gibreel_db:find(CacheName) of
{error, no_cache} -> no_cache;
{ok, Record} -> run_get_keys(Record)
end.
-spec flush(CacheName :: atom()) -> no_cache | ok.
flush(CacheName) ->
case gibreel_db:find(CacheName) of
{error, no_cache} -> no_cache;
{ok, Record} -> run_flush(Record)
end.
-spec foldl(CacheName :: atom(), Fun, Acc :: term()) -> no_cache | term()
when Fun :: fun((Key :: term(), Value :: term(), Version :: term(), Acc :: term()) -> term()).
foldl(CacheName, Fun, Acc) ->
case gibreel_db:find(CacheName) of
{error, no_cache} -> no_cache;
{ok, Record} -> run_foldl(Fun, Acc, Record)
end.
-spec reload(CacheName :: atom()) -> ok.
reload(CacheName) ->
gen_server:cast(CacheName, {reload_config}).
%% ====================================================================
%% Behavioural functions
%% ====================================================================
-record(state, {record, task}).
%% init
init([CacheName]) ->
case gibreel_db:find(CacheName) of
{error, no_cache} ->
error_logger:error_msg("Cache ~p not created, record not found!", [CacheName]),
{stop, no_cache};
{ok, Record=#cache_record{config=Config, storage=DB}} ->
erlang:register(CacheName, self()),
NewDB = g_storage:create(CacheName, Config, DB),
NewRecord = Record#cache_record{storage=NewDB},
gibreel_db:store(NewRecord),
Task = start_timer(Config),
setup_columbo(NewRecord),
error_logger:info_msg("Cache ~p created on [~p]...\n", [CacheName, self()]),
sync(NewRecord),
{ok, #state{record=NewRecord, task=Task}}
end.
%% handle_call
handle_call(Msg, _From, State) ->
error_logger:info_msg("handle_call(~p)", [Msg]),
{noreply, State}.
%% handle_cast
handle_cast({reload_config}, State=#state{record=#cache_record{name=CacheName}}) ->
{ok, Record} = gibreel_db:find(CacheName),
{noreply, State#state{record=Record}};
handle_cast(Msg, State) ->
error_logger:info_msg("handle_cast(~p)", [Msg]),
{noreply, State}.
%% handle_info
handle_info({cluster_msg, {get, Key, From, Ref}}, State=#state{record=Record}) ->
async:run(fun() ->
Reply = select(Key, Record),
From ! {cluster_msg, {value, Ref, Reply}}
end),
{noreply, State};
handle_info({cluster_msg, {store, Key, Value, NewVersion, Delay}}, State=#state{record=Record}) ->
async:run(fun() ->
api_store(Key, Value, ?NO_VERSION, Delay, NewVersion, Record)
end),
{noreply, State};
handle_info({cluster_msg, {touch, Key, Delay}}, State=#state{record=Record}) ->
async:run(fun() ->
api_touch(Key, Delay, Record)
end),
{noreply, State};
handle_info({cluster_msg, {remove, Key, NewVersion}}, State=#state{record=Record}) ->
async:run(fun() ->
api_remove(Key, ?NO_VERSION, NewVersion, Record)
end),
{noreply, State};
handle_info({run_purge}, State=#state{record=Record}) ->
purge(Record),
{noreply, State};
handle_info({cluster_msg, {sync, From}}, State=#state{record=Record}) ->
run_sync(Record, From),
{noreply, State};
handle_info({cluster_msg, {flush, Version}}, State=#state{record=Record}) ->
api_flush(Version, Record),
{noreply, State};
handle_info({stop_cache}, State) ->
{stop, normal, State}.
%% terminate
terminate(_Reason, #state{record=#cache_record{name=CacheName, storage=DB}, task=Task}) ->
stop_timer(Task),
erlang:unregister(CacheName),
g_storage:drop(DB),
ok.
%% code_change
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% ====================================================================
%% Internal functions
%% ====================================================================
% API
run_get(Key, Delay, Record=#cache_record{config=Config, storage=DB}) ->
case g_storage:find(DB, Key) of
{error, not_found} ->
case find_value(Key, Record) of
{ok, Value, Version} -> {ok, Value, Version};
Other -> Other
end;
{ok, Value, StoredVersion, Timeout} ->
case Config#cache_config.max_age of
?NO_MAX_AGE ->
touch_if_needed(Key, Delay, Record),
{ok, Value, StoredVersion};
_ ->
Now = current_time(),
if Now =< Timeout ->
touch_if_needed(Key, Delay, Record),
{ok, Value, StoredVersion};
true ->
case find_value(Key, Record) of
{ok, Value, Version} -> {ok, Value, Version};
Other ->
async:run(fun() ->
delete(Key, StoredVersion, Record)
end),
Other
end
end
end
end.
run_store(Key, Value, OldVersion, Delay, Record=#cache_record{name=CacheName, config=Config}) ->
Fun = fun(Version) ->
api_store(Key, Value, OldVersion, Delay, Version, Record)
end,
case brute_force(Fun, version()) of
{ok, Version} ->
async:run(fun() ->
cluster_notify(CacheName, {store, Key, Value, Version, Delay}, Config#cache_config.cluster_nodes)
end),
{ok, Version};
invalid_version -> invalid_version
end.
run_remove(Key, OldVersion, Record=#cache_record{name=CacheName, config=Config}) ->
Fun = fun(Version) ->
api_remove(Key, OldVersion, Version, Record)
end,
case brute_force(Fun, version()) of
{ok, Version} ->
async:run(fun() ->
cluster_notify(CacheName, {remove, Key, Version}, Config#cache_config.cluster_nodes)
end),
ok;
invalid_version -> invalid_version
end.
run_touch(Key, Delay, Record=#cache_record{name=CacheName, config=Config}) ->
case api_touch(Key, Delay, Record) of
true ->
async:run(fun() ->
cluster_notify(CacheName, {touch, Key, Delay}, Config#cache_config.cluster_nodes)
end);
false -> ok
end.
run_size(#cache_record{storage=DB}) -> g_storage:size(DB).
run_get_keys(#cache_record{storage=DB}) -> g_storage:keys(DB).
run_flush(Record=#cache_record{name=CacheName, config=Config}) ->
Version = version(),
api_flush(Version, Record),
async:run(fun() ->
cluster_notify(CacheName, {flush, Version}, Config#cache_config.cluster_nodes)
end),
ok.
run_foldl(Fun, Acc, #cache_record{storage=DB}) -> g_storage:foldl(DB, Fun, Acc).
% Server and API
sync(#cache_record{config=#cache_config{sync_mode=?LAZY_SYNC_MODE}}) -> ok;
sync(#cache_record{config=#cache_config{cluster_nodes=?CLUSTER_NODES_LOCAL}}) -> ok;
sync(Record=#cache_record{config=#cache_config{get_value_function=?NO_FUNCTION, cluster_nodes=Nodes}}) ->
Fun = fun() ->
Count = cluster_notify(Record#cache_record.name, {sync, self()}, Nodes),
KeyList = receive_keys(Count),
request_values(KeyList, Record)
end,
async:run(Fun);
sync(_Record) -> ok.
api_store(Key, Value, OldVersion, Delay, NewVersion, Record=#cache_record{config=Config, storage=DB}) ->
case validate_change(DB, Key, OldVersion, NewVersion) of
not_exists ->
insert(Key, Value, NewVersion, get_timeout(Delay, Config), Record),
ok;
{exists, StoredVersion} ->
delete(Key, StoredVersion, Record),
insert(Key, Value, NewVersion, get_timeout(Delay, Config), Record),
ok;
Other -> Other
end.
find_value(_Key, #cache_record{config=#cache_config{get_value_function=?NO_FUNCTION, cluster_nodes=?CLUSTER_NODES_LOCAL}}) ->
not_found;
find_value(_Key, #cache_record{config=#cache_config{get_value_function=?NO_FUNCTION, sync_mode=?FULL_SYNC_MODE}}) ->
not_found;
find_value(Key, Record=#cache_record{config=#cache_config{get_value_function=?NO_FUNCTION, cluster_nodes=Nodes}}) ->
case cluster_get(Key, Record#cache_record.name, Nodes) of
{ok, Value, Version} ->
async:run(fun() ->
api_store(Key, Value, ?NO_VERSION, ?USE_DEFAULT_EXPIRE, Version, Record)
end),
{ok, Value, Version};
not_found -> not_found
end;
find_value(Key, Record=#cache_record{config=#cache_config{get_value_function=Function}}) ->
try Function(Key) of
not_found -> not_found;
error -> error;
Value ->
Version = version(),
async:run(fun() ->
api_store(Key, Value, ?NO_VERSION, ?USE_DEFAULT_EXPIRE, Version, Record)
end),
{ok, Value, Version}
catch
Type:Error ->
error_logger:error_msg("~p(~p) [~p:~p]\n", [Function, Key, Type, Error]),
error
end.
touch_if_needed(_Key, ?NO_TOUCH, _Record) -> ok;
touch_if_needed(Key, Delay, Record) ->
run_touch(Key, Delay, Record).
api_remove(Key, OldVersion, NewVersion, Record=#cache_record{storage=DB}) ->
case validate_change(DB, Key, OldVersion, NewVersion) of
not_exists -> ok;
{exists, StoredVersion} ->
delete(Key, StoredVersion, Record),
ok;
Other -> Other
end.
api_flush(RefVersion, Record=#cache_record{storage=DB}) ->
Fun = fun() ->
Keys = g_storage:keys(DB, RefVersion),
lists:foreach(fun({Key, Version}) ->
delete(Key, Version, Record)
end, Keys)
end,
async:run(Fun).
api_touch(_Key, _Delay, #cache_record{config=#cache_config{max_age=?NO_MAX_AGE}}) -> false;
api_touch(Key, Delay, #cache_record{config=Config, storage=DB}) ->
Timeout = get_timeout(Delay, Config),
g_storage:touch(DB, Key, Timeout).
purge(#cache_record{config=#cache_config{max_age=?NO_MAX_AGE}}) -> ok;
purge(Record=#cache_record{storage=DB}) ->
Fun = fun() ->
Now = current_time(),
Keys = g_storage:expired(DB, Now),
lists:foreach(fun({Key, Version}) ->
delete(Key, Version, Record)
end, Keys)
end,
async:run(Fun).
select(Key, #cache_record{storage=DB}) ->
case g_storage:find(DB, Key) of
{error, not_found} -> not_found;
{ok, Value, Version, _Timeout} -> {ok, Value, Version}
end.
run_sync(Record, From) ->
Fun = fun() ->
Keys = run_get_keys(Record),
From ! {cluster_msg, {keys, Keys}}
end,
async:run(Fun).
% BL Utils
receive_keys(0) -> [];
receive_keys(_) ->
receive
{cluster_msg, {keys, List}} -> List
after ?SYNC_TIMEOUT -> []
end.
request_values([], _Record) -> ok;
request_values([Key|T], Record) ->
case cluster_get(Key, Record#cache_record.name, Record#cache_record.config#cache_config.cluster_nodes) of
{ok, Value, Version} -> api_store(Key, Value, ?NO_VERSION, ?USE_DEFAULT_EXPIRE, Version, Record);
_ -> ok
end,
request_values(T, Record).
validate_change(DB, Key, OldVersion, NewVersion) ->
case g_storage:find(DB, Key) of
{error, not_found} -> validate_operation(OldVersion, ?NO_RECORD, NewVersion);
{ok, _Value, StoredVersion, _Timeout} -> validate_operation(OldVersion, StoredVersion, NewVersion)
end.
validate_operation(?NO_VERSION, ?NO_RECORD, _NewVersion) -> not_exists;
validate_operation(?NO_VERSION, StoredVersion, NewVersion) ->
case StoredVersion =< NewVersion of
true -> {exists, StoredVersion};
_ -> {false, StoredVersion}
end;
validate_operation(_OldVersion, ?NO_RECORD, _NewVersion) -> {false, invalid_version};
validate_operation(StoredVersion, StoredVersion, NewVersion) ->
case StoredVersion =< NewVersion of
true -> {exists, StoredVersion};
_ -> {false, StoredVersion}
end;
validate_operation(_OldVersion, _StoredVersion, _NewVersion) -> {false, invalid_version}.
get_timeout(_Delay, #cache_config{max_age=?NO_MAX_AGE}) -> 0;
get_timeout(?USE_DEFAULT_EXPIRE, #cache_config{max_age=Expire}) ->
current_time() + Expire;
get_timeout(Delay, _Config) ->
current_time() + Delay.
cluster_get(Key, CacheName, Nodes) ->
Ref = make_ref(),
RequestsSent = cluster_notify(CacheName, {get, Key, self(), Ref}, Nodes),
receive_values(Ref, RequestsSent, 0).
receive_values(_Ref, Size, Size) -> not_found;
receive_values(Ref, Size, Count) ->
receive
{cluster_msg, {value, Ref, {ok, Value, Version}}} -> {ok, Value, Version};
{cluster_msg, {value, Ref, _}} -> receive_values(Ref, Size, Count + 1);
{cluster_msg, {value, _, _}} -> receive_values(Ref, Size, Count)
after ?CLUSTER_TIMEOUT -> not_found
end.
% Low level
insert(Key, Value, Version, Timeout, Record=#cache_record{storage=DB}) ->
Count = g_storage:insert(DB, Key, Value, Version, Timeout),
update_counter(Record, Count).
delete(Key, Version, Record=#cache_record{storage=DB}) ->
Count = g_storage:delete(DB, Key, Version),
update_counter(Record, Count).
update_counter(Record=#cache_record{config=Config}, TableSize) ->
case Config#cache_config.max_size of
?NO_MAX_SIZE -> ok;
MaxSize ->
if
TableSize > MaxSize ->
async:run(fun() ->
delete_older(Record)
end);
true -> ok
end
end.
delete_older(Record=#cache_record{storage=DB}) ->
case g_storage:older(DB) of
{error, not_found} -> ok;
{ok, Key, Version} -> delete(Key, Version, Record)
end.
% System
start_timer(#cache_config{purge_interval=?NO_PURGE}) -> ?NO_TASK;
start_timer(#cache_config{purge_interval=Interval}) ->
TimerInterval = Interval * 1000,
{ok, Task} = timer:send_interval(TimerInterval, {run_purge}),
Task.
setup_columbo(#cache_record{config=#cache_config{cluster_nodes=?CLUSTER_NODES_LOCAL}}) -> ok;
setup_columbo(#cache_record{config=#cache_config{cluster_nodes=?CLUSTER_NODES_ALL}}) -> ok;
setup_columbo(#cache_record{config=#cache_config{cluster_nodes=Nodes}}) ->
columbo:add_nodes(Nodes).
cluster_notify(_CacheName, _Msg, ?CLUSTER_NODES_LOCAL) -> 0;
cluster_notify(CacheName, Msg, ?CLUSTER_NODES_ALL) ->
columbo:send_to_all(CacheName, {cluster_msg, Msg});
cluster_notify(CacheName, Msg, Nodes) ->
columbo:send_to_nodes(CacheName, Nodes, {cluster_msg, Msg}).
stop_timer(?NO_TASK) -> ok;
stop_timer(Task) ->
timer:cancel(Task).
% Util
brute_force(Fun, Version) ->
case Fun(Version) of
ok -> {ok, Version};
{false, invalid_version} -> invalid_version;
{false, OldVersion} -> brute_force(Fun, OldVersion + 1)
end.
version() ->
cclock:cluster_timestamp().
current_time() ->
{Mega, Sec, _} = os:timestamp(),
(Mega * 1000000) + Sec.
get_option_value(Tag, Options, Default) ->
case lists:keyfind(Tag, 1, Options) of
false -> Default;
{_, Value} -> Value
end.