You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by da...@apache.org on 2018/09/05 21:32:18 UTC

[couchdb] 01/01: Reproduce race condition in couch_server

This is an automated email from the ASF dual-hosted git repository.

davisp pushed a commit to branch fix-couch-server-race-condition
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit c9fbed38313301b7d91e1199d7bbc512345e723c
Author: Paul J. Davis <pa...@gmail.com>
AuthorDate: Wed Sep 5 16:25:41 2018 -0500

    Reproduce race condition in couch_server
    
    A rather uncommon bug found in production. Will write more as this is
    just for show and tell.
    
    For now this test case just demonstrates the issue that was discovered.
    A fix is still being pondered.
---
 src/couch/test/couch_server_tests.erl | 193 ++++++++++++++++++++++++++++++++++
 1 file changed, 193 insertions(+)

diff --git a/src/couch/test/couch_server_tests.erl b/src/couch/test/couch_server_tests.erl
index a79f20f..694f045 100644
--- a/src/couch/test/couch_server_tests.erl
+++ b/src/couch/test/couch_server_tests.erl
@@ -14,6 +14,7 @@
 
 -include_lib("couch/include/couch_eunit.hrl").
 -include_lib("couch/include/couch_db.hrl").
+-include("../src/couch_server_int.hrl").
 
 start() ->
     Ctx = test_util:start_couch(),
@@ -105,3 +106,195 @@ bad_engine_option_test_() ->
 t_bad_engine_option() ->
     Resp = couch_server:create(?tempdb(), [{engine, <<"cowabunga!">>}]),
     ?assertEqual(Resp, {error, {invalid_engine_extension, <<"cowabunga!">>}}).
