%%% 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/4, size/0]). -export([get_by_index/1, get_by_indices/3, get_by_leaf_hash/1, get_by_entry_hash/1]). %% 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(binary(), binary(), binary(), non_neg_integer()) -> ok. add(LeafHash, EntryHash, Data, Index) -> gen_server:call(?MODULE, {add, {LeafHash, EntryHash, Data, Index}}). -spec get_by_indices(non_neg_integer(), non_neg_integer(), {sorted, true|false}) -> [{non_neg_integer(), binary(), binary()}]. get_by_indices(Start, End, {sorted, Sorted}) -> gen_server:call(?MODULE, {get_by_indices, {Start, End, Sorted}}). -spec get_by_index(binary()) -> notfound | {non_neg_integer(), binary(), binary()}. get_by_index(Index) -> gen_server:call(?MODULE, {get_by_index, Index}). -spec get_by_leaf_hash(binary()) -> notfound | {non_neg_integer(), binary(), binary()}. get_by_leaf_hash(LeafHash) -> gen_server:call(?MODULE, {get_by_leaf_hash, LeafHash}). -spec get_by_entry_hash(binary()) -> notfound | {non_neg_integer(), binary(), binary()}. get_by_entry_hash(EntryHash) -> gen_server:call(?MODULE, {get_by_entry_hash, EntryHash}). %% 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, {LeafHash, EntryHash, Data, Index}}, _From, State) -> R = mnesia:transaction( fun() -> mnesia:write( #plop{ index = Index, mtlhash = LeafHash, entryhash = EntryHash, logentry = Data}) end), {reply, R, State}; handle_call({get_by_indices, {Start, End, Sorted}}, _From, State) -> R = case Sorted of false -> select_index(Start, End); true -> %% FIXME: RAM hog -- how bad is it? lists:sort(select_index(Start, End)) end, {reply, R, State}; handle_call({get_by_index, Index}, _From, State) -> {reply, find_entry(fun() -> mnesia:read(plop, Index) end), State}; handle_call({get_by_leaf_hash, LeafHash}, _From, State) -> {reply, find_entry(fun() -> mnesia:index_read(plop, LeafHash, #plop.mtlhash) end), State}; handle_call({get_by_entry_hash, EntryHash}, _From, State) -> {reply, find_entry(fun() -> mnesia:index_read(plop, EntryHash, #plop.entryhash) end), State}; handle_call({dump, Table}, _From, State) -> R = mnesia:transaction( fun() -> Q = qlc:q([E || E <- mnesia:table(Table)]), qlc:e(Q) end), {reply, R, State}. %%%%%%%%%%%%%%%%%%%% %% Helper functions. -spec select_index(non_neg_integer(), non_neg_integer()) -> [{non_neg_integer(), binary(), binary()}]. select_index(Start, End) -> F = fun() -> %% Get index, mtlhash and logentry. 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()) -> notfound | {non_neg_integer(), binary(), binary()} | duplicate. find_entry(Fun) -> {atomic, Result} = mnesia:transaction(Fun), case Result of [] -> notfound; [#plop{index = I, mtlhash = H, logentry = E}] -> {I, H, E}; _ -> duplicate end.