Re: Race condition in mnesia_locker after nodedown for sticky_locks

Henrik Nord <[email protected]>
Newsgroups gmane.comp.lang.erlang.patches
Message-ID <[email protected]>
Hi

Thank you for your contribution!
I have created a pull request from this, (This is becoming the prefered 
way of submitting patches to Erlang/otp) and the team will take a look 
at your patch.

/Henrik

On 2013-10-04 08:52, Jan Chochol wrote:
> Hi OTP team,
>
> We found found bug (race condition) in mnesia code regarding
> disconnecting nodes owning sticky lock.
> Our (simplified) setup consists of two nodes, when first node acts as
> active one and second acts as standby.
> It means that all requests are normally going through first node and
> second node is only used after fail of the first. Mnesia in our
> scenario is used as HA data store.
> For this setup it seems that sticky locks are ideal solution - all
> operations are performed only on one node.
> Everything worked well, until we found that some times transactions
> started just after disconnecting first node hangs indefinitely.
> This bogus behaviour was very rare, so it seemed that there is some
> race condition how sticky locks are handled after disconnection of
> node.
>
> We found that there is problem in synchronization of transaction
> process (process initializing transaction) and mnesia_locker process.
> It can happen that mnesia_locker process respond to lock request with
> {switch, N, Req} message, but node N is already disconnected.
>
> Part of message trace to show the problem:
>
> List of interesting PIDs:
>
> mnesia_controller="<0.93.0>"
> mnesia_tm="<0.89.0>"
> mnesia_monitor="<0.85.0>"
> mnesia_locker="<0.87.0>"
> mnesia_recover="<0.88.0>"
> transaction process="<0.206.0>"
>
> Message trace:
>
> % mnesia_monitor lost track of mnesia_monitor at node 1
> (<0.85.0>) << {'EXIT',<5184.92.0>,noconnection}
>
> % mnesia_monitor receives nodedown - this message is ignored
> (<0.85.0>) << {nodedown,'gcfc-triple1_519129@localhost'}
>
> % mnesia_monitor call mnesia_recover:mnesia_down()
> (<0.85.0>) <0.88.0> ! {'$gen_cast',
> {mnesia_down,'gcfc-triple1_519129@localhost'}}
>
> % mnesia_recover receives mnesia_down, nothing to do there
> (<0.88.0>) << {'$gen_cast',{mnesia_down,'gcfc-triple1_519129@localhost'}}
>
> % mnesia_monitor call mnesia_controller:mnesia_down
> (<0.85.0>) <0.93.0> ! {'$gen_cast',
> {mnesia_down,'gcfc-triple1_519129@localhost'}}
>
> % mnesia_controller receives mnesia_down
> (<0.93.0>) << {'$gen_cast',{mnesia_down,'gcfc-triple1_519129@localhost'}}
>
> % mnesia_controller calls mnesia_recover:log_mnesia_down()
> (<0.93.0>) <0.88.0> ! {'$gen_call', {<0.93.0>,#Ref<0.0.0.1860>},
> {log_mnesia_down,'gcfc-triple1_519129@localhost'}}
>
> % mnesia_recover receives log_mnesia_down, store it in decision table and reply
> (<0.88.0>) << {'$gen_call',{<0.93.0>,#Ref<0.0.0.1860>},
> {log_mnesia_down,'gcfc-triple1_519129@localhost'}}
> (<0.88.0>) <0.93.0> ! {#Ref<0.0.0.1860>,ok}
>
> % mnesia_controller receives response from mnesia_recover:log_mnesia_down()
> (<0.93.0>) << {#Ref<0.0.0.1860>,ok}
>
> % mnesia_controller calls mnesia_monitor:mnesia_down(mnesia_controller, Node)
> (<0.93.0>) <0.85.0> ! {'$gen_cast', {mnesia_down,mnesia_controller,
> 'gcfc-triple1_519129@localhost'}}
>
> % mnesia_controller receives mnesia_down, mnesia_controller
> (<0.85.0>) << {'$gen_cast', {mnesia_down,mnesia_controller,
> 'gcfc-triple1_519129@localhost'}}
>
> % mnesia_controller forwards mnesia_down to mnesia_tm
> (<0.85.0>) <0.89.0> ! {mnesia_down,'gcfc-triple1_519129@localhost'}
>
> % mnesia_tm receives mnesia_down
> (<0.89.0>) << {mnesia_down,'gcfc-triple1_519129@localhost'}
>
> % mnesia_tm calls mnesia_monitor:mnesia_down(mnesia_tm, {Node,
> ParticipantsTids}),
> (<0.89.0>) <0.85.0> ! {'$gen_cast', {mnesia_down,mnesia_tm,
> {'gcfc-triple1_519129@localhost',[]}}}
>
> % mnesia_monitor receives mnesia_down, mnesia_tm
> % now it is going to call mnesia_locker:mnesia_down, which will send
> release_remote_non_pending
> (<0.85.0>) << {'$gen_cast', {mnesia_down, mnesia_tm,
> {'gcfc-triple1_519129@localhost',[]}}}
>
> % Our processes receives information about node down and try to
> recover from mnesiai
> (<0.206.0>) << {activate,true}
>
> % Starts outer transaction
> (<0.206.0>) <0.89.0> ! {{<0.206.0>,#Ref<0.0.0.1875>},start_outer}
> (<0.89.0>) << {{<0.206.0>,#Ref<0.0.0.1875>},start_outer}
>
> % Reply for start_outer
> (<0.89.0>) <0.206.0> ! {mnesia_tm,#Ref<0.0.0.1875>,
> {new_tid,{tid,176,<0.206.0>},245835}}
> (<0.206.0>) << {mnesia_tm,#Ref<0.0.0.1875>,
> {new_tid,{tid,176,<0.206.0>},245835}}
>
> % Try to obtain lock for mnesia:select (just failed node holded sticky
> lock for that table)
> (<0.206.0>) {mnesia_locker,'gcfc-triple2_519129@localhost'} !
> {<0.206.0>, {read, {tid,176, <0.206.0>},
> {'tasks_tt_gcfc-triple1_519129@localhost_recover',
> '______WHOLETABLE_____'}}}
> (<0.87.0>) << {<0.206.0>, {read, {tid,176,<0.206.0>},
> {'tasks_tt_gcfc-triple1_519129@localhost_recover',
> '______WHOLETABLE_____'}}}
>
> % mnesia_locker informs out process that it needs switch sticky lock
> (<0.87.0>) <0.206.0> ! {mnesia_locker,'gcfc-triple2_519129@localhost',
> {switch,'gcfc-triple1_519129@localhost', {<0.206.0>, {read,
> {tid,176,<0.206.0>},
> {'tasks_tt_gcfc-triple1_519129@localhost_recover',
> '______WHOLETABLE_____'}}}}}
> (<0.206.0>) << {mnesia_locker,'gcfc-triple2_519129@localhost',
> {switch,'gcfc-triple1_519129@localhost', {<0.206.0>, {read,
> {tid,176,<0.206.0>},
> {'tasks_tt_gcfc-triple1_519129@localhost_recover',
> '______WHOLETABLE_____'}}}}}
>
> % Our proces just want to send {<0.206.0>, {read, {tid,176,<0.206.0>},
> {'tasks_tt_gcfc-triple1_519129@localhost_recover',
> '______WHOLETABLE_____'}}} to other node
> % Other node is not connected, so Erlang tryies to connect to it
> (<0.21.0> is net_kernel process)
> (<0.206.0>) <0.21.0> ! {'$gen_call', {<0.206.0>,#Ref<0.0.0.1884>},
> {connect,normal,'gcfc-triple1_519129@localhost'}}
>
> % Connection to remote node was unsucsefull, and our process is hanged
> in this function:
> %
> % l_req_rec(Node, Store) ->
> %     ?ets_insert(Store, {nodes, Node}),
> %     receive
> %         {?MODULE, Node, Reply} ->
> %         Reply;
> %     {mnesia_down, Node} ->
> %         {not_granted, {node_not_running, Node}}
> % end.
> %
> % It will never get out, as sent message is lost
> (<0.206.0>) << {#Ref<0.0.0.1884>,false}
>
> % mnesia_monitor call mnesia_locker:mnesia_down, which is sending
> release_remote_non_pending
> (<0.85.0>) <0.87.0> ! {release_remote_non_pending,
> 'gcfc-triple1_519129@localhost',[]}
>
> % mnesia_locker receives release_remote_non_pending
> % it removes sticky locks of failed nodes and releases all transactions
> % but it is too late - process <0.206.0> is already stucked for
> waiting for response from not connected node
> (<0.87.0>) << {release_remote_non_pending,'gcfc-triple1_519129@localhost',[]}
>
> % mnesia_locker call mnesia_monitor:mnesia_down(mnesia_locker, Node)
> (<0.87.0>) <0.85.0> ! {'$gen_cast', {mnesia_down,mnesia_locker,
> 'gcfc-triple1_519129@localhost'}}
>
> % mnesia_monitor receives mnesia_down, mnesia_locker
> (<0.85.0>) << {'$gen_cast',
> {mnesia_down,mnesia_locker,'gcfc-triple1_519129@localhost'}}
>
> % mnesia_monitor calls mnesia_lib:report_system_event, which will send
> notify to mnesia_event
> (<0.85.0>) <0.83.0> ! {notify, {mnesia_system_event,
> {mnesia_down,'gcfc-triple1_519129@localhost'}}}
>
>
> So problem is that transaction process sends messages to not-connected
> node and awaits response which will never come.
> One of the easiest solution is to monitor remote process, which
> prevent this from happening.
>
> Suggested patch is available at:
>
> git fetch git://github.com/janchochol/otp.git sticky_lock_race
>
> https://github.com/janchochol/otp/compare/erlang:maint...sticky_lock_race
> https://github.com/janchochol/otp/compare/erlang:maint...sticky_lock_race.patch
>
>
> You can try to reproduce the problem with following module:
>
> --- BEGIN OF slock.erl ---
> %% stick_lock problem reproducer
> %%
> %% To reproduce problem execute these commands in parallel:
> %%
> %% erl -sname n2@localhost -cookie slock -s slock n2
> %% erl -sname n1@localhost -cookie slock -s slock n1
> %%
> %% Problem is timing related, so it has relatively low probability of
> reproducing.
> %% To increase probability use this patch:
> %%
> %% --- a/lib/mnesia/src/mnesia_locker.erl
> %% +++ b/lib/mnesia/src/mnesia_locker.erl
> %% @@ -663,6 +663,7 @@ mnesia_down(N, Pending) ->
> %%             %% race with mnesia_tm's coordinator processes
> %%             %% that may restart and acquire new locks.
> %%             %% mnesia_monitor ensures the sync.
> %% +           timer:sleep(1000),
> %%             Pid ! {release_remote_non_pending, N, Pending}
> %%      end.
> %%
> %%
> %% When problem appears n1 node will hang indefinitely.
> %% When problem is not present n1 will finish shortly after n2.
>
>
> -module(slock).
>
> -export([n1/0, n2/0]).
>
> wait_for_n2() ->
>      case rpc:call(n2@localhost, erlang, whereis, [n2]) of
>          Pid when is_pid(Pid) ->
>              Pid;
>          _ ->
>              timer:sleep(100),
>              wait_for_n2()
>      end.
>
> wait_fun(Fun, PollIntervalMs) ->
>      case Fun() of
>          false ->
>              timer:sleep(PollIntervalMs),
>              wait_fun(Fun, PollIntervalMs);
>          true ->
>              ok
>      end.
>
>
> n1() ->
>      register(n1, self()),
>      io:format("n1: statred~n", []),
>      io:format("n1: waiting for n2~n", []),
>      N2 = wait_for_n2(),
>      io:format("n1: n2 found~n", []),
>      io:format("n1: init mnesia start~n", []),
>      ok = application:start(mnesia, permanent),
>      ok = rpc:call(n2@localhost, application, start, [mnesia, permanent]),
>      {ok, [_ | _]} = mnesia:change_config(extra_db_nodes, [n2@localhost]),
>      {atomic, ok} = mnesia:create_table(t1, [{ram_copies,
> [n1@localhost, n2@localhost]}]),
>      ok = rpc:call(n2@localhost, mnesia, wait_for_tables, [[t1], infinity]),
>      io:format("n1: init mnesia end~n", []),
>      N2 ! {ping, self()},
>      receive step -> ok end,
>      wait_fun(fun() -> net_adm:ping(n2@localhost) =:= pang end, 100),
>      io:format("n1: select start~n", []),
>      Ret = mnesia:transaction(fun() ->
>                  wait_fun(fun() -> net_adm:ping(n2@localhost) =:= pang end, 10),
>                  mnesia:select(t1, [{{t1, '_', '_'}, [], ['$_']}])
>          end),
>      io:format("n1: select end: ~p~n", [Ret]),
>      init:stop().
>
> n2() ->
>      register(n2, self()),
>      io:format("n2: statred~n", []),
>      io:format("n2: waiting for n1 ping~n", []),
>      N2 = receive
>          {ping, IN2} ->
>              IN2
>      end,
>      io:format("n2: n1 pinged~n", []),
>      io:format("n2: write start~n", []),
>      mnesia:transaction(fun() -> mnesia:write(t1, {t1, a, b}, sticky_write) end),
>      io:format("n2: write end~n", []),
>      N2 ! step,
>      io:format("n2: finish~n", []),
>      erlang:halt().
> --- END OF slock.erl ---
>
> I do not see any other easy solution for this problem.
>
> Best Regards,
> Jan Chochol
> _______________________________________________
> erlang-patches mailing list
> [email protected]
> http://erlang.org/mailman/listinfo/erlang-patches

-- 
/Henrik Nord Erlang/OTP

_______________________________________________
erlang-patches mailing list
[email protected]
http://erlang.org/mailman/listinfo/erlang-patches
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.