%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1996-2018. All Rights Reserved.
%%
%% 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.
%%
%% %CopyrightEnd%
%%
%%
-module(mnesia_tm).
-export([
start/0,
init/1,
non_transaction/5,
transaction/6,
commit_participant/5,
dirty/2,
display_info/2,
do_update_op/3,
get_info/1,
get_transactions/0,
info/1,
mnesia_down/1,
prepare_checkpoint/2,
prepare_checkpoint/1, % Internal
prepare_snmp/3,
do_snmp/2,
put_activity_id/1,
put_activity_id/2,
block_tab/1,
unblock_tab/1,
fixtable/3,
new_cr_format/1
]).
%% sys callback functions
-export([system_continue/3,
system_terminate/4,
system_code_change/4
]).
-include("mnesia.hrl").
-import(mnesia_lib, [set/2]).
-import(mnesia_lib, [fatal/2, verbose/2, dbg_out/2]).
-record(state, {coordinators = gb_trees:empty(), participants = gb_trees:empty(), supervisor,
blocked_tabs = [], dirty_queue = [], fixed_tabs = []}).
%% Format on coordinators is [{Tid, EtsTabList} .....
-record(prep, {protocol = sym_trans,
%% async_dirty | sync_dirty | sym_trans | sync_sym_trans | asym_trans
records = [],
prev_tab = [], % initiate to a non valid table name
prev_types,
prev_snmp,
types,
majority = []
}).
-record(participant, {tid, pid, commit, disc_nodes = [],
ram_nodes = [], protocol = sym_trans}).
start() ->
mnesia_monitor:start_proc(?MODULE, ?MODULE, init, [self()]).
init(Parent) ->
register(?MODULE, self()),
process_flag(trap_exit, true),
process_flag(message_queue_data, off_heap),
%% Initialize the schema
IgnoreFallback = mnesia_monitor:get_env(ignore_fallback_at_startup),
mnesia_bup:tm_fallback_start(IgnoreFallback),
mnesia_schema:init(IgnoreFallback),
%% Handshake and initialize transaction recovery
mnesia_recover:init(),
Early = mnesia_monitor:init(),
AllOthers = mnesia_lib:uniq(Early ++ mnesia_lib:all_nodes()) -- [node()],
set(original_nodes, AllOthers),
mnesia_recover:connect_nodes(AllOthers),
%% Recover transactions, may wait for decision
case mnesia_monitor:use_dir() of
true ->
P = mnesia_dumper:opt_dump_log(startup), % previous log
L = mnesia_dumper:opt_dump_log(startup), % latest log
Msg = "Initial dump of log during startup: ~p~n",
mnesia_lib:verbose(Msg, [[P, L]]),
mnesia_log:init();
false ->
ignore
end,
mnesia_schema:purge_tmp_files(),
mnesia_recover:next_garb(),
mnesia_recover:next_check_overload(),
?eval_debug_fun({?MODULE, init}, [{nodes, AllOthers}]),
case val(debug) of
Debug when Debug /= debug, Debug /= trace ->
ignore;
_ ->
mnesia_subscr:subscribe(whereis(mnesia_event), {table, schema})
end,
proc_lib:init_ack(Parent, {ok, self()}),
doit_loop(#state{supervisor = Parent}).
%% Local function in order to avoid external function call
val(Var) ->
case ?catch_val_and_stack(Var) of
{'EXIT', Stacktrace} -> mnesia_lib:other_val(Var, Stacktrace);
Value -> Value
end.
reply({From,Ref}, R) ->
From ! {?MODULE, Ref, R};
reply(From, R) ->
From ! {?MODULE, node(), R}.
reply(From, R, State) ->
reply(From, R),
doit_loop(State).
req(R) ->
case whereis(?MODULE) of
undefined ->
{error, {node_not_running, node()}};
Pid ->
Ref = make_ref(),
Pid ! {{self(), Ref}, R},
rec(Pid, Ref)
end.
rec() ->
rec(whereis(?MODULE)).
rec(Pid) when is_pid(Pid) ->
receive
{?MODULE, _, Reply} ->
Reply;
{'EXIT', Pid, _} ->
{error, {node_not_running, node()}}
end;
rec(undefined) ->
{error, {node_not_running, node()}}.
rec(Pid, Ref) ->
receive
{?MODULE, Ref, Reply} ->
Reply;
{'EXIT', Pid, _} ->
{error, {node_not_running, node()}}
end.
tmlink({From, Ref}) when is_reference(Ref) ->
link(From);
tmlink(From) ->
link(From).
tmpid({Pid, _Ref}) when is_pid(Pid) ->
Pid;
tmpid(Pid) ->
Pid.
%% Returns a list of participant transaction Tid's
mnesia_down(Node) ->
%% Syncronously call needed in order to avoid
%% race with mnesia_tm's coordinator processes
%% that may restart and acquire new locks.
%% mnesia_monitor takes care of the sync
case whereis(?MODULE) of
undefined ->
mnesia_monitor:mnesia_down(?MODULE, Node);
Pid ->
Pid ! {mnesia_down, Node},
ok
end.
prepare_checkpoint(Nodes, Cp) ->
rpc:multicall(Nodes, ?MODULE, prepare_checkpoint, [Cp]).
prepare_checkpoint(Cp) ->
req({prepare_checkpoint,Cp}).
block_tab(Tab) ->
req({block_tab, Tab}).
unblock_tab(Tab) ->
req({unblock_tab, Tab}).
doit_loop(#state{coordinators=Coordinators,participants=Participants,supervisor=Sup}=State) ->
receive
{_From, {async_dirty, Tid, Commit, Tab}} ->
case lists:member(Tab, State#state.blocked_tabs) of
false ->
do_async_dirty(Tid, new_cr_format(Commit), Tab),
doit_loop(State);
true ->
Item = {async_dirty, Tid, new_cr_format(Commit), Tab},
State2 = State#state{dirty_queue = [Item | State#state.dirty_queue]},
doit_loop(State2)
end;
{From, {sync_dirty, Tid, Commit, Tab}} ->
case lists:member(Tab, State#state.blocked_tabs) of
false ->
do_sync_dirty(From, Tid, new_cr_format(Commit), Tab),
doit_loop(State);
true ->
Item = {sync_dirty, From, Tid, new_cr_format(Commit), Tab},
State2 = State#state{dirty_queue = [Item | State#state.dirty_queue]},
doit_loop(State2)
end;
{From, start_outer} -> %% Create and associate ets_tab with Tid
try ?ets_new_table(mnesia_trans_store, [bag, public]) of
Etab ->
tmlink(From),
C = mnesia_recover:incr_trans_tid_serial(),
?ets_insert(Etab, {nodes, node()}),
Tid = #tid{pid = tmpid(From), counter = C},
A2 = gb_trees:insert(Tid,[Etab],Coordinators),
S2 = State#state{coordinators = A2},
reply(From, {new_tid, Tid, Etab}, S2)
catch error:Reason -> %% system limit
Msg = "Cannot create an ets table for the "
"local transaction store",
reply(From, {error, {system_limit, Msg, Reason}}, State)
end;
{From, {ask_commit, Protocol, Tid, Commit0, DiscNs, RamNs}} ->
?eval_debug_fun({?MODULE, doit_ask_commit},
[{tid, Tid}, {prot, Protocol}]),
mnesia_checkpoint:tm_enter_pending(Tid, DiscNs, RamNs),
Commit = new_cr_format(Commit0),
Pid =
case Protocol of
asym_trans when node(Tid#tid.pid) /= node() ->
Args = [tmpid(From), Tid, Commit, DiscNs, RamNs],
spawn_link(?MODULE, commit_participant, Args);
_ when node(Tid#tid.pid) /= node() -> %% *_sym_trans
reply(From, {vote_yes, Tid}),
nopid
end,
P = #participant{tid = Tid,
pid = Pid,
commit = Commit,
disc_nodes = DiscNs,
ram_nodes = RamNs,
protocol = Protocol},
State2 = State#state{participants = gb_trees:insert(Tid,P,Participants)},
doit_loop(State2);
{Tid, do_commit} ->
case gb_trees:lookup(Tid, Participants) of
none ->
verbose("Tried to commit a non participant transaction ~p~n",[Tid]),
doit_loop(State);
{value, P} ->
?eval_debug_fun({?MODULE,do_commit,pre},[{tid,Tid},{participant,P}]),
case P#participant.pid of
nopid ->
Commit = P#participant.commit,
Member = lists:member(node(), P#participant.disc_nodes),
if Member == false ->
ignore;
P#participant.protocol == sym_trans ->
mnesia_log:log(Commit);
P#participant.protocol == sync_sym_trans ->
mnesia_log:slog(Commit)
end,
mnesia_recover:note_decision(Tid, committed),
do_commit(Tid, Commit),
if
P#participant.protocol == sync_sym_trans ->
Tid#tid.pid ! {?MODULE, node(), {committed, Tid}};
true ->
ignore
end,
mnesia_locker:release_tid(Tid),
transaction_terminated(Tid),
?eval_debug_fun({?MODULE,do_commit,post},[{tid,Tid},{pid,nopid}]),
doit_loop(State#state{participants=
gb_trees:delete(Tid,Participants)});
Pid when is_pid(Pid) ->
Pid ! {Tid, committed},
?eval_debug_fun({?MODULE, do_commit, post}, [{tid, Tid}, {pid, Pid}]),
doit_loop(State)
end
end;
{Tid, simple_commit} ->
mnesia_recover:note_decision(Tid, committed),
mnesia_locker:release_tid(Tid),
transaction_terminated(Tid),
doit_loop(State);
{Tid, {do_abort, Reason}} ->
?eval_debug_fun({?MODULE, do_abort, pre}, [{tid, Tid}]),
case gb_trees:lookup(Tid, Participants) of
none ->
verbose("Tried to abort a non participant transaction ~p: ~tp~n",
[Tid, Reason]),
mnesia_locker:release_tid(Tid),
doit_loop(State);
{value, P} ->
case P#participant.pid of
nopid ->
Commit = P#participant.commit,
mnesia_recover:note_decision(Tid, aborted),
do_abort(Tid, Commit),
if
P#participant.protocol == sync_sym_trans ->
Tid#tid.pid ! {?MODULE, node(), {aborted, Tid}};
true ->
ignore
end,
transaction_terminated(Tid),
mnesia_locker:release_tid(Tid),
?eval_debug_fun({?MODULE, do_abort, post}, [{tid, Tid}, {pid, nopid}]),
doit_loop(State#state{participants=
gb_trees:delete(Tid,Participants)});
Pid when is_pid(Pid) ->
Pid ! {Tid, {do_abort, Reason}},
?eval_debug_fun({?MODULE, do_abort, post},
[{tid, Tid}, {pid, Pid}]),
doit_loop(State)
end
end;
{From, {add_store, Tid}} -> %% new store for nested transaction
try ?ets_new_table(mnesia_trans_store, [bag, public]) of
Etab ->
A2 = add_coord_store(Coordinators, Tid, Etab),
reply(From, {new_store, Etab},
State#state{coordinators = A2})
catch error:Reason -> %% system limit
Msg = "Cannot create an ets table for a nested "
"local transaction store",
reply(From, {error, {system_limit, Msg, Reason}}, State)
end;
{From, {del_store, Tid, Current, Obsolete, PropagateStore}} ->
opt_propagate_store(Current, Obsolete, PropagateStore),
A2 = del_coord_store(Coordinators, Tid, Current, Obsolete),
reply(From, store_erased, State#state{coordinators = A2});
{'EXIT', Pid, Reason} ->
handle_exit(Pid, Reason, State);
{From, {restart, Tid, Store}} ->
A2 = restore_stores(Coordinators, Tid, Store),
clear_fixtable([Store]),
?ets_match_delete(Store, '_'),
?ets_insert(Store, {nodes, node()}),
reply(From, {restarted, Tid}, State#state{coordinators = A2});
{delete_transaction, Tid} ->
%% used to clear transactions which are committed
%% in coordinator or participant processes
case gb_trees:is_defined(Tid, Participants) of
false ->
case gb_trees:lookup(Tid, Coordinators) of
none ->
verbose("** ERROR ** Tried to delete a non transaction ~p~n",
[Tid]),
doit_loop(State);
{value, Etabs} ->
clear_fixtable(Etabs),
erase_ets_tabs(Etabs),
transaction_terminated(Tid),
doit_loop(State#state{coordinators =
gb_trees:delete(Tid,Coordinators)})
end;
true ->
transaction_terminated(Tid),
State2 = State#state{participants=gb_trees:delete(Tid,Participants)},
doit_loop(State2)
end;
{sync_trans_serial, Tid} ->
%% Do the Lamport thing here
mnesia_recover:sync_trans_tid_serial(Tid),
doit_loop(State);
{From, info} ->
reply(From, {info, gb_trees:values(Participants),
gb_trees:to_list(Coordinators)}, State);
{mnesia_down, N} ->
verbose("Got mnesia_down from ~p, reconfiguring...~n", [N]),
reconfigure_coordinators(N, gb_trees:to_list(Coordinators)),
Tids = gb_trees:keys(Participants),
reconfigure_participants(N, gb_trees:values(Participants)),
NewState = clear_fixtable(N, State),
mnesia_locker:mnesia_down(N, Tids),
mnesia_monitor:mnesia_down(?MODULE, N),
doit_loop(NewState);
{From, {unblock_me, Tab}} ->
case lists:member(Tab, State#state.blocked_tabs) of
false ->
verbose("Wrong dirty Op blocked on ~p ~tp ~p",
[node(), Tab, From]),
reply(From, unblocked),
doit_loop(State);
true ->
Item = {Tab, unblock_me, From},
State2 = State#state{dirty_queue = [Item | State#state.dirty_queue]},
doit_loop(State2)
end;
{From, {block_tab, Tab}} ->
State2 = State#state{blocked_tabs = [Tab | State#state.blocked_tabs]},
reply(From, ok, State2);
{From, {unblock_tab, Tab}} ->
BlockedTabs2 = State#state.blocked_tabs -- [Tab],
case lists:member(Tab, BlockedTabs2) of
false ->
mnesia_controller:unblock_table(Tab),
Queue = process_dirty_queue(Tab, State#state.dirty_queue),
State2 = State#state{blocked_tabs = BlockedTabs2,
dirty_queue = Queue},
reply(From, ok, State2);
true ->
State2 = State#state{blocked_tabs = BlockedTabs2},
reply(From, ok, State2)
end;
{From, {prepare_checkpoint, Cp}} ->
Res = mnesia_checkpoint:tm_prepare(Cp),
case Res of
{ok, _Name, IgnoreNew, _Node} ->
prepare_pending_coordinators(gb_trees:to_list(Coordinators), IgnoreNew),
prepare_pending_participants(gb_trees:values(Participants), IgnoreNew);
{error, _Reason} ->
ignore
end,
reply(From, Res, State);
{From, {fixtable, [Tab,Lock,Requester]}} ->
case ?catch_val({Tab, storage_type}) of
{'EXIT', _} ->
reply(From, error, State);
Storage ->
mnesia_lib:db_fixtable(Storage,Tab,Lock),
NewState = manage_fixtable(Tab,Lock,Requester,State),
reply(From, node(), NewState)
end;
{system, From, Msg} ->
dbg_out("~p got {system, ~p, ~tp}~n", [?MODULE, From, Msg]),
sys:handle_system_msg(Msg, From, Sup, ?MODULE, [], State);
Msg ->
verbose("** ERROR ** ~p got unexpected message: ~tp~n", [?MODULE, Msg]),
doit_loop(State)
end.
do_sync_dirty(From, Tid, Commit, _Tab) ->
?eval_debug_fun({?MODULE, sync_dirty, pre}, [{tid, Tid}]),
Res = do_dirty(Tid, Commit),
?eval_debug_fun({?MODULE, sync_dirty, post}, [{tid, Tid}]),
From ! {?MODULE, node(), {dirty_res, Res}}.
do_async_dirty(Tid, Commit, _Tab) ->
?eval_debug_fun({?MODULE, async_dirty, pre}, [{tid, Tid}]),
do_dirty(Tid, Commit),
?eval_debug_fun({?MODULE, async_dirty, post}, [{tid, Tid}]).
%% Process items in fifo order
process_dirty_queue(Tab, [Item | Queue]) ->
Queue2 = process_dirty_queue(Tab, Queue),
case Item of
{async_dirty, Tid, Commit, Tab} ->
do_async_dirty(Tid, Commit, Tab),
Queue2;
{sync_dirty, From, Tid, Commit, Tab} ->
do_sync_dirty(From, Tid, Commit, Tab),
Queue2;
{Tab, unblock_me, From} ->
reply(From, unblocked),
Queue2;
_ ->
[Item | Queue2]
end;
process_dirty_queue(_Tab, []) ->
[].
prepare_pending_coordinators([{Tid, [Store | _Etabs]} | Coords], IgnoreNew) ->
try ?ets_lookup(Store, pending) of
[] ->
prepare_pending_coordinators(Coords, IgnoreNew);
[Pending] ->
case lists:member(Tid, IgnoreNew) of
false ->
mnesia_checkpoint:tm_enter_pending(Pending);
true ->
ignore
end,
prepare_pending_coordinators(Coords, IgnoreNew)
catch error:_ ->
prepare_pending_coordinators(Coords, IgnoreNew)
end;
prepare_pending_coordinators([], _IgnoreNew) ->
ok.
prepare_pending_participants([Part | Parts], IgnoreNew) ->
Tid = Part#participant.tid,
D = Part#participant.disc_nodes,
R = Part#participant.ram_nodes,
case lists:member(Tid, IgnoreNew) of
false ->
mnesia_checkpoint:tm_enter_pending(Tid, D, R);
true ->
ignore
end,
prepare_pending_participants(Parts, IgnoreNew);
prepare_pending_participants([], _IgnoreNew) ->
ok.
handle_exit(Pid, _Reason, State) when node(Pid) /= node() ->
%% We got exit from a remote fool
doit_loop(State);
handle_exit(Pid, _Reason, State) when Pid == State#state.supervisor ->
%% Our supervisor has died, time to stop
do_stop(State);
handle_exit(Pid, Reason, State) ->
%% Check if it is a coordinator
case pid_search_delete(Pid, gb_trees:to_list(State#state.coordinators)) of
{none, _} ->
%% Check if it is a participant
Ps = gb_trees:values(State#state.participants),
case mnesia_lib:key_search_delete(Pid,#participant.pid,Ps) of
{none, _} ->
%% We got exit from a local fool
doit_loop(State);
{P = #participant{}, _RestP} ->
fatal("Participant ~p in transaction ~p died ~tp~n",
[P#participant.pid, P#participant.tid, Reason]),
NewPs = gb_trees:delete(P#participant.tid,State#state.participants),
doit_loop(State#state{participants = NewPs})
end;
{{Tid, Etabs}, RestC} ->
%% A local coordinator has died and
%% we must determine the outcome of the
%% transaction and tell mnesia_tm on the
%% other nodes about it and then recover
%% locally.
recover_coordinator(Tid, Etabs),
doit_loop(State#state{coordinators = RestC})
end.
recover_coordinator(Tid, Etabs) ->
verbose("Coordinator ~p in transaction ~p died.~n", [Tid#tid.pid, Tid]),
Store = hd(Etabs),
CheckNodes = get_elements(nodes,Store),
TellNodes = CheckNodes -- [node()],
try arrange(Tid, Store, async) of
{_N, Prep} ->
%% Tell the participants about the outcome
Protocol = Prep#prep.protocol,
Outcome = tell_outcome(Tid, Protocol, node(), CheckNodes, TellNodes),
%% Recover locally
CR = Prep#prep.records,
{DiscNs, RamNs} = commit_nodes(CR, [], []),
case lists:keysearch(node(), #commit.node, CR) of
{value, Local} ->
?eval_debug_fun({?MODULE, recover_coordinator, pre},
[{tid, Tid}, {outcome, Outcome}, {prot, Protocol}]),
recover_coordinator(Tid, Protocol, Outcome, Local, DiscNs, RamNs),
?eval_debug_fun({?MODULE, recover_coordinator, post},
[{tid, Tid}, {outcome, Outcome}, {prot, Protocol}]);
false -> %% When killed before store havn't been copied to
ok %% to the new nested trans store.
end
catch _:Reason:Stacktrace ->
dbg_out("Recovery of coordinator ~p failed: ~tp~n",
[Tid, {Reason, Stacktrace}]),
Protocol = asym_trans,
tell_outcome(Tid, Protocol, node(), CheckNodes, TellNodes)
end,
erase_ets_tabs(Etabs),
transaction_terminated(Tid),
mnesia_locker:release_tid(Tid).
recover_coordinator(Tid, sym_trans, committed, Local, _, _) ->
mnesia_recover:note_decision(Tid, committed),
do_dirty(Tid, Local);
recover_coordinator(Tid, sym_trans, aborted, _Local, _, _) ->
mnesia_recover:note_decision(Tid, aborted);
recover_coordinator(Tid, sync_sym_trans, committed, Local, _, _) ->
mnesia_recover:note_decision(Tid, committed),
do_dirty(Tid, Local);
recover_coordinator(Tid, sync_sym_trans, aborted, _Local, _, _) ->
mnesia_recover:note_decision(Tid, aborted);
recover_coordinator(Tid, asym_trans, committed, Local, DiscNs, RamNs) ->
D = #decision{tid = Tid, outcome = committed,
disc_nodes = DiscNs, ram_nodes = RamNs},
mnesia_recover:log_decision(D),
do_commit(Tid, Local);
recover_coordinator(Tid, asym_trans, aborted, Local, DiscNs, RamNs) ->
D = #decision{tid = Tid, outcome = aborted,
disc_nodes = DiscNs, ram_nodes = RamNs},
mnesia_recover:log_decision(D),
do_abort(Tid, Local).
restore_stores(Coords, Tid, Store) ->
Etstabs = gb_trees:get(Tid,Coords),
Remaining = lists:delete(Store, Etstabs),
erase_ets_tabs(Remaining),
gb_trees:update(Tid,[Store],Coords).
add_coord_store(Coords, Tid, Etab) ->
Stores = gb_trees:get(Tid, Coords),
gb_trees:update(Tid, [Etab|Stores], Coords).
del_coord_store(Coords, Tid, Current, Obsolete) ->
Stores = gb_trees:get(Tid, Coords),
Rest =
case Stores of
[Obsolete, Current | Tail] -> Tail;
[Current, Obsolete | Tail] -> Tail
end,
?ets_delete_table(Obsolete),
gb_trees:update(Tid, [Current|Rest], Coords).
erase_ets_tabs([H | T]) ->
?ets_delete_table(H),
erase_ets_tabs(T);
erase_ets_tabs([]) ->
ok.
%% Clear one transactions all fixtables
clear_fixtable([Store|_]) ->
Fixed = get_elements(fixtable, Store),
lists:foreach(fun({Tab,Node}) ->
rpc:cast(Node, ?MODULE, fixtable, [Tab,false,self()])
end, Fixed).
%% Clear all fixtable Node have done
clear_fixtable(Node, State=#state{fixed_tabs = FT0}) ->
case mnesia_lib:key_search_delete(Node, 1, FT0) of
{none, _Ft} ->
State;
{{Node,Tabs},FT} ->
lists:foreach(
fun(Tab) ->
case ?catch_val({Tab, storage_type}) of
{'EXIT', _} ->
ignore;
Storage ->
mnesia_lib:db_fixtable(Storage,Tab,false)
end
end, Tabs),
State#state{fixed_tabs=FT}
end.
manage_fixtable(Tab,true,Requester,State=#state{fixed_tabs = FT0}) ->
Node = node(Requester),
case mnesia_lib:key_search_delete(Node, 1, FT0) of
{none, FT}->
State#state{fixed_tabs=[{Node, [Tab]}|FT]};
{{Node,Tabs},FT} ->
State#state{fixed_tabs=[{Node, [Tab|Tabs]}|FT]}
end;
manage_fixtable(Tab,false,Requester,State = #state{fixed_tabs = FT0}) ->
Node = node(Requester),
case mnesia_lib:key_search_delete(Node, 1, FT0) of
{none,_FT} -> State; % Hmm? Safeguard
{{Node, Tabs0},FT} ->
case lists:delete(Tab, Tabs0) of
[] -> State#state{fixed_tabs=FT};
Tabs -> State#state{fixed_tabs=[{Node,Tabs}|FT]}
end
end.
%% Deletes a pid from a list of participants
%% or from a gb_trees of coordinators
%% {none, All} or {Tr, Rest}
pid_search_delete(Pid, Trs) ->
pid_search_delete(Pid, Trs, none, []).
pid_search_delete(Pid, [Tr = {Tid, _Ts} | Trs], _Val, Ack) when Tid#tid.pid == Pid ->
pid_search_delete(Pid, Trs, Tr, Ack);
pid_search_delete(Pid, [Tr | Trs], Val, Ack) ->
pid_search_delete(Pid, Trs, Val, [Tr | Ack]);
pid_search_delete(_Pid, [], Val, Ack) ->
{Val, gb_trees:from_orddict(lists:reverse(Ack))}.
transaction_terminated(Tid) ->
mnesia_checkpoint:tm_exit_pending(Tid),
Pid = Tid#tid.pid,
if
node(Pid) == node() ->
unlink(Pid);
true -> %% Do the Lamport thing here
mnesia_recover:sync_trans_tid_serial(Tid)
end.
%% If there are an surrounding transaction, we inherit it's context
non_transaction(OldState={_,_,Trans}, Fun, Args, ActivityKind, Mod)
when Trans /= non_transaction ->
Kind = case ActivityKind of
sync_dirty -> sync;
_ -> async
end,
case transaction(OldState, Fun, Args, infinity, Mod, Kind) of
{atomic, Res} -> Res;
{aborted,Res} -> exit(Res)
end;
non_transaction(OldState, Fun, Args, ActivityKind, Mod) ->
Id = {ActivityKind, self()},
NewState = {Mod, Id, non_transaction},
put(mnesia_activity_state, NewState),
try apply(Fun, Args) of
{'EXIT', Reason} -> exit(Reason);
{aborted, Reason} -> mnesia:abort(Reason);
Res -> Res
catch
throw:Throw -> throw(Throw);
error:Reason:ST -> exit({Reason, ST});
exit:Reason -> exit(Reason)
after
case OldState of
undefined -> erase(mnesia_activity_state);
_ -> put(mnesia_activity_state, OldState)
end
end.
transaction(OldTidTs, Fun, Args, Retries, Mod, Type) ->
Factor = 1,
case OldTidTs of
undefined -> % Outer
execute_outer(Mod, Fun, Args, Factor, Retries, Type);
{_, _, non_transaction} -> % Transaction inside ?sync_dirty
Res = execute_outer(Mod, Fun, Args, Factor, Retries, Type),
put(mnesia_activity_state, OldTidTs),
Res;
{OldMod, Tid, Ts} -> % Nested
execute_inner(Mod, Tid, OldMod, Ts, Fun, Args, Factor, Retries, Type);
_ -> % Bad nesting
{aborted, nested_transaction}
end.
execute_outer(Mod, Fun, Args, Factor, Retries, Type) ->
case req(start_outer) of
{error, Reason} ->
{aborted, Reason};
{new_tid, Tid, Store} ->
Ts = #tidstore{store = Store},
NewTidTs = {Mod, Tid, Ts},
put(mnesia_activity_state, NewTidTs),
execute_transaction(Fun, Args, Factor, Retries, Type)
end.
execute_inner(Mod, Tid, OldMod, Ts, Fun, Args, Factor, Retries, Type) ->
case req({add_store, Tid}) of
{error, Reason} ->
{aborted, Reason};
{new_store, Ets} ->
copy_ets(Ts#tidstore.store, Ets),
Up = [{OldMod,Ts#tidstore.store} | Ts#tidstore.up_stores],
NewTs = Ts#tidstore{level = 1 + Ts#tidstore.level,
store = Ets,
up_stores = Up},
NewTidTs = {Mod, Tid, NewTs},
put(mnesia_activity_state, NewTidTs),
execute_transaction(Fun, Args, Factor, Retries, Type)
end.
copy_ets(From, To) ->
do_copy_ets(?ets_first(From), From, To).
do_copy_ets('$end_of_table', _,_) ->
ok;
do_copy_ets(K, From, To) ->
Objs = ?ets_lookup(From, K),
insert_objs(Objs, To),
do_copy_ets(?ets_next(From, K), From, To).
insert_objs([H|T], Tab) ->
?ets_insert(Tab, H),
insert_objs(T, Tab);
insert_objs([], _Tab) ->
ok.
execute_transaction(Fun, Args, Factor, Retries, Type) ->
try apply_fun(Fun, Args, Type) of
{atomic, Value} ->
mnesia_lib:incr_counter(trans_commits),
erase(mnesia_activity_state),
%% no need to clear locks, already done by commit ...
%% Flush any un processed mnesia_down messages we might have
flush_downs(),
?SAFE(unlink(whereis(?MODULE))),
{atomic, Value};
{do_abort, Reason} ->
check_exit(Fun, Args, Factor, Retries, {aborted, Reason}, Type);
{nested_atomic, Value} ->
mnesia_lib:incr_counter(trans_commits),
{atomic, Value}
catch throw:Value -> %% User called throw
Reason = {aborted, {throw, Value}},
return_abort(Fun, Args, Reason);
error:Reason:ST ->
check_exit(Fun, Args, Factor, Retries, {Reason,ST}, Type);
_:Reason ->
check_exit(Fun, Args, Factor, Retries, Reason, Type)
end.
apply_fun(Fun, Args, Type) ->
Result = apply(Fun, Args),
case t_commit(Type) of
do_commit ->
{atomic, Result};
do_commit_nested ->
{nested_atomic, Result};
{do_abort, {aborted, Reason}} ->
{do_abort, Reason};
{do_abort, _} = Abort ->
Abort
end.
check_exit(Fun, Args, Factor, Retries, Reason, Type) ->
case Reason of
{aborted, C = #cyclic{}} ->
maybe_restart(Fun, Args, Factor, Retries, Type, C);
{aborted, {node_not_running, N}} ->
maybe_restart(Fun, Args, Factor, Retries, Type, {node_not_running, N});
{aborted, {bad_commit, N}} ->
maybe_restart(Fun, Args, Factor, Retries, Type, {bad_commit, N});
_ ->
return_abort(Fun, Args, Reason)
end.
maybe_restart(Fun, Args, Factor, Retries, Type, Why) ->
{Mod, Tid, Ts} = get(mnesia_activity_state),
case try_again(Retries) of
yes when Ts#tidstore.level == 1 ->
restart(Mod, Tid, Ts, Fun, Args, Factor, Retries, Type, Why);
yes ->
return_abort(Fun, Args, Why);
no ->
return_abort(Fun, Args, {aborted, nomore})
end.
try_again(infinity) -> yes;
try_again(X) when is_number(X) , X > 1 -> yes;
try_again(_) -> no.
%% We can only restart toplevel transactions.
%% If a deadlock situation occurs in a nested transaction
%% The whole thing including all nested transactions need to be
%% restarted. The stack is thus popped by a consequtive series of
%% exit({aborted, #cyclic{}}) calls
restart(Mod, Tid, Ts, Fun, Args, Factor0, Retries0, Type, Why) ->
mnesia_lib:incr_counter(trans_restarts),
Retries = decr(Retries0),
case Why of
{bad_commit, _N} ->
return_abort(Fun, Args, Why),
Factor = 1,
SleepTime = mnesia_lib:random_time(Factor, Tid#tid.counter),
dbg_out("Restarting transaction ~w: in ~wms ~w~n", [Tid, SleepTime, Why]),
timer:sleep(SleepTime),
execute_outer(Mod, Fun, Args, Factor, Retries, Type);
{node_not_running, _N} -> %% Avoids hanging in receive_release_tid_ack
return_abort(Fun, Args, Why),
Factor = 1,
SleepTime = mnesia_lib:random_time(Factor, Tid#tid.counter),
dbg_out("Restarting transaction ~w: in ~wms ~w~n", [Tid, SleepTime, Why]),
timer:sleep(SleepTime),
execute_outer(Mod, Fun, Args, Factor, Retries, Type);
_ ->
SleepTime = mnesia_lib:random_time(Factor0, Tid#tid.counter),
dbg_out("Restarting transaction ~w: in ~wms ~w~n", [Tid, SleepTime, Why]),
if
Factor0 /= 10 ->
ignore;
true ->
%% Our serial may be much larger than other nodes ditto
AllNodes = val({current, db_nodes}),
verbose("Sync serial ~p~n", [Tid]),
rpc:abcast(AllNodes, ?MODULE, {sync_trans_serial, Tid})
end,
intercept_friends(Tid, Ts),
Store = Ts#tidstore.store,
Nodes = get_elements(nodes,Store),
?MODULE ! {self(), {restart, Tid, Store}},
mnesia_locker:send_release_tid(Nodes, Tid),
timer:sleep(SleepTime),
mnesia_locker:receive_release_tid_acc(Nodes, Tid),
case get_restarted(Tid) of
{restarted, Tid} ->
execute_transaction(Fun, Args, Factor0 + 1,
Retries, Type);
{error, Reason} ->
mnesia:abort(Reason)
end
end.
get_restarted(Tid) ->
case Res = rec() of
{restarted, Tid} ->
Res;
{error,_} ->
Res;
_ -> %% We could get a couple of aborts to many.
get_restarted(Tid)
end.
decr(infinity) -> infinity;
decr(X) when is_integer(X), X > 1 -> X - 1;
decr(_X) -> 0.
return_abort(Fun, Args, Reason) ->
{_Mod, Tid, Ts} = get(mnesia_activity_state),
dbg_out("Transaction ~p calling ~tp with ~tp failed: ~n ~tp~n",
[Tid, Fun, Args, Reason]),
OldStore = Ts#tidstore.store,
Nodes = get_elements(nodes, OldStore),
intercept_friends(Tid, Ts),
?SAFE(mnesia_lib:incr_counter(trans_failures)),
Level = Ts#tidstore.level,
if
Level == 1 ->
mnesia_locker:async_release_tid(Nodes, Tid),
?SAFE(?MODULE ! {delete_transaction, Tid}),
erase(mnesia_activity_state),
flush_downs(),
?SAFE(unlink(whereis(?MODULE))),
{aborted, mnesia_lib:fix_error(Reason)};
true ->
%% Nested transaction
[{OldMod,NewStore} | Tail] = Ts#tidstore.up_stores,
req({del_store, Tid, NewStore, OldStore, true}),
Ts2 = Ts#tidstore{store = NewStore,
up_stores = Tail,
level = Level - 1},
NewTidTs = {OldMod, Tid, Ts2},
put(mnesia_activity_state, NewTidTs),
case Reason of
#cyclic{} ->
exit({aborted, Reason});
{node_not_running, _N} ->
exit({aborted, Reason});
{bad_commit, _N}->
exit({aborted, Reason});
_ ->
{aborted, mnesia_lib:fix_error(Reason)}
end
end.
flush_downs() ->
receive
{?MODULE, _, _} -> flush_downs(); % Votes
{mnesia_down, _} -> flush_downs()
after 0 -> flushed
end.
put_activity_id(MTT) ->
put_activity_id(MTT, undefined).
put_activity_id(undefined,_) ->
erase_activity_id();
put_activity_id({Mod, Tid = #tid{}, Ts = #tidstore{}},Fun) ->
flush_downs(),
Store = Ts#tidstore.store,
if
is_function(Fun) ->
?ets_insert(Store, {friends, {stop,Fun}});
true ->
?ets_insert(Store, {friends, self()})
end,
NewTidTs = {Mod, Tid, Ts},
put(mnesia_activity_state, NewTidTs);
put_activity_id(SimpleState,_) ->
put(mnesia_activity_state, SimpleState).
erase_activity_id() ->
flush_downs(),
erase(mnesia_activity_state).
get_elements(Type,Store) ->
try ?ets_lookup(Store, Type) of
[] -> [];
[{_,Val}] -> [Val];
Vals -> [Val|| {_,Val} <- Vals]
catch error:_ -> []
end.
opt_propagate_store(_Current, _Obsolete, false) ->
ok;
opt_propagate_store(Current, Obsolete, true) ->
propagate_store(Current, nodes, get_elements(nodes,Obsolete)),
propagate_store(Current, fixtable, get_elements(fixtable,Obsolete)),
propagate_store(Current, friends, get_elements(friends, Obsolete)).
propagate_store(Store, Var, [Val | Vals]) ->
?ets_insert(Store, {Var, Val}),
propagate_store(Store, Var, Vals);
propagate_store(_Store, _Var, []) ->
ok.
%% Tell all processes that are cooperating with the current transaction
intercept_friends(_Tid, Ts) ->
Friends = get_elements(friends,Ts#tidstore.store),
intercept_best_friend(Friends, false).
intercept_best_friend([],_) -> ok;
intercept_best_friend([{stop,Fun} | R],Ignore) ->
?CATCH(Fun()),
intercept_best_friend(R,Ignore);
intercept_best_friend([Pid | R],false) ->
Pid ! {activity_ended, undefined, self()},
wait_for_best_friend(Pid, 0),
intercept_best_friend(R,true);
intercept_best_friend([_|R],true) ->
intercept_best_friend(R,true).
wait_for_best_friend(Pid, Timeout) ->
receive
{'EXIT', Pid, _} -> ok;
{activity_ended, _, Pid} -> ok
after Timeout ->
case erlang:is_process_alive(Pid) of
true -> wait_for_best_friend(Pid, 1000);
false -> ok
end
end.
dirty(Protocol, Item) ->
{{Tab, Key}, _Val, _Op} = Item,
Tid = {dirty, self()},
Prep = prepare_items(Tid, Tab, Key, [Item], #prep{protocol= Protocol}),
CR = Prep#prep.records,
case Protocol of
async_dirty ->
%% Send commit records to the other involved nodes,
%% but do only wait for one node to complete.
%% Preferrably, the local node if possible.
ReadNode = val({Tab, where_to_read}),
{WaitFor, FirstRes} = async_send_dirty(Tid, CR, Tab, ReadNode),
rec_dirty(WaitFor, FirstRes);
sync_dirty ->
%% Send commit records to the other involved nodes,
%% and wait for all nodes to complete
{WaitFor, FirstRes} = sync_send_dirty(Tid, CR, Tab, []),
rec_dirty(WaitFor, FirstRes);
_ ->
mnesia:abort({bad_activity, Protocol})
end.
%% This is the commit function, The first thing it does,
%% is to find out which nodes that have been participating
%% in this particular transaction, all of the mnesia_locker:lock*
%% functions insert the names of the nodes where it aquires locks
%% into the local shadow Store
%% This function exacutes in the context of the user process
t_commit(Type) ->
{_Mod, Tid, Ts} = get(mnesia_activity_state),
Store = Ts#tidstore.store,
if
Ts#tidstore.level == 1 ->
intercept_friends(Tid, Ts),
%% N is number of updates
case arrange(Tid, Store, Type) of
{N, Prep} when N > 0 ->
multi_commit(Prep#prep.protocol,
majority_attr(Prep),
Tid, Prep#prep.records, Store);
{0, Prep} ->
multi_commit(read_only,
majority_attr(Prep),
Tid, Prep#prep.records, Store)
end;
true ->
%% nested commit
Level = Ts#tidstore.level,
[{OldMod,Obsolete} | Tail] = Ts#tidstore.up_stores,
req({del_store, Tid, Store, Obsolete, false}),
NewTs = Ts#tidstore{store = Store,
up_stores = Tail,
level = Level - 1},
NewTidTs = {OldMod, Tid, NewTs},
put(mnesia_activity_state, NewTidTs),
do_commit_nested
end.
majority_attr(#prep{majority = M}) ->
M.
%% This function arranges for all objects we shall write in S to be
%% in a list of {Node, CommitRecord}
%% Important function for the performance of mnesia.
arrange(Tid, Store, Type) ->
%% The local node is always included
Nodes = get_elements(nodes,Store),
Recs = prep_recs(Nodes, []),
Key = ?ets_first(Store),
N = 0,
Prep =
case Type of
async -> #prep{protocol = sym_trans, records = Recs};
sync -> #prep{protocol = sync_sym_trans, records = Recs}
end,
{New, Prepared} = do_arrange(Tid, Store, Key, Prep, N),
{New, Prepared#prep{records = reverse(Prepared#prep.records)}}.
reverse([]) ->
[];
reverse([H=#commit{ram_copies=Ram, disc_copies=DC,
disc_only_copies=DOC, ext=Ext}
|R]) ->
[
H#commit{
ram_copies = lists:reverse(Ram),
disc_copies = lists:reverse(DC),
disc_only_copies = lists:reverse(DOC),
ext = [{Type, lists:reverse(E)} || {Type,E} <- Ext]
}
| reverse(R)].
prep_recs([N | Nodes], Recs) ->
prep_recs(Nodes, [#commit{decision = presume_commit, node = N} | Recs]);
prep_recs([], Recs) ->
Recs.
%% storage_types is a list of {Node, Storage} tuples
%% where each tuple represents an active replica
do_arrange(Tid, Store, {Tab, Key}, Prep, N) ->
Oid = {Tab, Key},
Items = ?ets_lookup(Store, Oid), %% Store is a bag
P2 = prepare_items(Tid, Tab, Key, Items, Prep),
do_arrange(Tid, Store, ?ets_next(Store, Oid), P2, N + 1);
do_arrange(Tid, Store, SchemaKey, Prep, N) when SchemaKey == op ->
Items = ?ets_lookup(Store, SchemaKey), %% Store is a bag
P2 = prepare_schema_items(Tid, Items, Prep),
do_arrange(Tid, Store, ?ets_next(Store, SchemaKey), P2, N + 1);
do_arrange(Tid, Store, RestoreKey, Prep, N) when RestoreKey == restore_op ->
[{restore_op, R}] = ?ets_lookup(Store, RestoreKey),
Fun = fun({Tab, Key}, CommitRecs, _RecName, Where, Snmp) ->
Item = [{{Tab, Key}, {Tab, Key}, delete}],
do_prepare_items(Tid, Tab, Key, Where, Snmp, Item, CommitRecs);
(BupRec, CommitRecs, RecName, Where, Snmp) ->
Tab = element(1, BupRec),
Key = element(2, BupRec),
Item =
if
Tab == RecName ->
[{{Tab, Key}, BupRec, write}];
true ->
BupRec2 = setelement(1, BupRec, RecName),
[{{Tab, Key}, BupRec2, write}]
end,
do_prepare_items(Tid, Tab, Key, Where, Snmp, Item, CommitRecs)
end,
Recs2 = mnesia_schema:arrange_restore(R, Fun, Prep#prep.records),
P2 = Prep#prep{protocol = asym_trans, records = Recs2},
do_arrange(Tid, Store, ?ets_next(Store, RestoreKey), P2, N + 1);
do_arrange(_Tid, _Store, '$end_of_table', Prep, N) ->
{N, Prep};
do_arrange(Tid, Store, IgnoredKey, Prep, N) -> %% locks, nodes ... local atoms...
do_arrange(Tid, Store, ?ets_next(Store, IgnoredKey), Prep, N).
%% Returns a prep record with all items in reverse order
prepare_schema_items(Tid, Items, Prep) ->
Types = [{N, schema_ops} || N <- val({current, db_nodes})],
Recs = prepare_nodes(Tid, Types, Items, Prep#prep.records, schema),
Prep#prep{protocol = asym_trans, records = Recs}.
%% Returns a prep record with all items in reverse order
prepare_items(Tid, Tab, Key, Items, Prep) when Prep#prep.prev_tab == Tab ->
Types = Prep#prep.prev_types,
Snmp = Prep#prep.prev_snmp,
Recs = Prep#prep.records,
Recs2 = do_prepare_items(Tid, Tab, Key, Types, Snmp, Items, Recs),
Prep#prep{records = Recs2};
prepare_items(Tid, Tab, Key, Items, Prep) ->
Types = val({Tab, where_to_commit}),
case Types of
[] -> mnesia:abort({no_exists, Tab});
{blocked, _} ->
unblocked = req({unblock_me, Tab}),
prepare_items(Tid, Tab, Key, Items, Prep);
_ ->
Majority = needs_majority(Tab, Prep),
Snmp = val({Tab, snmp}),
Recs2 = do_prepare_items(Tid, Tab, Key, Types,
Snmp, Items, Prep#prep.records),
Prep2 = Prep#prep{records = Recs2, prev_tab = Tab,
majority = Majority,
prev_types = Types, prev_snmp = Snmp},
check_prep(Prep2, Types)
end.
do_prepare_items(Tid, Tab, Key, Types, Snmp, Items, Recs) ->
Recs2 = prepare_snmp(Tid, Tab, Key, Types, Snmp, Items, Recs), % May exit
prepare_nodes(Tid, Types, Items, Recs2, normal).
needs_majority(Tab, #prep{majority = M}) ->
case lists:keymember(Tab, 1, M) of
true ->
M;
false ->
case ?catch_val({Tab, majority}) of
{'EXIT', _} ->
M;
false ->
M;
true ->
CopyHolders = val({Tab, all_nodes}),
[{Tab, CopyHolders} | M]
end
end.
have_majority([], _) ->
ok;
have_majority([{Tab, AllNodes} | Rest], Nodes) ->
case mnesia_lib:have_majority(Tab, AllNodes, Nodes) of
true ->
have_majority(Rest, Nodes);
false ->
{error, Tab}
end.
prepare_snmp(Tab, Key, Items) ->
case val({Tab, snmp}) of
[] ->
[];
Ustruct when Key /= '_' ->
{_Oid, _Val, Op} = hd(Items),
%% Still making snmp oid (not used) because we want to catch errors here
%% And also it keeps backwards comp. with old nodes.
SnmpOid = mnesia_snmp_hook:key_to_oid(Tab, Key, Ustruct), % May exit
[{Op, Tab, Key, SnmpOid}];
_ ->
[{clear_table, Tab}]
end.
prepare_snmp(_Tid, _Tab, _Key, _Types, [], _Items, Recs) ->
Recs;
prepare_snmp(Tid, Tab, Key, Types, Us, Items, Recs) ->
if Key /= '_' ->
{_Oid, _Val, Op} = hd(Items),
SnmpOid = mnesia_snmp_hook:key_to_oid(Tab, Key, Us), % May exit
prepare_nodes(Tid, Types, [{Op, Tab, Key, SnmpOid}], Recs, snmp);
Key == '_' ->
prepare_nodes(Tid, Types, [{clear_table, Tab}], Recs, snmp)
end.
check_prep(#prep{majority = [], types = Types} = Prep, Types) ->
Prep;
check_prep(#prep{majority = M, types = undefined} = Prep, Types) ->
Protocol = if M == [] ->
Prep#prep.protocol;
true ->
asym_trans
end,
Prep#prep{protocol = Protocol, types = Types};
check_prep(Prep, _Types) ->
Prep#prep{protocol = asym_trans}.
%% Returns a list of commit records
prepare_nodes(Tid, [{Node, Storage} | Rest], Items, C, Kind) ->
{Rec, C2} = pick_node(Tid, Node, C, []),
Rec2 = prepare_node(Node, Storage, Items, Rec, Kind),
[Rec2 | prepare_nodes(Tid, Rest, Items, C2, Kind)];
prepare_nodes(_Tid, [], _Items, CommitRecords, _Kind) ->
CommitRecords.
pick_node(Tid, Node, [Rec | Rest], Done) ->
if
Rec#commit.node == Node ->
{Rec, Done ++ Rest};
true ->
pick_node(Tid, Node, Rest, [Rec | Done])
end;
pick_node({dirty,_}, Node, [], Done) ->
{#commit{decision = presume_commit, node = Node}, Done};
pick_node(_Tid, Node, [], _Done) ->
mnesia:abort({bad_commit, {missing_lock, Node}}).
prepare_node(Node, Storage, [Item | Items], #commit{ext=Ext0}=Rec, Kind) when Kind == snmp ->
Rec2 = case lists:keytake(snmp, 1, Ext0) of
false ->
Rec#commit{ext = [{snmp,[Item]}|Ext0]};
{_, {snmp,Snmp},Ext} ->
Rec#commit{ext = [{snmp,[Item|Snmp]}|Ext]}
end,
prepare_node(Node, Storage, Items, Rec2, Kind);
prepare_node(Node, Storage, [Item | Items], Rec, Kind) when Kind /= schema ->
Rec2 =
case Storage of
ram_copies ->
Rec#commit{ram_copies = [Item | Rec#commit.ram_copies]};
disc_copies ->
Rec#commit{disc_copies = [Item | Rec#commit.disc_copies]};
disc_only_copies ->
Rec#commit{disc_only_copies =
[Item | Rec#commit.disc_only_copies]};
{ext, Alias, Mod} ->
Ext0 = Rec#commit.ext,
case lists:keytake(ext_copies, 1, Ext0) of
false ->
Rec#commit{ext = [{ext_copies, [{{ext,Alias,Mod}, Item}]}|Ext0]};
{_,{_,EC},Ext} ->
Rec#commit{ext = [{ext_copies, [{{ext,Alias,Mod}, Item}|EC]}|Ext]}
end
end,
prepare_node(Node, Storage, Items, Rec2, Kind);
prepare_node(_Node, _Storage, Items, Rec, Kind)
when Kind == schema, Rec#commit.schema_ops == [] ->
Rec#commit{schema_ops = Items};
prepare_node(_Node, _Storage, [], Rec, _Kind) ->
Rec.
%% multi_commit((Protocol, Tid, CommitRecords, Store)
%% Local work is always performed in users process
multi_commit(read_only, _Maj = [], Tid, CR, _Store) ->
%% This featherweight commit protocol is used when no
%% updates has been performed in the transaction.
{DiscNs, RamNs} = commit_nodes(CR, [], []),
Msg = {Tid, simple_commit},
rpc:abcast(DiscNs -- [node()], ?MODULE, Msg),
rpc:abcast(RamNs -- [node()], ?MODULE, Msg),
mnesia_recover:note_decision(Tid, committed),
mnesia_locker:release_tid(Tid),
?MODULE ! {delete_transaction, Tid},
do_commit;
multi_commit(sym_trans, _Maj = [], Tid, CR, Store) ->
%% This lightweight commit protocol is used when all
%% the involved tables are replicated symetrically.
%% Their storage types must match on each node.
%%
%% 1 Ask the other involved nodes if they want to commit
%% All involved nodes votes yes if they are up
%% 2a Somebody has voted no
%% Tell all yes voters to do_abort
%% 2b Everybody has voted yes
%% Tell everybody to do_commit. I.e. that they should
%% prepare the commit, log the commit record and
%% perform the updates.
%%
%% The outcome is kept 3 minutes in the transient decision table.
%%
%% Recovery:
%% If somebody dies before the coordinator has
%% broadcasted do_commit, the transaction is aborted.
%%
%% If a participant dies, the table load algorithm
%% ensures that the contents of the involved tables
%% are picked from another node.
%%
%% If the coordinator dies, each participants checks
%% the outcome with all the others. If all are uncertain
%% about the outcome, the transaction is aborted. If
%% somebody knows the outcome the others will follow.
{DiscNs, RamNs} = commit_nodes(CR, [], []),
Pending = mnesia_checkpoint:tm_enter_pending(Tid, DiscNs, RamNs),
?ets_insert(Store, Pending),
{WaitFor, Local} = ask_commit(sym_trans, Tid, CR, DiscNs, RamNs),
{Outcome, []} = rec_all(WaitFor, Tid, do_commit, []),
?eval_debug_fun({?MODULE, multi_commit_sym},
[{tid, Tid}, {outcome, Outcome}]),
rpc:abcast(DiscNs -- [node()], ?MODULE, {Tid, Outcome}),
rpc:abcast(RamNs -- [node()], ?MODULE, {Tid, Outcome}),
case Outcome of
do_commit ->
mnesia_recover:note_decision(Tid, committed),
do_dirty(Tid, Local),
mnesia_locker:release_tid(Tid),
?MODULE ! {delete_transaction, Tid};
{do_abort, _Reason} ->
mnesia_recover:note_decision(Tid, aborted)
end,
?eval_debug_fun({?MODULE, multi_commit_sym, post},
[{tid, Tid}, {outcome, Outcome}]),
Outcome;
multi_commit(sync_sym_trans, _Maj = [], Tid, CR, Store) ->
%% This protocol is the same as sym_trans except that it
%% uses syncronized calls to disk_log and syncronized commits
%% when several nodes are involved.
{DiscNs, RamNs} = commit_nodes(CR, [], []),
Pending = mnesia_checkpoint:tm_enter_pending(Tid, DiscNs, RamNs),
?ets_insert(Store, Pending),
{WaitFor, Local} = ask_commit(sync_sym_trans, Tid, CR, DiscNs, RamNs),
{Outcome, []} = rec_all(WaitFor, Tid, do_commit, []),
?eval_debug_fun({?MODULE, multi_commit_sym_sync},
[{tid, Tid}, {outcome, Outcome}]),
[?ets_insert(Store, {waiting_for_commit_ack, Node}) || Node <- WaitFor],
rpc:abcast(DiscNs -- [node()], ?MODULE, {Tid, Outcome}),
rpc:abcast(RamNs -- [node()], ?MODULE, {Tid, Outcome}),
case Outcome of
do_commit ->
mnesia_recover:note_decision(Tid, committed),
mnesia_log:slog(Local),
do_commit(Tid, Local),
%% Just wait for completion result is ignore.
rec_all(WaitFor, Tid, ignore, []),
mnesia_locker:release_tid(Tid),
?MODULE ! {delete_transaction, Tid};
{do_abort, _Reason} ->
mnesia_recover:note_decision(Tid, aborted)
end,
?eval_debug_fun({?MODULE, multi_commit_sym, post},
[{tid, Tid}, {outcome, Outcome}]),
Outcome;
multi_commit(asym_trans, Majority, Tid, CR, Store) ->
%% This more expensive commit protocol is used when
%% table definitions are changed (schema transactions).
%% It is also used when the involved tables are
%% replicated asymetrically. If the storage type differs
%% on at least one node this protocol is used.
%%
%% 1 Ask the other involved nodes if they want to commit.
%% All involved nodes prepares the commit, logs a presume_abort
%% commit record and votes yes or no depending of the
%% outcome of the prepare. The preparation is also performed
%% by the coordinator.
%%
%% 2a Somebody has died or voted no
%% Tell all yes voters to do_abort
%% 2b Everybody has voted yes
%% Put a unclear marker in the log.
%% Tell the others to pre_commit. I.e. that they should
%% put a unclear marker in the log and reply
%% acc_pre_commit when they are done.
%%
%% 3a Somebody died
%% Tell the remaining participants to do_abort
%% 3b Everybody has replied acc_pre_commit
%% Tell everybody to committed. I.e that they should
%% put a committed marker in the log, perform the updates
%% and reply done_commit when they are done. The coordinator
%% must wait with putting his committed marker inte the log
%% until the committed has been sent to all the others.
%% Then he performs local commit before collecting replies.
%%
%% 4 Everybody has either died or replied done_commit
%% Return to the caller.
%%
%% Recovery:
%% If the coordinator dies, the participants (and
%% the coordinator when he starts again) must do
%% the following:
%%
%% If we have no unclear marker in the log we may
%% safely abort, since we know that nobody may have
%% decided to commit yet.
%%
%% If we have a committed marker in the log we may
%% safely commit since we know that everybody else
%% also will come to this conclusion.
%%
%% If we have a unclear marker but no committed
%% in the log we are uncertain about the real outcome
%% of the transaction and must ask the others before
%% we can decide what to do. If someone knows the
%% outcome we will do the same. If nobody knows, we
%% will wait for the remaining involved nodes to come
%% up. When all involved nodes are up and uncertain,
%% we decide to commit (first put a committed marker
%% in the log, then do the updates).
D = #decision{tid = Tid, outcome = presume_abort},
{D2, CR2} = commit_decision(D, CR, [], []),
DiscNs = D2#decision.disc_nodes,
RamNs = D2#decision.ram_nodes,
case have_majority(Majority, DiscNs ++ RamNs) of
ok -> ok;
{error, Tab} -> mnesia:abort({no_majority, Tab})
end,
Pending = mnesia_checkpoint:tm_enter_pending(Tid, DiscNs, RamNs),
?ets_insert(Store, Pending),
{WaitFor, Local} = ask_commit(asym_trans, Tid, CR2, DiscNs, RamNs),
SchemaPrep = ?CATCH(mnesia_schema:prepare_commit(Tid, Local, {coord, WaitFor})),
{Votes, Pids} = rec_all(WaitFor, Tid, do_commit, []),
?eval_debug_fun({?MODULE, multi_commit_asym_got_votes},
[{tid, Tid}, {votes, Votes}]),
case Votes of
do_commit ->
case SchemaPrep of
{_Modified, C = #commit{}, DumperMode} ->
mnesia_log:log(C), % C is not a binary
?eval_debug_fun({?MODULE, multi_commit_asym_log_commit_rec},
[{tid, Tid}]),
D3 = C#commit.decision,
D4 = D3#decision{outcome = unclear},
mnesia_recover:log_decision(D4),
?eval_debug_fun({?MODULE, multi_commit_asym_log_commit_dec},
[{tid, Tid}]),
tell_participants(Pids, {Tid, pre_commit}),
%% Now we are uncertain and we do not know
%% if all participants have logged that
%% they are uncertain or not
rec_acc_pre_commit(Pids, Tid, Store, {C,Local},
do_commit, DumperMode, [], []);
{'EXIT', Reason} ->
%% The others have logged the commit
%% record but they are not uncertain
mnesia_recover:note_decision(Tid, aborted),
?eval_debug_fun({?MODULE, multi_commit_asym_prepare_exit},
[{tid, Tid}]),
tell_participants(Pids, {Tid, {do_abort, Reason}}),
do_abort(Tid, Local),
{do_abort, Reason}
end;
{do_abort, Reason} ->
%% The others have logged the commit
%% record but they are not uncertain
mnesia_recover:note_decision(Tid, aborted),
?eval_debug_fun({?MODULE, multi_commit_asym_do_abort}, [{tid, Tid}]),
tell_participants(Pids, {Tid, {do_abort, Reason}}),
do_abort(Tid, Local),
{do_abort, Reason}
end.
%% Returns do_commit or {do_abort, Reason}
rec_acc_pre_commit([Pid | Tail], Tid, Store, Commit, Res, DumperMode,
GoodPids, SchemaAckPids) ->
receive
{?MODULE, _, {acc_pre_commit, Tid, Pid, true}} ->
rec_acc_pre_commit(Tail, Tid, Store, Commit, Res, DumperMode,
[Pid | GoodPids], [Pid | SchemaAckPids]);
{?MODULE, _, {acc_pre_commit, Tid, Pid, false}} ->
rec_acc_pre_commit(Tail, Tid, Store, Commit, Res, DumperMode,
[Pid | GoodPids], SchemaAckPids);
{?MODULE, _, {acc_pre_commit, Tid, Pid}} ->
%% Kept for backwards compatibility. Remove after Mnesia 4.x
rec_acc_pre_commit(Tail, Tid, Store, Commit, Res, DumperMode,
[Pid | GoodPids], [Pid | SchemaAckPids]);
{?MODULE, _, {do_abort, Tid, Pid, _Reason}} ->
AbortRes = {do_abort, {bad_commit, node(Pid)}},
rec_acc_pre_commit(Tail, Tid, Store, Commit, AbortRes, DumperMode,
GoodPids, SchemaAckPids);
{mnesia_down, Node} when Node == node(Pid) ->
AbortRes = {do_abort, {bad_commit, Node}},
?SAFE(Pid ! {Tid, AbortRes}), %% Tell him that he has died
rec_acc_pre_commit(Tail, Tid, Store, Commit, AbortRes, DumperMode,
GoodPids, SchemaAckPids)
end;
rec_acc_pre_commit([], Tid, Store, {Commit,OrigC}, Res, DumperMode, GoodPids, SchemaAckPids) ->
D = Commit#commit.decision,
case Res of
do_commit ->
%% Now everybody knows that the others
%% has voted yes. We also know that
%% everybody are uncertain.
prepare_sync_schema_commit(Store, SchemaAckPids),
tell_participants(GoodPids, {Tid, committed}),
D2 = D#decision{outcome = committed},
mnesia_recover:log_decision(D2),
?eval_debug_fun({?MODULE, rec_acc_pre_commit_log_commit},
[{tid, Tid}]),
%% Now we have safely logged committed
%% and we can recover without asking others
do_commit(Tid, Commit, DumperMode),
?eval_debug_fun({?MODULE, rec_acc_pre_commit_done_commit},
[{tid, Tid}]),
sync_schema_commit(Tid, Store, SchemaAckPids),
mnesia_locker:release_tid(Tid),
?MODULE ! {delete_transaction, Tid};
{do_abort, Reason} ->
tell_participants(GoodPids, {Tid, {do_abort, Reason}}),
D2 = D#decision{outcome = aborted},
mnesia_recover:log_decision(D2),
?eval_debug_fun({?MODULE, rec_acc_pre_commit_log_abort},
[{tid, Tid}]),
do_abort(Tid, OrigC),
?eval_debug_fun({?MODULE, rec_acc_pre_commit_done_abort},
[{tid, Tid}])
end,
Res.
%% Note all nodes in case of mnesia_down mgt
prepare_sync_schema_commit(_Store, []) ->
ok;
prepare_sync_schema_commit(Store, [Pid | Pids]) ->
?ets_insert(Store, {waiting_for_commit_ack, node(Pid)}),
prepare_sync_schema_commit(Store, Pids).
sync_schema_commit(_Tid, _Store, []) ->
ok;
sync_schema_commit(Tid, Store, [Pid | Tail]) ->
receive
{?MODULE, _, {schema_commit, Tid, Pid}} ->
?ets_match_delete(Store, {waiting_for_commit_ack, node(Pid)}),
sync_schema_commit(Tid, Store, Tail);
{mnesia_down, Node} when Node == node(Pid) ->
?ets_match_delete(Store, {waiting_for_commit_ack, Node}),
sync_schema_commit(Tid, Store, Tail)
end.
tell_participants([Pid | Pids], Msg) ->
Pid ! Msg,
tell_participants(Pids, Msg);
tell_participants([], _Msg) ->
ok.
-spec commit_participant(_, _, _, _, _) -> no_return().
%% Trap exit because we can get a shutdown from application manager
commit_participant(Coord, Tid, Bin, DiscNs, RamNs) when is_binary(Bin) ->
process_flag(trap_exit, true),
Commit = binary_to_term(Bin),
commit_participant(Coord, Tid, Bin, Commit, DiscNs, RamNs);
commit_participant(Coord, Tid, C = #commit{}, DiscNs, RamNs) ->
process_flag(trap_exit, true),
commit_participant(Coord, Tid, C, C, DiscNs, RamNs).
commit_participant(Coord, Tid, Bin, C0, DiscNs, _RamNs) ->
?eval_debug_fun({?MODULE, commit_participant, pre}, [{tid, Tid}]),
try mnesia_schema:prepare_commit(Tid, C0, {part, Coord}) of
{Modified, C = #commit{}, DumperMode} ->
%% If we cannot find any local unclear decision
%% we should presume abort at startup recovery
case lists:member(node(), DiscNs) of
false ->
ignore;
true ->
case Modified of
false -> mnesia_log:log(Bin);
true -> mnesia_log:log(C)
end
end,
?eval_debug_fun({?MODULE, commit_participant, vote_yes},
[{tid, Tid}]),
reply(Coord, {vote_yes, Tid, self()}),
receive
{Tid, pre_commit} ->
D = C#commit.decision,
mnesia_recover:log_decision(D#decision{outcome = unclear}),
?eval_debug_fun({?MODULE, commit_participant, pre_commit},
[{tid, Tid}]),
Expect_schema_ack = C#commit.schema_ops /= [],
reply(Coord, {acc_pre_commit, Tid, self(), Expect_schema_ack}),
%% Now we are vulnerable for failures, since
%% we cannot decide without asking others
receive
{Tid, committed} ->
mnesia_recover:log_decision(D#decision{outcome = committed}),
?eval_debug_fun({?MODULE, commit_participant, log_commit},
[{tid, Tid}]),
do_commit(Tid, C, DumperMode),
case Expect_schema_ack of
false -> ignore;
true -> reply(Coord, {schema_commit, Tid, self()})
end,
?eval_debug_fun({?MODULE, commit_participant, do_commit},
[{tid, Tid}]);
{Tid, {do_abort, _Reason}} ->
mnesia_recover:log_decision(D#decision{outcome = aborted}),
?eval_debug_fun({?MODULE, commit_participant, log_abort},
[{tid, Tid}]),
mnesia_schema:undo_prepare_commit(Tid, C0),
?eval_debug_fun({?MODULE, commit_participant, undo_prepare},
[{tid, Tid}]);
{'EXIT', _MnesiaTM, Reason} ->
reply(Coord, {do_abort, Tid, self(), {bad_commit,Reason}}),
mnesia_recover:log_decision(D#decision{outcome = aborted}),
mnesia_schema:undo_prepare_commit(Tid, C0);
Msg ->
verbose("** ERROR ** commit_participant ~p, got unexpected msg: ~tp~n",
[Tid, Msg])
end;
{Tid, {do_abort, Reason}} ->
reply(Coord, {do_abort, Tid, self(), Reason}),
mnesia_schema:undo_prepare_commit(Tid, C0),
?eval_debug_fun({?MODULE, commit_participant, pre_commit_undo_prepare},
[{tid, Tid}]);
{'EXIT', _, Reason} ->
reply(Coord, {do_abort, Tid, self(), {bad_commit,Reason}}),
mnesia_schema:undo_prepare_commit(Tid, C0),
?eval_debug_fun({?MODULE, commit_participant, pre_commit_undo_prepare}, [{tid, Tid}]);
Msg ->
reply(Coord, {do_abort, Tid, self(), {bad_commit,internal}}),
verbose("** ERROR ** commit_participant ~p, got unexpected msg: ~tp~n",
[Tid, Msg])
end
catch _:Reason ->
?eval_debug_fun({?MODULE, commit_participant, vote_no},
[{tid, Tid}]),
reply(Coord, {vote_no, Tid, Reason}),
mnesia_schema:undo_prepare_commit(Tid, C0)
end,
mnesia_locker:release_tid(Tid),
?MODULE ! {delete_transaction, Tid},
unlink(whereis(?MODULE)),
exit(normal).
do_abort(Tid, Bin) when is_binary(Bin) ->
%% Possible optimization:
%% If we want we could pass arround a flag
%% that tells us whether the binary contains
%% schema ops or not. Only if the binary
%% contains schema ops there are meningful
%% unpack the binary and perform
%% mnesia_schema:undo_prepare_commit/1.
do_abort(Tid, binary_to_term(Bin));
do_abort(Tid, Commit) ->
mnesia_schema:undo_prepare_commit(Tid, Commit),
Commit.
do_dirty(Tid, Commit) when Commit#commit.schema_ops == [] ->
mnesia_log:log(Commit),
do_commit(Tid, Commit).
%% do_commit(Tid, CommitRecord)
do_commit(Tid, Bin) when is_binary(Bin) ->
do_commit(Tid, binary_to_term(Bin));
do_commit(Tid, C) ->
do_commit(Tid, C, optional).
do_commit(Tid, Bin, DumperMode) when is_binary(Bin) ->
do_commit(Tid, binary_to_term(Bin), DumperMode);
do_commit(Tid, C, DumperMode) ->
mnesia_dumper:update(Tid, C#commit.schema_ops, DumperMode),
R = do_snmp(Tid, proplists:get_value(snmp, C#commit.ext, [])),
R2 = do_update(Tid, ram_copies, C#commit.ram_copies, R),
R3 = do_update(Tid, disc_copies, C#commit.disc_copies, R2),
R4 = do_update(Tid, disc_only_copies, C#commit.disc_only_copies, R3),
R5 = do_update_ext(Tid, C#commit.ext, R4),
mnesia_subscr:report_activity(Tid),
R5.
%% This could/should be optimized
do_update_ext(_Tid, [], OldRes) -> OldRes;
do_update_ext(Tid, Ext, OldRes) ->
case lists:keyfind(ext_copies, 1, Ext) of
false -> OldRes;
{_, Ops} ->
Do = fun({{ext, _,_} = Storage, Op}, R) ->
do_update(Tid, Storage, [Op], R)
end,
lists:foldl(Do, OldRes, Ops)
end.
%% Update the items
do_update(Tid, Storage, [Op | Ops], OldRes) ->
try do_update_op(Tid, Storage, Op) of
ok -> do_update(Tid, Storage, Ops, OldRes);
NewRes -> do_update(Tid, Storage, Ops, NewRes)
catch _:Reason:ST ->
%% This may only happen when we recently have
%% deleted our local replica, changed storage_type
%% or transformed table
%% BUGBUG: Updates may be lost if storage_type is changed.
%% Determine actual storage type and try again.
%% BUGBUG: Updates may be lost if table is transformed.
verbose("do_update in ~w failed: ~tp -> {'EXIT', ~tp}~n",
[Tid, Op, {Reason, ST}]),
do_update(Tid, Storage, Ops, OldRes)
end;
do_update(_Tid, _Storage, [], Res) ->
Res.
do_update_op(Tid, Storage, {{Tab, K}, Obj, write}) ->
commit_write(?catch_val({Tab, commit_work}), Tid, Storage,
Tab, K, Obj, undefined),
mnesia_lib:db_put(Storage, Tab, Obj);
do_update_op(Tid, Storage, {{Tab, K}, Val, delete}) ->
commit_delete(?catch_val({Tab, commit_work}), Tid, Storage, Tab, K, Val, undefined),
mnesia_lib:db_erase(Storage, Tab, K);
do_update_op(Tid, Storage, {{Tab, K}, {RecName, Incr}, update_counter}) ->
{NewObj, OldObjs} =
try
NewVal = mnesia_lib:db_update_counter(Storage, Tab, K, Incr),
true = is_integer(NewVal) andalso (NewVal >= 0),
{{RecName, K, NewVal}, [{RecName, K, NewVal - Incr}]}
catch error:_ when Incr > 0 ->
New = {RecName, K, Incr},
mnesia_lib:db_put(Storage, Tab, New),
{New, []};
error:_ ->
Zero = {RecName, K, 0},
mnesia_lib:db_put(Storage, Tab, Zero),
{Zero, []}
end,
commit_update(?catch_val({Tab, commit_work}), Tid, Storage, Tab,
K, NewObj, OldObjs),
element(3, NewObj);
do_update_op(Tid, Storage, {{Tab, Key}, Obj, delete_object}) ->
commit_del_object(?catch_val({Tab, commit_work}),
Tid, Storage, Tab, Key, Obj),
mnesia_lib:db_match_erase(Storage, Tab, Obj);
do_update_op(Tid, Storage, {{Tab, Key}, Obj, clear_table}) ->
commit_clear(?catch_val({Tab, commit_work}), Tid, Storage, Tab, Key, Obj),
mnesia_lib:db_match_erase(Storage, Tab, Obj).
commit_write([], _, _, _, _, _, _) -> ok;
commit_write([{checkpoints, CpList}|R], Tid, Storage, Tab, K, Obj, Old) ->
mnesia_checkpoint:tm_retain(Tid, Tab, K, write, CpList),
commit_write(R, Tid, Storage, Tab, K, Obj, Old);
commit_write([H|R], Tid, Storage, Tab, K, Obj, Old)
when element(1, H) == subscribers ->
mnesia_subscr:report_table_event(H, Tab, Tid, Obj, write, Old),
commit_write(R, Tid, Storage, Tab, K, Obj, Old);
commit_write([H|R], Tid, Storage, Tab, K, Obj, Old)
when element(1, H) == index ->
mnesia_index:add_index(H, Storage, Tab, K, Obj, Old),
commit_write(R, Tid, Storage, Tab, K, Obj, Old).
commit_update([], _, _, _, _, _, _) -> ok;
commit_update([{checkpoints, CpList}|R], Tid, Storage, Tab, K, Obj, _) ->
Old = mnesia_checkpoint:tm_retain(Tid, Tab, K, write, CpList),
commit_update(R, Tid, Storage, Tab, K, Obj, Old);
commit_update([H|R], Tid, Storage, Tab, K, Obj, Old)
when element(1, H) == subscribers ->
mnesia_subscr:report_table_event(H, Tab, Tid, Obj, write, Old),
commit_update(R, Tid, Storage, Tab, K, Obj, Old);
commit_update([H|R], Tid,Storage, Tab, K, Obj, Old)
when element(1, H) == index ->
mnesia_index:add_index(H, Storage, Tab, K, Obj, Old),
commit_update(R, Tid, Storage, Tab, K, Obj, Old).
commit_delete([], _, _, _, _, _, _) -> ok;
commit_delete([{checkpoints, CpList}|R], Tid, Storage, Tab, K, Obj, _) ->
Old = mnesia_checkpoint:tm_retain(Tid, Tab, K, delete, CpList),
commit_delete(R, Tid, Storage, Tab, K, Obj, Old);
commit_delete([H|R], Tid, Storage, Tab, K, Obj, Old)
when element(1, H) == subscribers ->
mnesia_subscr:report_table_event(H, Tab, Tid, Obj, delete, Old),
commit_delete(R, Tid, Storage, Tab, K, Obj, Old);
commit_delete([H|R], Tid, Storage, Tab, K, Obj, Old)
when element(1, H) == index ->
mnesia_index:delete_index(H, Storage, Tab, K),
commit_delete(R, Tid, Storage, Tab, K, Obj, Old).
commit_del_object([], _, _, _, _, _) -> ok;
commit_del_object([{checkpoints, CpList}|R], Tid, Storage, Tab, K, Obj) ->
mnesia_checkpoint:tm_retain(Tid, Tab, K, delete_object, CpList),
commit_del_object(R, Tid, Storage, Tab, K, Obj);
commit_del_object([H|R], Tid, Storage, Tab, K, Obj) when element(1, H) == subscribers ->
mnesia_subscr:report_table_event(H, Tab, Tid, Obj, delete_object),
commit_del_object(R, Tid, Storage, Tab, K, Obj);
commit_del_object([H|R], Tid, Storage, Tab, K, Obj) when element(1, H) == index ->
mnesia_index:del_object_index(H, Storage, Tab, K, Obj),
commit_del_object(R, Tid, Storage, Tab, K, Obj).
commit_clear([], _, _, _, _, _) -> ok;
commit_clear([{checkpoints, CpList}|R], Tid, Storage, Tab, K, Obj) ->
mnesia_checkpoint:tm_retain(Tid, Tab, K, clear_table, CpList),
commit_clear(R, Tid, Storage, Tab, K, Obj);
commit_clear([H|R], Tid, Storage, Tab, K, Obj)
when element(1, H) == subscribers ->
mnesia_subscr:report_table_event(H, Tab, Tid, Obj, clear_table, undefined),
commit_clear(R, Tid, Storage, Tab, K, Obj);
commit_clear([H|R], Tid, Storage, Tab, K, Obj)
when element(1, H) == index ->
mnesia_index:clear_index(H, Tab, K, Obj),
commit_clear(R, Tid, Storage, Tab, K, Obj).
do_snmp(_, []) -> ok;
do_snmp(Tid, [Head|Tail]) ->
try mnesia_snmp_hook:update(Head)
catch _:Reason:ST ->
%% This should only happen when we recently have
%% deleted our local replica or recently deattached
%% the snmp table
verbose("do_snmp in ~w failed: ~tp -> {'EXIT', ~tp}~n",
[Tid, Head, {Reason, ST}])
end,
do_snmp(Tid, Tail).
commit_nodes([C | Tail], AccD, AccR) ->
case C of
#commit{disc_copies=[], disc_only_copies=[], schema_ops=[], ext=Ext} ->
case lists:keyfind(ext_copies, 1, Ext) of
false -> commit_nodes(Tail, AccD, [C#commit.node | AccR]);
_ -> commit_nodes(Tail, [C#commit.node | AccD], AccR)
end;
_ ->
commit_nodes(Tail, [C#commit.node | AccD], AccR)
end;
commit_nodes([], AccD, AccR) ->
{AccD, AccR}.
commit_decision(D, [C | Tail], AccD, AccR) ->
N = C#commit.node,
{D2, Tail2} =
case C of
#commit{disc_copies=[], disc_only_copies=[], schema_ops=[], ext=Ext} ->
case lists:keyfind(ext_copies, 1, Ext) of
false -> commit_decision(D, Tail, AccD, [N | AccR]);
_ -> commit_decision(D, Tail, [N | AccD], AccR)
end;
#commit{schema_ops=[]} ->
commit_decision(D, Tail, [N | AccD], AccR);
#commit{schema_ops=Ops} ->
case ram_only_ops(N, Ops) of
true -> commit_decision(D, Tail, AccD, [N | AccR]);
false -> commit_decision(D, Tail, [N | AccD], AccR)
end
end,
{D2, [C#commit{decision = D2} | Tail2]};
commit_decision(D, [], AccD, AccR) ->
{D#decision{disc_nodes = AccD, ram_nodes = AccR}, []}.
ram_only_ops(N, [{op, change_table_copy_type, N, _FromS, _ToS, Cs} | _Ops ]) ->
case lists:member({name, schema}, Cs) of
true ->
%% We always use disk if change type of the schema
false;
false ->
not lists:member(N, val({schema, disc_copies}))
end;
ram_only_ops(N, _Ops) ->
not lists:member(N, val({schema, disc_copies})).
%% Returns {WaitFor, Res}
sync_send_dirty(Tid, [Head | Tail], Tab, WaitFor) ->
Node = Head#commit.node,
if
Node == node() ->
{WF, _} = sync_send_dirty(Tid, Tail, Tab, WaitFor),
Res = do_dirty(Tid, Head),
{WF, Res};
true ->
{?MODULE, Node} ! {self(), {sync_dirty, Tid, ext_format(Head), Tab}},
sync_send_dirty(Tid, Tail, Tab, [Node | WaitFor])
end;
sync_send_dirty(_Tid, [], _Tab, WaitFor) ->
{WaitFor, {'EXIT', {aborted, {node_not_running, WaitFor}}}}.
%% Returns {WaitFor, Res}
async_send_dirty(_Tid, _Nodes, Tab, nowhere) ->
{[], {'EXIT', {aborted, {no_exists, Tab}}}};
async_send_dirty(Tid, Nodes, Tab, ReadNode) ->
async_send_dirty(Tid, Nodes, Tab, ReadNode, [], ok).
async_send_dirty(Tid, [Head | Tail], Tab, ReadNode, WaitFor, Res) ->
Node = Head#commit.node,
if
ReadNode == Node, Node == node() ->
NewRes = do_dirty(Tid, Head),
async_send_dirty(Tid, Tail, Tab, ReadNode, WaitFor, NewRes);
ReadNode == Node ->
{?MODULE, Node} ! {self(), {sync_dirty, Tid, ext_format(Head), Tab}},
NewRes = {'EXIT', {aborted, {node_not_running, Node}}},
async_send_dirty(Tid, Tail, Tab, ReadNode, [Node | WaitFor], NewRes);
true ->
{?MODULE, Node} ! {self(), {async_dirty, Tid, ext_format(Head), Tab}},
async_send_dirty(Tid, Tail, Tab, ReadNode, WaitFor, Res)
end;
async_send_dirty(_Tid, [], _Tab, _ReadNode, WaitFor, Res) ->
{WaitFor, Res}.
rec_dirty([Node | Tail], Res) when Node /= node() ->
NewRes = get_dirty_reply(Node, Res),
rec_dirty(Tail, NewRes);
rec_dirty([], Res) ->
Res.
get_dirty_reply(Node, Res) ->
receive
{?MODULE, Node, {'EXIT', Reason}} ->
{'EXIT', {aborted, {badarg, Reason}}};
{?MODULE, Node, {dirty_res, ok}} ->
case Res of
{'EXIT', {aborted, {node_not_running, _Node}}} ->
ok;
_ ->
%% Prioritize bad results, but node_not_running
Res
end;
{?MODULE, Node, {dirty_res, Reply}} ->
Reply;
{mnesia_down, Node} ->
case get(mnesia_activity_state) of
{_, Tid, _Ts} when element(1,Tid) == tid ->
%% Hmm dirty called inside a transaction, to avoid
%% hanging transaction we need to restart the transaction
mnesia:abort({node_not_running, Node});
_ ->
%% It's ok to ignore mnesia_down's since we will make
%% the replicas consistent again when Node is started
Res
end
after 1000 ->
case lists:member(Node, val({current, db_nodes})) of
true ->
get_dirty_reply(Node, Res);
false ->
Res
end
end.
%% Assume that CommitRecord is no binary
%% Return {Res, Pids}
ask_commit(Protocol, Tid, CR, DiscNs, RamNs) ->
ask_commit(Protocol, Tid, CR, DiscNs, RamNs, [], no_local).
ask_commit(Protocol, Tid, [Head | Tail], DiscNs, RamNs, WaitFor, Local) ->
Node = Head#commit.node,
if
Node == node() ->
ask_commit(Protocol, Tid, Tail, DiscNs, RamNs, WaitFor, Head);
true ->
CR = ext_format(Head),
Msg = {ask_commit, Protocol, Tid, CR, DiscNs, RamNs},
{?MODULE, Node} ! {self(), Msg},
ask_commit(Protocol, Tid, Tail, DiscNs, RamNs, [Node | WaitFor], Local)
end;
ask_commit(_Protocol, _Tid, [], _DiscNs, _RamNs, WaitFor, Local) ->
{WaitFor, Local}.
ext_format(#commit{ext=[]}=CR) -> CR;
ext_format(#commit{node=Node, ext=Ext}=CR) ->
case mnesia_monitor:needs_protocol_conversion(Node) of
true ->
case lists:keyfind(snmp, 1, Ext) of
false -> CR#commit{ext=[]};
{snmp, List} -> CR#commit{ext=List}
end;
false -> CR
end.
new_cr_format(#commit{ext=[]}=Cr) -> Cr;
new_cr_format(#commit{ext=[{_,_}|_]}=Cr) -> Cr;
new_cr_format(#commit{ext=Snmp}=Cr) ->
Cr#commit{ext=[{snmp,Snmp}]}.
rec_all([Node | Tail], Tid, Res, Pids) ->
receive
{?MODULE, Node, {vote_yes, Tid}} ->
rec_all(Tail, Tid, Res, Pids);
{?MODULE, Node, {vote_yes, Tid, Pid}} ->
rec_all(Tail, Tid, Res, [Pid | Pids]);
{?MODULE, Node, {vote_no, Tid, Reason}} ->
rec_all(Tail, Tid, {do_abort, Reason}, Pids);
{?MODULE, Node, {committed, Tid}} ->
rec_all(Tail, Tid, Res, Pids);
{?MODULE, Node, {aborted, Tid}} ->
rec_all(Tail, Tid, Res, Pids);
{mnesia_down, Node} ->
%% Make sure that mnesia_tm knows it has died
%% it may have been restarted
Abort = {do_abort, {bad_commit, Node}},
?SAFE({?MODULE, Node} ! {Tid, Abort}),
rec_all(Tail, Tid, Abort, Pids)
end;
rec_all([], _Tid, Res, Pids) ->
{Res, Pids}.
get_transactions() ->
{info, Participant, Coordinator} = req(info),
lists:map(fun({Tid, _Tabs}) ->
Status = tr_status(Tid,Participant),
{Tid#tid.counter, Tid#tid.pid, Status}
end,Coordinator).
tr_status(Tid,Participant) ->
case lists:keymember(Tid, 1, Participant) of
true -> participant;
false -> coordinator
end.
get_info(Timeout) ->
case whereis(?MODULE) of
undefined ->
{timeout, Timeout};
Pid ->
Pid ! {self(), info},
receive
{?MODULE, _, {info, Part, Coord}} ->
{info, Part, Coord}
after Timeout ->
{timeout, Timeout}
end
end.
display_info(Stream, {timeout, T}) ->
io:format(Stream, "---> No info about coordinator and participant transactions, "
"timeout ~p <--- ~n", [T]);
display_info(Stream, {info, Part, Coord}) ->
io:format(Stream, "---> Participant transactions <--- ~n", []),
lists:foreach(fun(P) -> pr_participant(Stream, P) end, Part),
io:format(Stream, "---> Coordinator transactions <---~n", []),
lists:foreach(fun({Tid, _Tabs}) -> pr_tid(Stream, Tid) end, Coord).
pr_participant(Stream, P) ->
Commit0 = P#participant.commit,
Commit =
if
is_binary(Commit0) -> binary_to_term(Commit0);
true -> Commit0
end,
pr_tid(Stream, P#participant.tid),
io:format(Stream, "with participant objects ~tp~n", [Commit]).
pr_tid(Stream, Tid) ->
io:format(Stream, "Tid: ~p (owned by ~p) ~n",
[Tid#tid.counter, Tid#tid.pid]).
info(Serial) ->
io:format( "Info about transaction with serial == ~p~n", [Serial]),
{info, Participant, Trs} = req(info),
search_pr_participant(Serial, Participant),
search_pr_coordinator(Serial, Trs).
search_pr_coordinator(_S, []) -> no;
search_pr_coordinator(S, [{Tid, _Ts}|Tail]) ->
case Tid#tid.counter of
S ->
io:format( "Tid is coordinator, owner == \n", []),
display_pid_info(Tid#tid.pid),
search_pr_coordinator(S, Tail);
_ ->
search_pr_coordinator(S, Tail)
end.
search_pr_participant(_S, []) ->
false;
search_pr_participant(S, [ P | Tail]) ->
Tid = P#participant.tid,
Commit0 = P#participant.commit,
if
Tid#tid.counter == S ->
io:format( "Tid is participant to commit, owner == \n", []),
Pid = Tid#tid.pid,
display_pid_info(Pid),
io:format( "Tid wants to write objects \n",[]),
Commit =
if
is_binary(Commit0) -> binary_to_term(Commit0);
true -> Commit0
end,
io:format("~tp~n", [Commit]),
search_pr_participant(S,Tail); %% !!!!!
true ->
search_pr_participant(S, Tail)
end.
display_pid_info(Pid) ->
case rpc:pinfo(Pid) of
undefined ->
io:format( "Dead process \n");
Info ->
Call = fetch(initial_call, Info),
Curr = case fetch(current_function, Info) of
{Mod,F,Args} when is_list(Args) ->
{Mod,F,length(Args)};
Other ->
Other
end,
Reds = fetch(reductions, Info),
LM = fetch(message_queue_len, Info),
pformat(io_lib:format("~p", [Pid]),
io_lib:format("~tp", [Call]),
io_lib:format("~tp", [Curr]), Reds, LM)
end.
pformat(A1, A2, A3, A4, A5) ->
io:format( "~-12s ~-21ts ~-21ts ~9w ~4w~n", [A1,A2,A3,A4,A5]).
fetch(Key, Info) ->
case lists:keysearch(Key, 1, Info) of
{value, {_, Val}} ->
Val;
_ ->
0
end.
%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%% reconfigure stuff comes here ......
%%%%%%%%%%%%%%%%%%%%%
reconfigure_coordinators(N, [{Tid, [Store | _]} | Coordinators]) ->
case mnesia_recover:outcome(Tid, unknown) of
committed ->
WaitingNodes = ?ets_lookup(Store, waiting_for_commit_ack),
case lists:keymember(N, 2, WaitingNodes) of
false ->
ignore; % avoid spurious mnesia_down messages
true ->
send_mnesia_down(Tid, Store, N)
end;
_ ->
%% Tell the coordinator about the mnesia_down
send_mnesia_down(Tid, Store, N)
end,
reconfigure_coordinators(N, Coordinators);
reconfigure_coordinators(_N, []) ->
ok.
send_mnesia_down(Tid, Store, Node) ->
Msg = {mnesia_down, Node},
send_to_pids([Tid#tid.pid | get_elements(friends,Store)], Msg).
send_to_pids([Pid | Pids], Msg) when is_pid(Pid) ->
Pid ! Msg,
send_to_pids(Pids, Msg);
send_to_pids([_ | Pids], Msg) ->
send_to_pids(Pids, Msg);
send_to_pids([], _Msg) ->
ok.
reconfigure_participants(N, [P | Tail]) ->
case lists:member(N, P#participant.disc_nodes) or
lists:member(N, P#participant.ram_nodes) of
false ->
%% Ignore, since we are not a participant
%% in the transaction.
reconfigure_participants(N, Tail);
true ->
%% We are on a participant node, lets
%% check if the dead one was a
%% participant or a coordinator.
Tid = P#participant.tid,
if
node(Tid#tid.pid) /= N ->
%% Another participant node died. Ignore.
reconfigure_participants(N, Tail);
true ->
%% The coordinator node has died and
%% we must determine the outcome of the
%% transaction and tell mnesia_tm on all
%% nodes (including the local node) about it
verbose("Coordinator ~p in transaction ~p died~n",
[Tid#tid.pid, Tid]),
Nodes = P#participant.disc_nodes ++
P#participant.ram_nodes,
AliveNodes = Nodes -- [N],
Protocol = P#participant.protocol,
tell_outcome(Tid, Protocol, N, AliveNodes, AliveNodes),
reconfigure_participants(N, Tail)
end
end;
reconfigure_participants(_, []) ->
[].
%% We need to determine the outcome of the transaction and
%% tell mnesia_tm on all involved nodes (including the local node)
%% about the outcome.
tell_outcome(Tid, Protocol, Node, CheckNodes, TellNodes) ->
Outcome = mnesia_recover:what_happened(Tid, Protocol, CheckNodes),
case Outcome of
aborted ->
rpc:abcast(TellNodes, ?MODULE, {Tid,{do_abort, {mnesia_down, Node}}});
committed ->
rpc:abcast(TellNodes, ?MODULE, {Tid, do_commit})
end,
Outcome.
do_stop(#state{coordinators = Coordinators}) ->
Msg = {mnesia_down, node()},
lists:foreach(fun({Tid, _}) -> Tid#tid.pid ! Msg end, gb_trees:to_list(Coordinators)),
mnesia_checkpoint:stop(),
mnesia_log:stop(),
exit(shutdown).
fixtable(Tab, Lock, Me) ->
case req({fixtable, [Tab,Lock,Me]}) of
error ->
exit({no_exists, Tab});
Else ->
Else
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% System upgrade
system_continue(_Parent, _Debug, State) ->
doit_loop(State).
-spec system_terminate(_, _, _, _) -> no_return().
system_terminate(_Reason, _Parent, _Debug, State) ->
do_stop(State).
system_code_change(State=#state{coordinators=Cs0,participants=Ps0},_Module,_OldVsn,downgrade) ->
case is_tuple(Cs0) of
true ->
Cs = gb_trees:to_list(Cs0),
Ps = gb_trees:values(Ps0),
{ok, State#state{coordinators=Cs,participants=Ps}};
false ->
{ok, State}
end;
system_code_change(State=#state{coordinators=Cs0,participants=Ps0},_Module,_OldVsn,_Extra) ->
case is_list(Cs0) of
true ->
Cs = gb_trees:from_orddict(lists:sort(Cs0)),
Ps1 = [{P#participant.tid,P}|| P <- Ps0],
Ps = gb_trees:from_orddict(lists:sort(Ps1)),
{ok, State#state{coordinators=Cs,participants=Ps}};
false ->
{ok, State}
end.