blob: 09d063313589ac0404546fea77bef571819a8f1b [file] [log] [blame]
#!/usr/bin/env escript
%% -*- erlang -*-
% 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.
-record(user_ctx, {
name = null,
roles = [],
handler
}).
-define(i2l(I), integer_to_list(I)).
test_db_name() -> <<"couch_test_update_conflicts">>.
main(_) ->
test_util:init_code_path(),
etap:plan(35),
case (catch test()) of
ok ->
etap:end_tests();
Other ->
etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
etap:bail(Other)
end,
ok.
test() ->
couch_server_sup:start_link(test_util:config_files()),
couch_config:set("couchdb", "delayed_commits", "true", false),
lists:foreach(
fun(NumClients) -> test_concurrent_doc_update(NumClients) end,
[100, 500, 1000, 2000, 5000]),
test_bulk_delete_create(),
couch_server_sup:stop(),
ok.
% Verify that if multiple clients try to update the same document
% simultaneously, only one of them will get success response and all
% the other ones will get a conflict error. Also validate that the
% client which got the success response got its document version
% persisted into the database.
test_concurrent_doc_update(NumClients) ->
{ok, Db} = create_db(test_db_name()),
Doc = couch_doc:from_json_obj({[
{<<"_id">>, <<"foobar">>},
{<<"value">>, 0}
]}),
{ok, Rev} = couch_db:update_doc(Db, Doc, []),
ok = couch_db:close(Db),
RevStr = couch_doc:rev_to_str(Rev),
etap:diag("Created first revision of test document"),
etap:diag("Spawning " ++ ?i2l(NumClients) ++
" clients to update the document"),
Clients = lists:map(
fun(Value) ->
ClientDoc = couch_doc:from_json_obj({[
{<<"_id">>, <<"foobar">>},
{<<"_rev">>, RevStr},
{<<"value">>, Value}
]}),
Pid = spawn_client(ClientDoc),
{Value, Pid, erlang:monitor(process, Pid)}
end,
lists:seq(1, NumClients)),
lists:foreach(fun({_, Pid, _}) -> Pid ! go end, Clients),
etap:diag("Waiting for clients to finish"),
{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} ->
etap:bail("Client " ++ ?i2l(Value) ++
" got update error: " ++ couch_util:to_list(Error))
after 60000 ->
etap:bail("Timeout waiting for client " ++ ?i2l(Value) ++ " to die")
end
end,
{0, nil},
Clients),
etap:diag("Verifying client results"),
etap:is(
NumConflicts,
NumClients - 1,
"Got " ++ ?i2l(NumClients - 1) ++ " client conflicts"),
{ok, Db2} = couch_db:open_int(test_db_name(), []),
{ok, Leaves} = couch_db:open_doc_revs(Db2, <<"foobar">>, all, []),
ok = couch_db:close(Db2),
etap:is(length(Leaves), 1, "Only one document revision was persisted"),
[{ok, Doc2}] = Leaves,
{JsonDoc} = couch_doc:to_json_obj(Doc2, []),
etap:is(
couch_util:get_value(<<"value">>, JsonDoc),
SavedValue,
"Persisted doc has the right value"),
ok = timer:sleep(1000),
etap:diag("Restarting the server"),
couch_server_sup:stop(),
ok = timer:sleep(1000),
couch_server_sup:start_link(test_util:config_files()),
{ok, Db3} = couch_db:open_int(test_db_name(), []),
{ok, Leaves2} = couch_db:open_doc_revs(Db3, <<"foobar">>, all, []),
ok = couch_db:close(Db3),
etap:is(length(Leaves2), 1, "Only one document revision was persisted"),
[{ok, Doc3}] = Leaves,
etap:is(Doc3, Doc2, "Got same document after server restart"),
delete_db(Db3).
% COUCHDB-188
test_bulk_delete_create() ->
{ok, Db} = create_db(test_db_name()),
Doc = couch_doc:from_json_obj({[
{<<"_id">>, <<"foobar">>},
{<<"value">>, 0}
]}),
{ok, Rev} = couch_db:update_doc(Db, Doc, []),
DeletedDoc = couch_doc:from_json_obj({[
{<<"_id">>, <<"foobar">>},
{<<"_rev">>, couch_doc:rev_to_str(Rev)},
{<<"_deleted">>, true}
]}),
NewDoc = couch_doc:from_json_obj({[
{<<"_id">>, <<"foobar">>},
{<<"value">>, 666}
]}),
{ok, Results} = couch_db:update_docs(Db, [DeletedDoc, NewDoc], []),
ok = couch_db:close(Db),
etap:is(length([ok || {ok, _} <- Results]), 2,
"Deleted and non-deleted versions got an ok reply"),
[{ok, Rev1}, {ok, Rev2}] = Results,
{ok, Db2} = couch_db:open_int(test_db_name(), []),
{ok, [{ok, Doc1}]} = couch_db:open_doc_revs(
Db2, <<"foobar">>, [Rev1], [conflicts, deleted_conflicts]),
{ok, [{ok, Doc2}]} = couch_db:open_doc_revs(
Db2, <<"foobar">>, [Rev2], [conflicts, deleted_conflicts]),
ok = couch_db:close(Db2),
{Doc1Props} = couch_doc:to_json_obj(Doc1, []),
{Doc2Props} = couch_doc:to_json_obj(Doc2, []),
etap:is(couch_util:get_value(<<"_deleted">>, Doc1Props), true,
"Document was deleted"),
etap:is(couch_util:get_value(<<"_deleted">>, Doc2Props), undefined,
"New document not flagged as deleted"),
etap:is(couch_util:get_value(<<"value">>, Doc2Props), 666,
"New leaf revision has the right value"),
etap:is(couch_util:get_value(<<"_conflicts">>, Doc1Props), undefined,
"Deleted document has no conflicts"),
etap:is(couch_util:get_value(<<"_deleted_conflicts">>, Doc1Props), undefined,
"Deleted document has no deleted conflicts"),
etap:is(couch_util:get_value(<<"_conflicts">>, Doc2Props), undefined,
"New leaf revision doesn't have conflicts"),
etap:is(couch_util:get_value(<<"_deleted_conflicts">>, Doc2Props), undefined,
"New leaf revision doesn't have deleted conflicts"),
etap:is(element(1, Rev1), 2, "Deleted revision has position 2"),
etap:is(element(1, Rev2), 1, "New leaf revision has position 1"),
delete_db(Db2).
spawn_client(Doc) ->
spawn(fun() ->
{ok, Db} = couch_db:open_int(test_db_name(), []),
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).
create_db(DbName) ->
couch_db:create(
DbName,
[{user_ctx, #user_ctx{roles = [<<"_admin">>]}}, overwrite]).
delete_db(Db) ->
ok = couch_server:delete(
couch_db:name(Db), [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}]).