summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/fabric/src/fabric.erl5
-rw-r--r--src/fabric/src/fabric_doc_update.erl139
2 files changed, 137 insertions, 7 deletions
diff --git a/src/fabric/src/fabric.erl b/src/fabric/src/fabric.erl
index 5840cd63c..77ca8b7d9 100644
--- a/src/fabric/src/fabric.erl
+++ b/src/fabric/src/fabric.erl
@@ -326,6 +326,11 @@ update_doc(DbName, Doc, Options) ->
{ok, NewRev};
{accepted, [{accepted, NewRev}]} ->
{accepted, NewRev};
+ % This is a corner case when we update one document when
+ % news_edits:false, VDU forbids updating the document,
+ % and revision trees are out of sync across nodes.
+ {accepted, [{{_Id, _Rev}, {forbidden, Msg}}]} ->
+ throw({forbidden, Msg});
{ok, [{{_Id, _Rev}, Error}]} ->
throw(Error);
{ok, [Error]} ->
diff --git a/src/fabric/src/fabric_doc_update.erl b/src/fabric/src/fabric_doc_update.erl
index 62b639bab..7cb8efda5 100644
--- a/src/fabric/src/fabric_doc_update.erl
+++ b/src/fabric/src/fabric_doc_update.erl
@@ -157,16 +157,17 @@ force_reply(Doc, [FirstReply | _] = Replies, {Health, W, Acc}) ->
false ->
CounterKey = [fabric, doc_update, mismatched_errors],
couch_stats:increment_counter(CounterKey),
- {error, W, [{Doc, FirstReply} | Acc]}
+ case check_forbidden_msg(Replies, Health) of
+ {NewHealth, forbidden, Msg} ->
+ {NewHealth, W, [{Doc, {forbidden, Msg}} | Acc]};
+ false ->
+ {error, W, [{Doc, FirstReply} | Acc]}
+ end
end;
[AcceptedRev | _] ->
CounterKey = [fabric, doc_update, write_quorum_errors],
couch_stats:increment_counter(CounterKey),
- NewHealth =
- case Health of
- ok -> accepted;
- _ -> Health
- end,
+ NewHealth = change_health_accepted(Health),
{NewHealth, W, [{Doc, {accepted, AcceptedRev}} | Acc]}
end
end.
@@ -182,6 +183,35 @@ maybe_reply(Doc, Replies, {stop, W, Acc}) ->
continue
end.
+% This is a corner case where
+% 1) revision tree for the document are out of sync across nodes
+% 2) update on one node extends the revision tree
+% 3) VDU forbids the document
+% 4) remaining nodes do not extend revision tree, so noreply is returned
+% If at at least one nodes forbids the update, then we reject
+% the update no matter what other nodes return.
+check_forbidden_msg(Replies, Health) ->
+ Pred = fun(R) ->
+ case R of
+ {_, {forbidden, _}} ->
+ true;
+ _ ->
+ false
+ end
+ end,
+ case lists:search(Pred, Replies) of
+ {value, {_, {forbidden, Msg}}} ->
+ NewHealth = change_health_accepted(Health),
+ {NewHealth, forbidden, Msg};
+ false ->
+ false
+ end.
+
+change_health_accepted(ok) ->
+ accepted;
+change_health_accepted(Health) ->
+ Health.
+
update_quorum_met(W, Replies) ->
Counters = lists:foldl(
fun(R, D) -> orddict:update_counter(R, 1, D) end,
@@ -255,6 +285,8 @@ validate_atomic_update(_DbName, AllDocs, true) ->
throw({aborted, PreCommitFailures}).
-ifdef(TEST).
+
+-define(NODEBUG, false).
-include_lib("eunit/include/eunit.hrl").
setup_all() ->
@@ -273,7 +305,10 @@ doc_update_test_() ->
[
fun doc_update1/0,
fun doc_update2/0,
- fun doc_update3/0
+ fun doc_update3/0,
+ fun one_forbid_test/0,
+ fun two_forbid_test/0,
+ fun extend_tree_forbid_test/0
]
}.
@@ -406,6 +441,96 @@ doc_update3() ->
?assertEqual({ok, [{Doc1, {ok, Doc1}}, {Doc2, {ok, Doc2}}]}, Reply).
+one_forbid_test() ->
+ Doc1 = #doc{revs = {1, [<<"foo">>]}},
+ Doc2 = #doc{revs = {1, [<<"bar">>]}},
+ Docs = [Doc1, Doc2],
+ Shards =
+ mem3_util:create_partition_map("foo", 3, 1, ["node1", "node2", "node3"]),
+ GroupedDocs = group_docs_by_shard_hack(<<"foo">>, Shards, Docs),
+
+ Acc0 = {
+ length(Shards),
+ length(Docs),
+ list_to_integer("2"),
+ GroupedDocs,
+ dict:from_list([{Doc, []} || Doc <- Docs])
+ },
+
+ {ok, {WaitingCount1, _, _, _, _} = Acc1} =
+ handle_message({ok, [{ok, Doc1}, {noreply, Doc2}]}, hd(Shards), Acc0),
+ ?assertEqual(WaitingCount1, 2),
+
+ {ok, {WaitingCount2, _, _, _, _} = Acc2} =
+ handle_message({ok, [{ok, Doc1}, {noreply, Doc2}]}, lists:nth(2, Shards), Acc1),
+ ?assertEqual(WaitingCount2, 1),
+
+ {stop, Reply} =
+ handle_message({ok, [{ok, Doc1}, {Doc2, {forbidden, <<"not allowed">>}}]}, lists:nth(3, Shards), Acc2),
+
+ ?assertEqual({accepted, [{Doc1, {ok, Doc1}}, {Doc2, {forbidden, <<"not allowed">>}}]}, Reply).
+
+two_forbid_test() ->
+ Doc1 = #doc{revs = {1, [<<"foo">>]}},
+ Doc2 = #doc{revs = {1, [<<"bar">>]}},
+ Docs = [Doc1, Doc2],
+ Shards =
+ mem3_util:create_partition_map("foo", 3, 1, ["node1", "node2", "node3"]),
+ GroupedDocs = group_docs_by_shard_hack(<<"foo">>, Shards, Docs),
+
+ Acc0 = {
+ length(Shards),
+ length(Docs),
+ list_to_integer("2"),
+ GroupedDocs,
+ dict:from_list([{Doc, []} || Doc <- Docs])
+ },
+
+ {ok, {WaitingCount1, _, _, _, _} = Acc1} =
+ handle_message({ok, [{ok, Doc1}, {noreply, Doc2}]}, hd(Shards), Acc0),
+ ?assertEqual(WaitingCount1, 2),
+
+ {ok, {WaitingCount2, _, _, _, _} = Acc2} =
+ handle_message({ok, [{ok, Doc1}, {Doc2, {forbidden, <<"not allowed">>}}]}, lists:nth(2, Shards), Acc1),
+ ?assertEqual(WaitingCount2, 1),
+
+ {stop, Reply} =
+ handle_message({ok, [{ok, Doc1}, {Doc2, {forbidden, <<"not allowed">>}}]}, lists:nth(3, Shards), Acc2),
+
+ ?assertEqual({accepted, [{Doc1, {ok, Doc1}}, {Doc2, {forbidden, <<"not allowed">>}}]}, Reply).
+
+% This should actually never happen, because an `{ok, Doc}` message means that the revision
+% tree is extended and so the VDU should forbid the document.
+% Leaving this test here to make sure quorum rules still apply.
+extend_tree_forbid_test() ->
+ Doc1 = #doc{revs = {1, [<<"foo">>]}},
+ Doc2 = #doc{revs = {1, [<<"bar">>]}},
+ Docs = [Doc1, Doc2],
+ Shards =
+ mem3_util:create_partition_map("foo", 3, 1, ["node1", "node2", "node3"]),
+ GroupedDocs = group_docs_by_shard_hack(<<"foo">>, Shards, Docs),
+
+ Acc0 = {
+ length(Shards),
+ length(Docs),
+ list_to_integer("2"),
+ GroupedDocs,
+ dict:from_list([{Doc, []} || Doc <- Docs])
+ },
+
+ {ok, {WaitingCount1, _, _, _, _} = Acc1} =
+ handle_message({ok, [{ok, Doc1}, {ok, Doc2}]}, hd(Shards), Acc0),
+ ?assertEqual(WaitingCount1, 2),
+
+ {ok, {WaitingCount2, _, _, _, _} = Acc2} =
+ handle_message({ok, [{ok, Doc1}, {Doc2, {forbidden, <<"not allowed">>}}]}, lists:nth(2, Shards), Acc1),
+ ?assertEqual(WaitingCount2, 1),
+
+ {stop, Reply} =
+ handle_message({ok, [{ok, Doc1}, {ok, Doc2}]}, lists:nth(3, Shards), Acc2),
+
+ ?assertEqual({ok, [{Doc1, {ok, Doc1}}, {Doc2, {ok, Doc2}}]}, Reply).
+
% needed for testing to avoid having to start the mem3 application
group_docs_by_shard_hack(_DbName, Shards, Docs) ->
dict:to_list(