+
+
+interleaved_requests_test_() ->
+    {
+        setup,
+        fun start_interleaved/0,
+        fun stop_interleaved/1,
+        fun make_interleaved_requests/1
+    }.
+
+
+start_interleaved() ->
+    TestDbName = ?tempdb(),
+    meck:new(couch_db, [passthrough]),
+    meck:expect(couch_db, start_link, fun(Engine, DbName, Filename, Options) ->
+        case DbName of
+            TestDbName ->
+                receive
+                    go -> ok
+                end,
+                Res = meck:passthrough([Engine, DbName, Filename, Options]),
+                % We're unlinking and sending a delayed
+                % EXIT signal so that we can mimic a specific
+                % message order in couch_server. On a test machine
+                % this is a big race condition which affects the
+                % ability to induce the bug.
+                case Res of
+                    {ok, Db} ->
+                        DbPid = couch_db:get_pid(Db),
+                        unlink(DbPid),
+                        Msg = {'EXIT', DbPid, killed},
+                        erlang:send_after(2000, whereis(couch_server), Msg);
+                    _ ->
+                        ok
+                end,
+                Res;
+            _ ->
+                meck:passthrough([Engine, DbName, Filename, Options])
+        end
+    end),
+    {test_util:start_couch(), TestDbName}.
+
+
+stop_interleaved({Ctx, TestDbName}) ->
+    %couch_server:delete(TestDbName, [?ADMIN_CTX]),
+    meck:unload(),
+    test_util:stop_couch(Ctx).
+
+
+make_interleaved_requests({_, TestDbName}) ->
+    [
+        fun() -> t_interleaved_create_delete_open(TestDbName) end
+    ].
+
+
+t_interleaved_create_delete_open(DbName) ->
+    {CrtRef, DelRef, OpenRef} = {make_ref(), make_ref(), make_ref()},
+    CrtMsg = {'$gen_call', {self(), CrtRef}, {create, DbName, [?ADMIN_CTX]}},
+    DelMsg = {'$gen_call', {self(), DelRef}, {delete, DbName, [?ADMIN_CTX]}},
+    OpenMsg = {'$gen_call', {self(), OpenRef}, {open, DbName, [?ADMIN_CTX]}},
+
+    % Start our first instance that will succeed in
+    % an invalid state. Notice that the opener pid
+    % spawned by couch_server:open_async/5 will halt
+    % in our meck expect function waiting for a message.
+    %
+    % We're using raw message passing here so that we don't
+    % have to coordinate multiple processes for this test.
+    whereis(couch_server) ! CrtMsg,
+
+    {ok, OpenerPid} = get_opener_pid(DbName),
+
+    % We have to suspend couch_server so that we can enqueue
+    % our next requests and let the opener finish processing.
+    erlang:suspend_process(whereis(couch_server)),
+
+    % Since couch_server is suspend, this delete request won't
+    % be processed until after the opener has sent its
+    % successful open response via gen_server:call/3
+    whereis(couch_server) ! DelMsg,
+
+    % This open request will be in the queue after the
+    % delete request but before the gen_server:call/3
+    % message which will establish the mixed up state
+    % in the couch_dbs ets table
+    whereis(couch_server) ! OpenMsg,
+
+    % First release the opener pid so it can continue
+    % working while we tweak meck
+    OpenerPid ! go,
+
+    % Replace our expect call to meck so that the OpenMsg
+    % isn't blocked on the receive
+    meck:expect(couch_db, start_link, fun(Engine, DbName1, Filename, Options) ->
+        meck:passthrough([Engine, DbName1, Filename, Options])
+    end),
+
+    % Wait for the '$gen_call' message from OpenerPid to arrive
+    % in couch_server's mailbox
+    {ok, Db} = wait_for_call_message(OpenerPid),
+
+    % We have to suspend the database process so that its 'EXIT'
+    % signal does affect the state of the couch_dbs ets table
+    erlang:suspend_process(couch_db:get_pid(Db)),
+
+    % Now monitor and resume the couch_server and assert that
+    % couch_server does not crash while processing OpenMsg
+    CSRef = erlang:monitor(process, whereis(couch_server)),
+    receive
+        {'DOWN', CSRef, _, _, Reason0} ->
+            erlang:error({crashed, Reason0})
+    after 0 ->
+        ok
+    end,
+
+    erlang:resume_process(whereis(couch_server)),
+
+    % Now make sure we get our responses as expected.
+    % The create response is expected to return not_found
+    % due to the delete request canceling the async opener
+    % pid and sending not_found to all waiters unconditionally
+    receive
+        {CrtRef, not_found} ->
+            ok;
+        {'DOWN', CSRef, _, _, Reason1} ->
+            erlang:error({crashed, Reason1})
+    after 5000 ->
+        erlang:error({timeout, create_message})
+    end,
+
+    timer:sleep(2000),
+
+    receive
+        {DelRef, ok} ->
+            ok;
+        {'DOWN', CSRef, _, _, Reason2} ->
+            erlang:error({crashed, Reason2})
+    after 5000 ->
+        erlang:error({timeout, delete_message})
+    end,
+
+    % The open request will actually get the {ok, Db}
+    % response from the initial create request
+    receive
+        {OpenRef, {ok, _}} ->
+            ok;
+        {'DOWN', CSRef, _, _, Reason3} ->
+            erlang:error({crashed, Reason3});
+        Else ->
+            {_, OtherMessages} = process_info(self(), messages),
+            erlang:error({wtf, [Else | OtherMessages]})
+    after 5000 ->
+        erlang:error({timeout, open_message})
+    end,
+
+    % And finally assert that couch_server is still
+    % alive.
+    ?assert(is_process_alive(whereis(couch_server))),
+    receive
+        {'DOWN', CSRef, _, _, Reason4} ->
+            erlang:error({crashed, Reason4})
+    after 500 ->
+        ok
+    end.
+
+
+get_opener_pid(DbName) ->
+    WaitFun = fun() ->
+        case ets:lookup(couch_dbs, DbName) of
+            [#entry{pid = Pid}] ->
+                {ok, Pid};
+            [] ->
+                wait
+        end
+    end,
+    test_util:wait(WaitFun).
+
+
+wait_for_call_message(Pid) ->
+    WaitFun = fun() ->
+        {_, Messages} = erlang:process_info(whereis(couch_server), messages),
+        Ret = lists:foldl(fun(Msg, Acc) ->
+            case Msg of
+                {'$gen_call', {Pid, _}, {open_result, _, _, {ok, _} = Resp}} ->
+                    Resp;
+                _ ->
+                    Acc
+            end
+        end, undefined, Messages),
+        if Ret == undefined -> wait; true -> Ret end
+    end,
+    test_util:wait(WaitFun).