%% ------------------------------------------------------------------- %% %% Copyright (c) 2007-2015 Basho Technologies, Inc. All Rights Reserved. %% %% This file is provided to you 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(machi_projection_store). -include("machi_projection.hrl"). %% API -export([ start_link/3, get_latest_epoch/2, get_latest_epoch/3, read_latest_projection/2, read_latest_projection/3, read/3, read/4, write/3, write/4, get_all_projections/2, get_all_projections/3, list_all_projections/2, list_all_projections/3 ]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(NO_EPOCH, {-1,<<0:(20*8)/big>>}). -record(state, { public_dir = "" :: string(), private_dir = "" :: string(), wedged = true :: boolean(), wedge_notify_pid :: pid() | atom(), max_public_epoch = ?NO_EPOCH :: {-1 | non_neg_integer(), binary()}, max_private_epoch = ?NO_EPOCH :: {-1 | non_neg_integer(), binary()} }). start_link(RegName, DataDir, NotifyWedgeStateChanges) -> gen_server:start_link({local, RegName}, ?MODULE, [DataDir, NotifyWedgeStateChanges], []). get_latest_epoch(PidSpec, ProjType) -> get_latest_epoch(PidSpec, ProjType, infinity). get_latest_epoch(PidSpec, ProjType, Timeout) when ProjType == 'public' orelse ProjType == 'private' -> g_call(PidSpec, {get_latest_epoch, ProjType}, Timeout). read_latest_projection(PidSpec, ProjType) -> read_latest_projection(PidSpec, ProjType, infinity). read_latest_projection(PidSpec, ProjType, Timeout) when ProjType == 'public' orelse ProjType == 'private' -> g_call(PidSpec, {read_latest_projection, ProjType}, Timeout). read(PidSpec, ProjType, Epoch) -> read(PidSpec, ProjType, Epoch, infinity). read(PidSpec, ProjType, Epoch, Timeout) when ProjType == 'public' orelse ProjType == 'private', is_integer(Epoch), Epoch >= 0 -> g_call(PidSpec, {read, ProjType, Epoch}, Timeout). write(PidSpec, ProjType, Proj) -> write(PidSpec, ProjType, Proj, infinity). write(PidSpec, ProjType, Proj, Timeout) when ProjType == 'public' orelse ProjType == 'private', is_record(Proj, projection_v1), is_integer(Proj#projection_v1.epoch_number), Proj#projection_v1.epoch_number >= 0 -> g_call(PidSpec, {write, ProjType, Proj}, Timeout). get_all_projections(PidSpec, ProjType) -> get_all_projections(PidSpec, ProjType, infinity). get_all_projections(PidSpec, ProjType, Timeout) when ProjType == 'public' orelse ProjType == 'private' -> g_call(PidSpec, {get_all_projections, ProjType}, Timeout). list_all_projections(PidSpec, ProjType) -> list_all_projections(PidSpec, ProjType, infinity). list_all_projections(PidSpec, ProjType, Timeout) when ProjType == 'public' orelse ProjType == 'private' -> g_call(PidSpec, {list_all_projections, ProjType}, Timeout). %%%%%%%%%%%%%%%%%%%%%%%%%%% g_call(PidSpec, Arg, Timeout) -> LC1 = lclock_get(), {Res, LC2} = gen_server:call(PidSpec, {Arg, LC1}, Timeout), lclock_update(LC2), Res. %%%%%%%%%%%%%%%%%%%%%%%%%%% init([DataDir, NotifyWedgeStateChanges]) -> lclock_init(), PublicDir = machi_util:make_projection_filename(DataDir, "public"), PrivateDir = machi_util:make_projection_filename(DataDir, "private"), ok = filelib:ensure_dir(PublicDir ++ "/ignored"), ok = filelib:ensure_dir(PrivateDir ++ "/ignored"), MaxPublicEpoch = find_max_epoch(PublicDir), MaxPrivateEpoch = find_max_epoch(PrivateDir), {ok, #state{public_dir=PublicDir, private_dir=PrivateDir, wedged=true, wedge_notify_pid=NotifyWedgeStateChanges, max_public_epoch=MaxPublicEpoch, max_private_epoch=MaxPrivateEpoch}}. handle_call({{get_latest_epoch, ProjType}, LC1}, _From, S) -> LC2 = lclock_update(LC1), EpochT = if ProjType == public -> S#state.max_public_epoch; ProjType == private -> S#state.max_private_epoch end, {reply, {{ok, EpochT}, LC2}, S}; handle_call({{read_latest_projection, ProjType}, LC1}, _From, S) -> LC2 = lclock_update(LC1), {EpochNum, _CSum} = if ProjType == public -> S#state.max_public_epoch; ProjType == private -> S#state.max_private_epoch end, {Reply, NewS} = do_proj_read(ProjType, EpochNum, S), {reply, {Reply, LC2}, NewS}; handle_call({{read, ProjType, Epoch}, LC1}, _From, S) -> LC2 = lclock_update(LC1), {Reply, NewS} = do_proj_read(ProjType, Epoch, S), {reply, {Reply, LC2}, NewS}; handle_call({{write, ProjType, Proj}, LC1}, _From, S) -> LC2 = lclock_update(LC1), {Reply, NewS} = do_proj_write(ProjType, Proj, S), {reply, {Reply, LC2}, NewS}; handle_call({{get_all_projections, ProjType}, LC1}, _From, S) -> LC2 = lclock_update(LC1), Dir = pick_path(ProjType, S), Epochs = find_all(Dir), All = [begin {{ok, Proj}, _} = do_proj_read(ProjType, Epoch, S), Proj end || Epoch <- Epochs], {reply, {{ok, All}, LC2}, S}; handle_call({{list_all_projections, ProjType}, LC1}, _From, S) -> LC2 = lclock_update(LC1), Dir = pick_path(ProjType, S), {reply, {{ok, find_all(Dir)}, LC2}, S}; handle_call(_Request, _From, S) -> Reply = whaaaaaaaaaaaaa, {reply, Reply, S}. handle_cast(_Msg, S) -> {noreply, S}. handle_info(_Info, S) -> {noreply, S}. terminate(_Reason, _S) -> ok. code_change(_OldVsn, S, _Extra) -> {ok, S}. %%%%%%%%%%%%%%%%%%%%%%%%%%% do_proj_read(_ProjType, Epoch, S) when Epoch < 0 -> {{error, not_written}, S}; do_proj_read(ProjType, Epoch, S_or_Dir) -> Dir = if is_record(S_or_Dir, state) -> pick_path(ProjType, S_or_Dir); is_list(S_or_Dir) -> S_or_Dir end, Path = filename:join(Dir, epoch2name(Epoch)), case file:read_file(Path) of {ok, Bin} -> %% TODO and if Bin is corrupt? (even if binary_to_term() succeeds) {{ok, binary_to_term(Bin)}, S_or_Dir}; {error, enoent} -> {{error, not_written}, S_or_Dir}; {error, Else} -> {{error, Else}, S_or_Dir} end. do_proj_write(ProjType, #projection_v1{epoch_number=Epoch}=Proj, S) -> %% TODO: We probably ought to check the projection checksum for sanity, eh? Dir = pick_path(ProjType, S), Path = filename:join(Dir, epoch2name(Epoch)), case file:read_file_info(Path) of {ok, _FI} -> {{error, written}, S}; {error, enoent} -> {ok, FH} = file:open(Path, [write, raw, binary]), ok = file:write(FH, term_to_binary(Proj)), ok = file:sync(FH), ok = file:close(FH), EpochT = {Epoch, Proj#projection_v1.epoch_csum}, NewS = if ProjType == public, Epoch > element(1, S#state.max_public_epoch) -> io:format(user, "TODO: tell ~p we are wedged by epoch ~p\n", [S#state.wedge_notify_pid, Epoch]), S#state{max_public_epoch=EpochT, wedged=true}; ProjType == private, Epoch > element(1, S#state.max_private_epoch) -> io:format(user, "TODO: tell ~p we are unwedged by epoch ~p\n", [S#state.wedge_notify_pid, Epoch]), S#state{max_private_epoch=EpochT, wedged=false}; true -> S end, {ok, NewS}; {error, Else} -> {{error, Else}, S} end. pick_path(public, S) -> S#state.public_dir; pick_path(private, S) -> S#state.private_dir. epoch2name(Epoch) -> machi_util:int_to_hexstr(Epoch, 32). name2epoch(Name) -> machi_util:hexstr_to_int(Name). find_all(Dir) -> Fs = filelib:wildcard("*", Dir), lists:sort([name2epoch(F) || F <- Fs]). find_max_epoch(Dir) -> Fs = lists:sort(filelib:wildcard("*", Dir)), if Fs == [] -> ?NO_EPOCH; true -> EpochNum = name2epoch(lists:last(Fs)), {{ok, Proj}, _} = do_proj_read(proj_type_ignored, EpochNum, Dir), {EpochNum, Proj} end. %%%%%%%%%%%%%%%%%%%%%%%%%%% -ifdef(TEST). lclock_init() -> lamport_clock:init(). lclock_get() -> lamport_clock:get(). lclock_update(LC) -> lamport_clock:update(LC). -else. % TEST lclock_init() -> ok. lclock_get() -> ok. lclock_update(_LC) -> ok. -endif. % TEST