This is an automated email from the ASF dual-hosted git repository.
nickva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/couchdb.git
The following commit(s) were added to refs/heads/main by this push:
new 0eb3ec67e Handle errors better in index cleanup
0eb3ec67e is described below
commit 0eb3ec67e3450e3ca71846e067227ae4668f0beb
Author: Nick Vatamaniuc <[email protected]>
AuthorDate: Tue Jul 28 19:36:56 2026 -0400
Handle errors better in index cleanup
Previously, if a node was down when cleaning up any indexes we'd get an
exception like:
```
{erpc,noconnection} : node@host
[<<"erpc:receive_response/3 L1442">>,<<"dreyfus_fabric_cleanup:recv/3
L54">>
[<<"erpc:receive_response/3 L1442">>,<<"dreyfus_fabric_cleanup:recv/3
L54">>,...
```
We can handle that better by anticipating a few more error conditions:
noconnection, exits, throws, timeouts.
To avoid copying the mess in three place let's just use a single helper
function in fabric.
Add tests for all the silly bits.
---
src/dreyfus/src/dreyfus_fabric_cleanup.erl | 14 +--
src/fabric/src/fabric_index_cleanup.erl | 35 +++++--
src/fabric/test/eunit/fabric_tests.erl | 143 ++++++++++++++++++++++++++++-
src/nouveau/src/nouveau_fabric_cleanup.erl | 14 +--
4 files changed, 170 insertions(+), 36 deletions(-)
diff --git a/src/dreyfus/src/dreyfus_fabric_cleanup.erl
b/src/dreyfus/src/dreyfus_fabric_cleanup.erl
index 0488211be..f01afc4f3 100644
--- a/src/dreyfus/src/dreyfus_fabric_cleanup.erl
+++ b/src/dreyfus/src/dreyfus_fabric_cleanup.erl
@@ -26,7 +26,7 @@ go(DbName) ->
erpc:send_request(Node, ?MODULE, go_local, [DbName, Dbs,
Sigs], Node, Acc)
end,
Reqs = maps:fold(Fun, erpc:reqids_new(), ByNode),
- recv(DbName, Reqs, fabric_util:abs_request_timeout());
+ fabric_index_cleanup:recv(?MODULE, DbName, Reqs,
fabric_util:abs_request_timeout());
Error ->
couch_log:error("~p : error fetching ddocs db:~p ~p", [?MODULE,
DbName, Error]),
Error
@@ -49,15 +49,3 @@ go_local(DbName, Dbs, #{} = Sigs) ->
error:database_does_not_exist ->
ok
end.
-
-recv(DbName, Reqs, Timeout) ->
- case erpc:receive_response(Reqs, Timeout, true) of
- {ok, _Lable, Reqs1} ->
- recv(DbName, Reqs1, Timeout);
- {Error, Label, Reqs1} ->
- ErrMsg = "~p : error cleaning dreyfus indexes db:~p req:~p
error:~p",
- couch_log:error(ErrMsg, [?MODULE, DbName, Label, Error]),
- recv(DbName, Reqs1, Timeout);
- no_request ->
- ok
- end.
diff --git a/src/fabric/src/fabric_index_cleanup.erl
b/src/fabric/src/fabric_index_cleanup.erl
index 13759ba1d..a329530ab 100644
--- a/src/fabric/src/fabric_index_cleanup.erl
+++ b/src/fabric/src/fabric_index_cleanup.erl
@@ -16,7 +16,8 @@
cleanup_all_nodes/0,
cleanup_all_nodes/1,
cleanup_this_node/0,
- cleanup_this_node/1
+ cleanup_this_node/1,
+ recv/4
]).
cleanup_all_nodes() ->
@@ -55,7 +56,7 @@ cleanup_indexes(DbName, Nodes) ->
Acc3
end,
Reqs = maps:fold(Fun, erpc:reqids_new(), ByNode),
- recv(DbName, Reqs, fabric_util:abs_request_timeout());
+ recv(?MODULE, DbName, Reqs, fabric_util:abs_request_timeout());
Error ->
couch_log:error("~p : error fetching ddocs db:~p ~p", [?MODULE,
DbName, Error]),
Error
@@ -68,14 +69,30 @@ send(Node, M, F, A, Reqs) ->
Label = {Node, M, F},
erpc:send_request(Node, M, F, A, Label, Reqs).
-recv(DbName, Reqs, Timeout) ->
- case erpc:receive_response(Reqs, Timeout, true) of
+% Receive responses for an erpc request collection built with
+% erpc:send_request/6. Used by this module and the dreyfus and nouveau cleanup
+% modules. Cleanup or timeout are best-effort we log them and keep going (a
+% node might be off for hardware replacement or something).
+%
+recv(Module, DbName, Reqs, Timeout) ->
+ try erpc:receive_response(Reqs, Timeout, true) of
{ok, _Label, Reqs1} ->
- recv(DbName, Reqs1, Timeout);
- {Error, Label, Reqs1} ->
- ErrMsg = "~p : error cleaning indexes db:~p req:~p error:~p",
- couch_log:error(ErrMsg, [?MODULE, DbName, Label, Error]),
- recv(DbName, Reqs1, Timeout);
+ recv(Module, DbName, Reqs1, Timeout);
+ {Res, Label, Reqs1} ->
+ log_error(Module, DbName, Label, Res),
+ recv(Module, DbName, Reqs1, Timeout);
no_request ->
ok
+ catch
+ error:{erpc, timeout} ->
+ Labels = [Label || {_ReqId, Label} <- erpc:reqids_to_list(Reqs)],
+ log_error(Module, DbName, Labels, timeout),
+ ok;
+ Class:{Reason, Label, Reqs1} when is_map(Reqs1) ->
+ log_error(Module, DbName, Label, {Class, Reason}),
+ recv(Module, DbName, Reqs1, Timeout)
end.
+
+log_error(Module, DbName, Label, Error) ->
+ ErrMsg = "~p : error cleaning indexes db:~p req:~p error:~p",
+ couch_log:error(ErrMsg, [Module, DbName, Label, Error]).
diff --git a/src/fabric/test/eunit/fabric_tests.erl
b/src/fabric/test/eunit/fabric_tests.erl
index daf018231..38c43e1d5 100644
--- a/src/fabric/test/eunit/fabric_tests.erl
+++ b/src/fabric/test/eunit/fabric_tests.erl
@@ -15,6 +15,7 @@
-include_lib("couch/include/couch_db.hrl").
-include_lib("couch/include/couch_eunit.hrl").
-include_lib("couch_mrview/include/couch_mrview.hrl").
+-include_lib("mem3/include/mem3.hrl").
cleanup_index_files_test_() ->
{
@@ -29,7 +30,9 @@ cleanup_index_files_test_() ->
?TDEF_FE(t_cleanup_index_file_after_ddoc_update),
?TDEF_FE(t_cleanup_index_file_after_ddoc_delete),
?TDEF_FE(t_cleanup_empty_view_checkpoints),
- ?TDEF_FE(t_cleanup_disallowed_language_checkpoints)
+ ?TDEF_FE(t_cleanup_disallowed_language_checkpoints),
+ ?TDEF_FE(t_cleanup_index_files_with_node_down),
+ ?TDEF_FE(t_cleanup_search_indexes_with_node_down)
]
}.
@@ -223,6 +226,60 @@ t_cleanup_disallowed_language_checkpoints({_, DbName}) ->
purges(DbName)
).
+t_cleanup_index_files_with_node_down({_, DbName}) ->
+ FakeNode = 'bogus@nohost',
+ mock_node_down(DbName, FakeNode),
+ meck:new(mem3_util, [passthrough]),
+ meck:expect(mem3_util, live_nodes, fun() -> meck:passthrough([]) ++
[FakeNode] end),
+ ?assertEqual(ok, fabric:cleanup_index_files_all_nodes(DbName)),
+ ErpcError = {error, {erpc, noconnection}},
+ lists:foreach(
+ fun({M, F}) ->
+ Label = {FakeNode, M, F},
+ Args = [fabric_index_cleanup, DbName, Label, ErpcError],
+ ?assert(meck:called(couch_log, error, ['_', Args]))
+ end,
+ [
+ {couch_mrview_cleanup, cleanup},
+ {dreyfus_fabric_cleanup, go_local},
+ {nouveau_fabric_cleanup, go_local}
+ ]
+ ).
+
+t_cleanup_search_indexes_with_node_down({_, DbName}) ->
+ FakeNode = 'bogus@nohost',
+ mock_node_down(DbName, FakeNode),
+ ErpcError = {error, {erpc, noconnection}},
+ ?assertEqual(ok, dreyfus_fabric_cleanup:go(DbName)),
+ Args1 = [dreyfus_fabric_cleanup, DbName, FakeNode, ErpcError],
+ ?assert(meck:called(couch_log, error, ['_', Args1])),
+ ?assertEqual(ok, nouveau_fabric_cleanup:go(DbName)),
+ Args2 = [nouveau_fabric_cleanup, DbName, FakeNode, ErpcError],
+ ?assert(meck:called(couch_log, error, ['_', Args2])).
+
+% Jump through hoops to pretend we have a node down. We mock DbName's shard map
+% to return an extra shard copy on a bogus down node.
+%
+mock_node_down(DbName, FakeNode) ->
+ DDocRes = fabric_util:get_design_doc_records(DbName),
+ meck:new(fabric_util, [passthrough]),
+ meck:expect(fabric_util, get_design_doc_records, fun(Db) ->
+ case Db =:= DbName of
+ true -> DDocRes;
+ false -> meck:passthrough([Db])
+ end
+ end),
+ meck:new(mem3, [passthrough]),
+ meck:expect(mem3, shards, fun(Db) ->
+ Shards = meck:passthrough([Db]),
+ case Db =:= DbName of
+ true -> Shards ++ [(hd(Shards))#shard{node = FakeNode}];
+ false -> Shards
+ end
+ end),
+ meck:new(couch_log, [passthrough]),
+ meck:expect(couch_log, error, 2, ok).
+
shard_names(DbName) ->
[mem3:name(S) || S <- mem3:local_shards(DbName)].
@@ -494,3 +551,87 @@ teardown_fabric(Ctx) ->
clear_shards_db() ->
ShardsDb = ?l2b(config:get("mem3", "shards_db", "_dbs")),
couch_server:delete(ShardsDb, [?ADMIN_CTX]).
+
+index_cleanup_recv_test_() ->
+ {
+ foreach,
+ fun() -> meck:expect(couch_log, error, 2, ok) end,
+ fun(_) -> meck:unload() end,
+ [
+ ?TDEF_FE(t_recv_no_requests),
+ ?TDEF_FE(t_recv_ok_responses),
+ ?TDEF_FE(t_recv_logs_non_ok_responses),
+ ?TDEF_FE(t_recv_handles_noconnection),
+ ?TDEF_FE(t_recv_handles_remote_exceptions),
+ ?TDEF_FE(t_recv_handles_timeout)
+ ]
+ }.
+
+t_recv_no_requests(_) ->
+ ?assertEqual(ok, recv(erpc:reqids_new(), 5000)),
+ ?assertEqual(0, meck:num_calls(couch_log, error, 2)).
+
+t_recv_ok_responses(_) ->
+ Reqs0 = erpc:reqids_new(),
+ Reqs1 = send_fun(node(), req1, fun() -> ok end, Reqs0),
+ Reqs2 = send_fun(node(), req2, fun() -> ok end, Reqs1),
+ ?assertEqual(ok, recv(Reqs2, 5000)),
+ ?assertEqual(0, meck:num_calls(couch_log, error, 2)).
+
+t_recv_logs_non_ok_responses(_) ->
+ Reqs1 = send_fun(node(), req1, fun() -> {error, potato} end,
erpc:reqids_new()),
+ ?assertEqual(ok, recv(Reqs1, 5000)),
+ ?assert(meck:called(couch_log, error, ['_', ['_', '_', req1, {error,
potato}]])).
+
+t_recv_handles_noconnection(_) ->
+ Self = self(),
+ Reqs0 = erpc:reqids_new(),
+ Reqs1 = erpc:send_request('bogus@totallybogus', erlang, node, [],
down_node, Reqs0),
+ Reqs2 = send_fun(
+ node(),
+ req2,
+ fun() ->
+ Self ! req2_ran,
+ ok
+ end,
+ Reqs1
+ ),
+ ?assertEqual(ok, recv(Reqs2, 5000)),
+ % Log the bogus one but keep going otherwise
+ NoConn = {error, {erpc, noconnection}},
+ ?assert(meck:called(couch_log, error, ['_', ['_', '_', down_node,
NoConn]])),
+ ?assertEqual(1, meck:num_calls(couch_log, error, 2)),
+ receive
+ req2_ran -> ok
+ end.
+
+t_recv_handles_remote_exceptions(_) ->
+ % A variety of failures on the other side: exits, errors and throws
+ Reqs0 = erpc:reqids_new(),
+ Reqs1 = send_fun(node(), err_req, fun() -> error(potato) end, Reqs0),
+ Reqs2 = send_fun(node(), throw_req, fun() -> throw(potato) end, Reqs1),
+ Reqs3 = send_fun(node(), exit_req, fun() -> exit(potato) end, Reqs2),
+ Reqs4 = send_fun(node(), ok_req, fun() -> ok end, Reqs3),
+ ?assertEqual(ok, recv(Reqs4, 5000)),
+ ?assert(
+ meck:called(couch_log, error, ['_', ['_', '_', err_req, {error,
{exception, potato, '_'}}]])
+ ),
+ ?assert(meck:called(couch_log, error, ['_', ['_', '_', throw_req, {throw,
potato}]])),
+ ?assert(
+ meck:called(couch_log, error, ['_', ['_', '_', exit_req, {exit,
{exception, potato}}]])
+ ),
+ ?assertEqual(3, meck:num_calls(couch_log, error, 2)).
+
+t_recv_handles_timeout(_) ->
+ Reqs1 = send_fun(node(), slow_req, fun() -> timer:sleep(10000) end,
erpc:reqids_new()),
+ ?assertEqual(ok, recv(Reqs1, 100)),
+ % Test the global timeout
+ ?assert(meck:called(couch_log, error, ['_', ['_', '_', [slow_req],
timeout]])).
+
+recv(Reqs, TimeoutMSec) ->
+ % Note: this is a standard erpc format see
https://www.erlang.org/doc/apps/kernel/erpc.html
+ Timeout = {abs, erlang:monotonic_time(millisecond) + TimeoutMSec},
+ fabric_index_cleanup:recv(?MODULE, <<"db">>, Reqs, Timeout).
+
+send_fun(Node, Label, Fun, Reqs) ->
+ erpc:send_request(Node, erlang, apply, [Fun, []], Label, Reqs).
diff --git a/src/nouveau/src/nouveau_fabric_cleanup.erl
b/src/nouveau/src/nouveau_fabric_cleanup.erl
index 75c2190b8..d67c6f9c9 100644
--- a/src/nouveau/src/nouveau_fabric_cleanup.erl
+++ b/src/nouveau/src/nouveau_fabric_cleanup.erl
@@ -26,7 +26,7 @@ go(DbName) ->
erpc:send_request(Node, ?MODULE, go_local, [DbName, Dbs,
Sigs], Node, Acc)
end,
Reqs = maps:fold(Fun, erpc:reqids_new(), ByNode),
- recv(DbName, Reqs, fabric_util:abs_request_timeout());
+ fabric_index_cleanup:recv(?MODULE, DbName, Reqs,
fabric_util:abs_request_timeout());
Error ->
couch_log:error("~p : error fetching ddocs db:~p ~p", [?MODULE,
DbName, Error]),
Error
@@ -51,15 +51,3 @@ go_local(DbName, Dbs, Sigs) ->
error:database_does_not_exist ->
ok
end.
-
-recv(DbName, Reqs, Timeout) ->
- case erpc:receive_response(Reqs, Timeout, true) of
- {ok, _Label, Reqs1} ->
- recv(DbName, Reqs1, Timeout);
- {Error, Label, Reqs1} ->
- ErrMsg = "~p : error cleaning nouveau indexes db:~p node: ~p
error:~p",
- couch_log:error(ErrMsg, [?MODULE, DbName, Label, Error]),
- recv(DbName, Reqs1, Timeout);
- no_request ->
- ok
- end.