%%% Copyright (c) 2014, NORDUnet A/S. %%% See LICENSE for licensing information. -module(db). -behaviour(gen_server). %% API. -export([start_link/0, stop/0]). -export([init_db/0, init_db/1, init_tables/0, init_tables/1]). -export([add/1, find/2, get_by_index/2, get_by_index_sorted/2, size/0]). %% API for testing. -export([dump/1, destroy_tables/0, info_tables/0, dump_to_file/1]). %% gen_server callbacks. -export([init/1, handle_call/3, terminate/2, handle_cast/2, handle_info/2, code_change/3]). -include_lib("stdlib/include/qlc.hrl"). -include("db.hrl"). -include("$CTROOT/plop/include/plop.hrl"). %% @doc Set up a database schema on all nodes that are to be included %% in the "database cluster". Has to be run _before_ mnesia has been %% started. init_db() -> init_db([node()]). init_db(Nodes) -> ok = mnesia:create_schema(Nodes), rpc:multicall(Nodes, application, start, [mnesia]), init_tables(Nodes), rpc:multicall(Nodes, application, stop, [mnesia]). %% @doc Run once, or rather every time you start on a new database. %% If run more than once, we'll get {aborted, {already_exists, TABLE}}. init_tables() -> init_tables([node()]). init_tables(Nodes) -> %% We've once upon a time invoked mnesia:create_schema/1 with the %% nodes that will be part of the database. RamCopies = [], DiscCopies = [], DiscOnlyCopies = Nodes, mnesia:start(), {atomic, ok} = mnesia:create_table(plop, [{type, set}, {ram_copies, RamCopies}, {disc_copies, DiscCopies}, {disc_only_copies, DiscOnlyCopies}, {attributes, record_info(fields, plop)}, {majority, true}]), {atomic, ok} = mnesia:add_table_index(plop, entryhash), {atomic, ok} = mnesia:add_table_index(plop, mtlhash). destroy_tables() -> mnesia:delete_table(plop). info_tables() -> mnesia:table_info(plop, all). dump_to_file(Filename) -> mnesia:dump_to_textfile(Filename). size() -> mnesia:table_info(plop, size). init(_Args) -> {mnesia:wait_for_tables([plop], 5000), []}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). stop() -> gen_server:call(?MODULE, stop). %%%%%%%%%%%%%%%%%%%% %% Public API. -spec add(plop()) -> {atomic, ok}. add(Entry) -> gen_server:call(?MODULE, {add, Entry}). %% @doc Find one entry. -spec find(entryhash | mtlhash | index, binary()) -> [] | plop() | duplicate_hash_in_db. find(Type, Hash) -> gen_server:call(?MODULE, {find, Type, Hash}). -spec get_by_index(non_neg_integer(), non_neg_integer()) -> [{mtl(), binary()}]. get_by_index(Start, End) -> gen_server:call(?MODULE, {get_by_index, {Start, End}}). -spec get_by_index_sorted(non_neg_integer(), non_neg_integer()) -> [{mtl(), binary()}]. get_by_index_sorted(Start, End) -> gen_server:call(?MODULE, {get_by_index_sorted, {Start, End}}). %% Testing and debugging. dump(Table) -> gen_server:call(?MODULE, {dump, Table}). %%%%%%%%%%%%%%%%%%%% %% gen_server callbacks. handle_cast(_Request, State) -> {noreply, State}. handle_info(_Info, State) -> {noreply, State}. code_change(_OldVsn, State, _Extra) -> {ok, State}. terminate(_Reason, _State) -> io:format("~p terminating~n", [?MODULE]), ok. %%%%%%%%%%%%%%%%%%%% %% The meat. handle_call(stop, _From, State) -> {stop, normal, stopped, State}; handle_call({add, Entry}, _From, State) -> F = fun() -> mnesia:write(Entry) end, Res = mnesia:transaction(F), {reply, Res, State}; handle_call({dump, Table}, _From, State) -> F = fun() -> Q = qlc:q([E || E <- mnesia:table(Table)]), qlc:e(Q) end, Res = mnesia:transaction(F), {reply, Res, State}; handle_call({find, entryhash, Hash}, _From, State) -> {reply, find_entry(fun() -> mnesia:index_read(plop, Hash, #plop.entryhash) end), State}; handle_call({find, mtlhash, Hash}, _From, State) -> {reply, find_entry(fun() -> mnesia:index_read(plop, Hash, #plop.mtlhash) end), State}; handle_call({find, index, Index}, _From, State) -> {reply, find_entry(fun() -> mnesia:read(plop, Index) end), State}; handle_call({get_by_index, {Start, End}}, _From, State) -> Res = [{MTL, Extra} || [_Index, MTL, Extra] <- select_index(Start, End)], {reply, Res, State}; handle_call({get_by_index_sorted, {Start, End}}, _From, State) -> %% FIXME: RAM hog -- how bad is it? Res = [{MTL, Extra} || [_Index, MTL, Extra] <- lists:sort(select_index(Start, End))], {reply, Res, State}. %%%%%%%%%%%%%%%%%%%% %% Helper functions. select_index(Start, End) -> F = fun() -> %% Get index, mtl and extra_data. MatchHead = {plop, '$1', '_', '_', '$2', '$3', '_'}, Guard = [{'>=', '$1', Start}, {'=<', '$1', End}], Result = ['$$'], mnesia:select(plop, [{MatchHead, Guard, Result}]) end, {atomic, Res} = mnesia:transaction(F), Res. -spec find_entry(fun()) -> [] | plop() | duplicate_hash_in_db. find_entry(Fun) -> {atomic, Result} = mnesia:transaction(Fun), case length(Result) of 0 -> []; 1 -> hd(Result); _ -> duplicate_hash_in_db % FIXME: log an error? end.