blob: 1f810f38498722d2e1df55cdc6e82f2d7ef69311 [file] [log] [blame]
% 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.
-module(couchdb_update_conflicts_tests).
-include_lib("couch/include/couch_eunit.hrl").
-include_lib("couch/include/couch_db.hrl").
-define(i2l(I), integer_to_list(I)).
-define(DOC_ID, <<"foobar">>).
-define(NUM_CLIENTS, [100, 500, 1000, 2000, 5000, 10000]).
-define(TIMEOUT, 10000).
start() ->
Ctx = test_util:start_couch(),
config:set("couchdb", "delayed_commits", "true", false),
Ctx.
setup() ->
DbName = ?tempdb(),
{ok, Db} = couch_db:create(DbName, [?ADMIN_CTX, overwrite]),
Doc = couch_doc:from_json_obj({[{<<"_id">>, ?DOC_ID},
{<<"value">>, 0}]}),
{ok, Rev} = couch_db:update_doc(Db, Doc, []),
ok = couch_db:close(Db),
RevStr = couch_doc:rev_to_str(Rev),
{DbName, RevStr}.
setup(_) ->
setup().
teardown({DbName, _}) ->
ok = couch_server:delete(DbName, []),
ok.
teardown(_, {DbName, _RevStr}) ->
teardown({DbName, _RevStr}).
view_indexes_cleanup_test_() ->
{
"Update conflicts",
{
setup,
fun start/0, fun test_util:stop_couch/1,
[
concurrent_updates(),
couchdb_188()
]
}
}.
concurrent_updates()->
{
"Concurrent updates",
{
foreachx,
fun setup/1, fun teardown/2,
[{NumClients, fun should_concurrently_update_doc/2}
|| NumClients <- ?NUM_CLIENTS]
}
}.
couchdb_188()->
{
"COUCHDB-188",
{
foreach,
fun setup/0, fun teardown/1,
[fun should_bulk_create_delete_doc/1]
}
}.
should_concurrently_update_doc(NumClients, {DbName, InitRev})->
{?i2l(NumClients) ++ " clients",
{inorder,
[{"update doc",
{timeout, ?TIMEOUT div 1000,
?_test(concurrent_doc_update(NumClients, DbName, InitRev))}},
{"ensure in single leaf",
?_test(ensure_in_single_revision_leaf(DbName))}]}}.
should_bulk_create_delete_doc({DbName, InitRev})->
?_test(bulk_delete_create(DbName, InitRev)).
concurrent_doc_update(NumClients, DbName, InitRev) ->
Clients = lists:map(
fun(Value) ->
ClientDoc = couch_doc:from_json_obj({[
{<<"_id">>, ?DOC_ID},
{<<"_rev">>, InitRev},
{<<"value">>, Value}
]}),
Pid = spawn_client(DbName, ClientDoc),
{Value, Pid, erlang:monitor(process, Pid)}
end,
lists:seq(1, NumClients)),
lists:foreach(fun({_, Pid, _}) -> Pid ! go end, Clients),
{NumConflicts, SavedValue} = lists:foldl(
fun({Value, Pid, MonRef}, {AccConflicts, AccValue}) ->
receive
{'DOWN', MonRef, process, Pid, {ok, _NewRev}} ->
{AccConflicts, Value};
{'DOWN', MonRef, process, Pid, conflict} ->
{AccConflicts + 1, AccValue};
{'DOWN', MonRef, process, Pid, Error} ->
erlang:error({assertion_failed,
[{module, ?MODULE},
{line, ?LINE},
{reason, "Client " ++ ?i2l(Value)
++ " got update error: "
++ couch_util:to_list(Error)}]})
after ?TIMEOUT div 2 ->
erlang:error({assertion_failed,
[{module, ?MODULE},
{line, ?LINE},
{reason, "Timeout waiting for client "
++ ?i2l(Value) ++ " to die"}]})
end
end, {0, nil}, Clients),
?assertEqual(NumClients - 1, NumConflicts),
{ok, Db} = couch_db:open_int(DbName, []),
{ok, Leaves} = couch_db:open_doc_revs(Db, ?DOC_ID, all, []),
ok = couch_db:close(Db),
?assertEqual(1, length(Leaves)),
[{ok, Doc2}] = Leaves,
{JsonDoc} = couch_doc:to_json_obj(Doc2, []),
?assertEqual(SavedValue, couch_util:get_value(<<"value">>, JsonDoc)).
ensure_in_single_revision_leaf(DbName) ->
{ok, Db} = couch_db:open_int(DbName, []),
{ok, Leaves} = couch_db:open_doc_revs(Db, ?DOC_ID, all, []),
ok = couch_db:close(Db),
[{ok, Doc}] = Leaves,
%% FIXME: server restart won't work from test side
%% stop(ok),
%% start(),
{ok, Db2} = couch_db:open_int(DbName, []),
{ok, Leaves2} = couch_db:open_doc_revs(Db2, ?DOC_ID, all, []),
ok = couch_db:close(Db2),
?assertEqual(1, length(Leaves2)),
[{ok, Doc2}] = Leaves,
?assertEqual(Doc, Doc2).
bulk_delete_create(DbName, InitRev) ->
{ok, Db} = couch_db:open_int(DbName, []),
DeletedDoc = couch_doc:from_json_obj({[
{<<"_id">>, ?DOC_ID},
{<<"_rev">>, InitRev},
{<<"_deleted">>, true}
]}),
NewDoc = couch_doc:from_json_obj({[
{<<"_id">>, ?DOC_ID},
{<<"value">>, 666}
]}),
{ok, Results} = couch_db:update_docs(Db, [DeletedDoc, NewDoc], []),
ok = couch_db:close(Db),
?assertEqual(2, length([ok || {ok, _} <- Results])),
[{ok, Rev1}, {ok, Rev2}] = Results,
{ok, Db2} = couch_db:open_int(DbName, []),
{ok, [{ok, Doc1}]} = couch_db:open_doc_revs(
Db2, ?DOC_ID, [Rev1], [conflicts, deleted_conflicts]),
{ok, [{ok, Doc2}]} = couch_db:open_doc_revs(
Db2, ?DOC_ID, [Rev2], [conflicts, deleted_conflicts]),
ok = couch_db:close(Db2),
{Doc1Props} = couch_doc:to_json_obj(Doc1, []),
{Doc2Props} = couch_doc:to_json_obj(Doc2, []),
%% Document was deleted
?assert(couch_util:get_value(<<"_deleted">>, Doc1Props)),
%% New document not flagged as deleted
?assertEqual(undefined, couch_util:get_value(<<"_deleted">>,
Doc2Props)),
%% New leaf revision has the right value
?assertEqual(666, couch_util:get_value(<<"value">>,
Doc2Props)),
%% Deleted document has no conflicts
?assertEqual(undefined, couch_util:get_value(<<"_conflicts">>,
Doc1Props)),
%% Deleted document has no deleted conflicts
?assertEqual(undefined, couch_util:get_value(<<"_deleted_conflicts">>,
Doc1Props)),
%% New leaf revision doesn't have conflicts
?assertEqual(undefined, couch_util:get_value(<<"_conflicts">>,
Doc1Props)),
%% New leaf revision doesn't have deleted conflicts
?assertEqual(undefined, couch_util:get_value(<<"_deleted_conflicts">>,
Doc1Props)),
%% Deleted revision has position 2
?assertEqual(2, element(1, Rev1)),
%% New leaf revision has position 3
?assertEqual(3, element(1, Rev2)).
spawn_client(DbName, Doc) ->
spawn(fun() ->
{ok, Db} = couch_db:open_int(DbName, []),
receive
go -> ok
end,
erlang:yield(),
Result = try
couch_db:update_doc(Db, Doc, [])
catch _:Error ->
Error
end,
ok = couch_db:close(Db),
exit(Result)
end).