summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Vatamaniuc <vatamane@apache.org>2019-02-05 19:09:16 -0500
committerNick Vatamaniuc <vatamane@apache.org>2019-02-15 16:42:43 -0500
commit363bb1e48468ffd3bd027c78d2f9a2a646ac2670 (patch)
tree89a9f6cddd34ae9594720cca3152980557aad766
parent57bde4c556f5970496d773cec6226cab21344e4d (diff)
downloadcouchdb-363bb1e48468ffd3bd027c78d2f9a2a646ac2670.tar.gz
Add API tests and fix a sneaky bug
The bug was that when jobs were reporting they were updating the ets table, they were also overwritting the refrence which was monitoring the job. To fix it we have make sure to preserve it but we can otherwise update everything else from using the new job record.
-rw-r--r--src/mem3/src/mem3_reshard.erl9
-rw-r--r--src/mem3/src/mem3_reshard_httpd.erl7
-rw-r--r--src/mem3/src/mem3_reshard_httpd_util.erl16
-rw-r--r--src/mem3/test/mem3_reshard_api_test.erl188
4 files changed, 213 insertions, 7 deletions
diff --git a/src/mem3/src/mem3_reshard.erl b/src/mem3/src/mem3_reshard.erl
index 07279f882..a3f2cf115 100644
--- a/src/mem3/src/mem3_reshard.erl
+++ b/src/mem3/src/mem3_reshard.erl
@@ -475,13 +475,13 @@ kill_job_int(#job{pid = undefined} = Job) ->
kill_job_int(#job{pid = Pid, ref = Ref} = Job) ->
couch_log:info("~p kill_job_int ~p", [?MODULE, jobfmt(Job)]),
+ demonitor(Ref, [flush]),
case erlang:is_process_alive(Pid) of
true ->
ok = mem3_reshard_job_sup:terminate_child(Pid);
false ->
ok
end,
- demonitor(Ref, [flush]),
Job1 = Job#job{pid = undefined, ref = undefined},
true = ets:insert(?MODULE, Job1),
Job1.
@@ -681,8 +681,11 @@ checkpoint_int(Job, State, From) ->
-spec report_int(#job{}, pid()) -> ok | not_found.
report_int(Job, From) ->
case ets:lookup(?MODULE, Job#job.id) of
- [#job{pid = From}] ->
- true = ets:insert(?MODULE, Job),
+ [#job{pid = From, ref = Ref}] ->
+ % We care over the reference used to monitor this job. The job
+ % record coming in from the job itself won't have and if we just
+ % ets:insert it we'd end up forgetting the old ref
+ true = ets:insert(?MODULE, Job#job{ref = Ref}),
ok;
_ ->
couch_log:error("~p reporting : couldn't find ~p", [?MODULE, Job]),
diff --git a/src/mem3/src/mem3_reshard_httpd.erl b/src/mem3/src/mem3_reshard_httpd.erl
index e840cb1d0..c34bea13b 100644
--- a/src/mem3/src/mem3_reshard_httpd.erl
+++ b/src/mem3/src/mem3_reshard_httpd.erl
@@ -176,7 +176,12 @@ handle_reshard_req(#httpd{method='GET',
case mem3_reshard_httpd_util:get_job(JobId) of
{ok, {Props}} ->
JobState = couch_util:get_value(job_state, Props),
- send_json(Req, 200, {[{state, JobState}]});
+ {SIProps} = couch_util:get_value(state_info, Props),
+ Reason = case couch_util:get_value(reason, SIProps) of
+ undefined -> null;
+ Val -> couch_util:to_binary(Val)
+ end,
+ send_json(Req, 200, {[{state, JobState}, {reason, Reason}]});
{error, not_found} ->
throw(not_found)
end;
diff --git a/src/mem3/src/mem3_reshard_httpd_util.erl b/src/mem3/src/mem3_reshard_httpd_util.erl
index 684135a96..b7e8a482b 100644
--- a/src/mem3/src/mem3_reshard_httpd_util.erl
+++ b/src/mem3/src/mem3_reshard_httpd_util.erl
@@ -53,7 +53,10 @@ validate_node(Node0) when is_binary(Node0) ->
catch
error:badarg ->
throw({bad_request, <<"`node` is not a valid node name">>})
- end.
+ end;
+
+validate_node(_Node) ->
+ throw({bad_request, <<"Invalid `node`">>}).
validate_shard(undefined) ->
@@ -65,7 +68,10 @@ validate_shard(Shard) when is_binary(Shard) ->
Shard;
_ ->
throw({bad_request, <<"`shard` is invalid">>})
- end.
+ end;
+
+validate_shard(_Shard) ->
+ throw({bad_request, <<"Invalid `shard`">>}).
validate_db(undefined) ->
@@ -78,7 +84,11 @@ validate_db(DbName) when is_binary(DbName) ->
catch
_:_ ->
throw({bad_request, <<"Invalid `db`">>})
- end.
+ end;
+
+validate_db(_bName) ->
+ throw({bad_request, <<"Invalid `db`">>}).
+
validate_range(undefined) ->
diff --git a/src/mem3/test/mem3_reshard_api_test.erl b/src/mem3/test/mem3_reshard_api_test.erl
new file mode 100644
index 000000000..60f9d6524
--- /dev/null
+++ b/src/mem3/test/mem3_reshard_api_test.erl
@@ -0,0 +1,188 @@
+% 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(mem3_reshard_api_test).
+
+
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("couch/include/couch_db.hrl").
+
+
+-define(USER, "mem3_reshard_api_test_admin").
+-define(PASS, "pass").
+-define(AUTH, {basic_auth, {?USER, ?PASS}}).
+-define(JSON, {"Content-Type", "application/json"}).
+-define(DB1, "mem3_reshard_api_test_db1").
+-define(RESHARD, "_reshard/").
+-define(RESHARD_JOBS, "_reshard/jobs/").
+
+
+setup() ->
+ Hashed = couch_passwords:hash_admin_password(?PASS),
+ ok = config:set("admins", ?USER, ?b2l(Hashed), _Persist=false),
+ ok = config:set("mem3_reshard", "store_state", "false", false),
+ Addr = config:get("chttpd", "bind_address", "127.0.0.1"),
+ Port = mochiweb_socket_server:get(chttpd, port),
+ Url = lists:concat(["http://", Addr, ":", Port, "/"]),
+ Db1Url = Url ++ ?DB1,
+ create_db(Db1Url ++ "?q=1&n=1"),
+ Url.
+
+
+teardown(Url) ->
+ mem3_reshard:reset_state(),
+ delete_db(Url ++ ?DB1),
+ ok = config:delete("admins", ?USER, _Persist=false).
+
+
+start_couch() ->
+ test_util:start_couch(?CONFIG_CHAIN, [mem3, chttpd]).
+
+
+stop_couch(Ctx) ->
+ test_util:stop_couch(Ctx).
+
+
+mem3_reshard_api_test_() ->
+ {
+ "mem3 shard split api tests",
+ {
+ setup,
+ fun start_couch/0, fun stop_couch/1,
+ {
+ foreach,
+ fun setup/0, fun teardown/1,
+ [
+ fun reshard_basics/1,
+ fun reshard_create_job_basic/1
+ ]
+ }
+ }
+ }.
+
+
+reshard_basics(Url) ->
+ ?_test(begin
+ % GET /_reshard
+ {C1, R1} = req(get, Url ++ ?RESHARD),
+ ?assertEqual(200, C1),
+ ?assertMatch(#{
+ <<"state">> := <<"running">>,
+ <<"state_reason">> := null,
+ <<"completed">> := 0,
+ <<"failed">> := 0,
+ <<"running">> := 0,
+ <<"stopped">> := 0,
+ <<"total">> := 0
+ }, R1),
+ % GET _reshard/state
+ {C2, R2} = req(get, Url ++ ?RESHARD ++ "/state"),
+ ?assertEqual(200, C2),
+ ?assertMatch(#{
+ <<"state">> := <<"running">>,
+ <<"reason">> := null
+ }, R2),
+ % GET _reshard/jobs
+ {C3, R3} = req(get, Url ++ ?RESHARD_JOBS),
+ ?assertEqual(200, C3),
+ ?assertMatch(#{
+ <<"jobs">> := [],
+ <<"offset">> := 0,
+ <<"total_rows">> := 0
+ }, R3),
+ % Some invalid paths and methods
+ ?assertMatch({404, _}, req(get, Url ++ ?RESHARD ++ "/invalidpath")),
+ ?assertMatch({405, _}, req(put, Url ++ ?RESHARD, #{dont => thinkso})),
+ ?assertMatch({405, _}, req(post, Url ++ ?RESHARD, #{notgonna => happen}))
+ end).
+
+
+reshard_create_job_basic(Url) ->
+ ?_test(begin
+ % POST /_reshard/jobs
+ Body = #{type => split, db => <<?DB1>>},
+ {C1, R1} = req(post, Url ++ ?RESHARD_JOBS, Body),
+ ?assertEqual(201, C1),
+ ?assertMatch([#{<<"ok">> := true, <<"id">> := J, <<"shard">> := S}]
+ when is_binary(J) andalso is_binary(S), R1),
+ [#{<<"id">> := Id, <<"shard">> := Shard}] = R1,
+ % GET /_reshard/jobs
+ {C2, R2} = req(get, Url ++ ?RESHARD_JOBS),
+ ?assertEqual(200, C2),
+ ?assertMatch(#{
+ <<"jobs">> := [#{<<"id">> := Id, <<"type">> := <<"split">>}],
+ <<"offset">> := 0,
+ <<"total_rows">> := 1
+ }, R2),
+ % GET /_reshard/job/$jobid
+ {C3, R3} = req(get, Url ++ ?RESHARD_JOBS ++ ?b2l(Id)),
+ ?assertEqual(200, C3),
+ ThisNode = atom_to_binary(node(), utf8),
+ ?assertMatch(#{<<"id">> := Id}, R3),
+ ?assertMatch(#{<<"type">> := <<"split">>}, R3),
+ ?assertMatch(#{<<"source">> := Shard}, R3),
+ ?assertMatch(#{<<"history">> := History} when length(History) > 1, R3),
+ ?assertMatch(#{<<"node">> := ThisNode}, R3),
+ ?assertMatch(#{<<"split_state">> := SSt} when is_binary(SSt), R3),
+ ?assertMatch(#{<<"job_state">> := JSt} when is_binary(JSt), R3),
+ ?assertMatch(#{<<"state_info">> := #{}}, R3),
+ ?assertMatch(#{<<"targets">> := Targets} when length(Targets) == 2, R3),
+ % GET /_reshard/job/$jobid/state
+ {C4, R4} = req(get, Url ++ ?RESHARD_JOBS ++ ?b2l(Id) ++ "/state"),
+ ?assertEqual(200, C4),
+ ?assertMatch(#{<<"state">> := JSt} when is_binary(JSt), R4),
+ ?assertMatch(#{<<"reason">> := Reason} when is_binary(Reason)
+ orelse Reason =:= null, R4),
+ % GET /_reshard
+ {C5, R5} = req(get, Url ++ ?RESHARD),
+ ?assertEqual(200, C5),
+ ?assertMatch(#{
+ <<"state">> := <<"running">>,
+ <<"state_reason">> := null,
+ <<"total">> := 1
+ }, R5),
+ % DELETE /_reshard/jobs/$jobid
+ DelResult = req(delete, Url ++ ?RESHARD_JOBS ++ ?b2l(Id)),
+ ?assertMatch({200, #{<<"ok">> := true}}, DelResult),
+ % GET _reshard/jobs
+ {C6, R6} = req(get, Url ++ ?RESHARD_JOBS),
+ ?assertEqual(200, C6),
+ ?assertMatch(#{
+ <<"jobs">> := [],
+ <<"offset">> := 0,
+ <<"total_rows">> := 0
+ }, R6)
+ end).
+
+
+create_db(Url) ->
+ {ok, Status, _, _} = test_request:put(Url, [?JSON, ?AUTH], "{}"),
+ ?assert(Status =:= 201 orelse Status =:= 202).
+
+
+delete_db(Url) ->
+ {ok, 200, _, _} = test_request:delete(Url, [?AUTH]).
+
+
+req(Method, Url) ->
+ Headers = [?AUTH],
+ {ok, Code, _, Res} = test_request:request(Method, Url, Headers),
+ {Code, jiffy:decode(Res, [return_maps])}.
+
+
+req(Method, Url, #{} = Body) ->
+ req(Method, Url, jiffy:encode(Body));
+
+req(Method, Url, Body) ->
+ Headers = [?JSON, ?AUTH],
+ {ok, Code, _, Res} = test_request:request(Method, Url, Headers, Body),
+ {Code, jiffy:decode(Res, [return_maps])}.