blob: 0722103a4eda4efb79156b4ae6cdfe6cf01e6d51 [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(LOCAL_DOC_ID, <<"_local/foobar">>).
-define(NUM_CLIENTS, [100, 500, 1000, 2000, 5000, 10000]).
-define(TIMEOUT, 20000).
start() ->
test_util:start_couch().
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(),
bulk_docs_updates()
]
}
}.
concurrent_updates() ->
{
"Concurrent updates",
{
foreachx,
fun setup/1,
fun teardown/2,
[
{NumClients, fun should_concurrently_update_doc/2}
|| NumClients <- ?NUM_CLIENTS
]
}
}.
bulk_docs_updates() ->
{
"Bulk docs updates",
{
foreach,
fun setup/0,
fun teardown/1,
[
fun should_bulk_create_delete_doc/1,
fun should_bulk_create_local_doc/1,
fun should_ignore_invalid_local_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)).
should_bulk_create_local_doc({DbName, _}) ->
?_test(bulk_create_local_doc(DbName)).
should_ignore_invalid_local_doc({DbName, _}) ->
?_test(ignore_invalid_local_doc(DbName)).
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)).
bulk_create_local_doc(DbName) ->
{ok, Db} = couch_db:open_int(DbName, []),
LocalDoc = couch_doc:from_json_obj(
{[
{<<"_id">>, ?LOCAL_DOC_ID},
{<<"_rev">>, <<"0-1">>}
]}
),
{ok, Results} = couch_db:update_docs(
Db,
[LocalDoc],
[],
?REPLICATED_CHANGES
),
ok = couch_db:close(Db),
?assertEqual([], Results),
{ok, Db2} = couch_db:open_int(DbName, []),
{ok, LocalDoc1} = couch_db:open_doc_int(Db2, ?LOCAL_DOC_ID, []),
ok = couch_db:close(Db2),
?assertEqual(?LOCAL_DOC_ID, LocalDoc1#doc.id),
?assertEqual({0, [<<"2">>]}, LocalDoc1#doc.revs).
ignore_invalid_local_doc(DbName) ->
{ok, Db} = couch_db:open_int(DbName, []),
LocalDoc = couch_doc:from_json_obj(
{[
{<<"_id">>, ?LOCAL_DOC_ID},
{<<"_rev">>, <<"0-abcdef">>}
]}
),
{ok, Results} = couch_db:update_docs(
Db,
[LocalDoc],
[],
?REPLICATED_CHANGES
),
ok = couch_db:close(Db),
?assertEqual([], Results),
{ok, Db2} = couch_db:open_int(DbName, []),
Result2 = couch_db:open_doc_int(Db2, ?LOCAL_DOC_ID, []),
ok = couch_db:close(Db2),
?assertEqual({not_found, missing}, Result2).
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).