Merge remote-tracking branch 'banjiewen/stale-stable-update'
diff --git a/priv/couch_js/help.h b/priv/couch_js/help.h
index 7601e9d..e6afaa8 100644
--- a/priv/couch_js/help.h
+++ b/priv/couch_js/help.h
@@ -54,6 +54,7 @@
" most SIZE bytes of memory to be allocated\n"
" -u FILE path to a .uri file containing the address\n"
" (or addresses) of one or more servers\n"
+ " --no-eval Disable runtime code evaluation\n"
"\n"
"Report bugs at <%s>.\n";
diff --git a/priv/couch_js/main.c b/priv/couch_js/main.c
index 50d072c..dabeb19 100644
--- a/priv/couch_js/main.c
+++ b/priv/couch_js/main.c
@@ -349,6 +349,26 @@
};
+static JSBool
+csp_allows(JSContext* cx)
+{
+ couch_args *args = (couch_args*)JS_GetContextPrivate(cx);
+ if(args->no_eval) {
+ return JS_FALSE;
+ } else {
+ return JS_TRUE;
+ }
+}
+
+
+static JSSecurityCallbacks security_callbacks = {
+ NULL,
+ NULL,
+ NULL,
+ csp_allows
+};
+
+
int
main(int argc, const char* argv[])
{
@@ -382,7 +402,8 @@
JS_SetOptions(cx, JSOPTION_TYPE_INFERENCE);
#endif
JS_SetContextPrivate(cx, args);
-
+ JS_SetRuntimeSecurityCallbacks(rt, &security_callbacks);
+
SETUP_REQUEST(cx);
global = JS_NewCompartmentAndGlobalObject(cx, &global_class, NULL);
diff --git a/priv/couch_js/utf8.c b/priv/couch_js/utf8.c
index fcafff6..4cdb9c2 100644
--- a/priv/couch_js/utf8.c
+++ b/priv/couch_js/utf8.c
@@ -83,10 +83,10 @@
{
// Invalid second half of surrogate pair
v = (uint32) 0xFFFD;
+ // Undo our character advancement
+ src--;
+ srclen++;
}
- // Undo our character advancement
- src--;
- srclen++;
}
else
{
diff --git a/priv/couch_js/util.c b/priv/couch_js/util.c
index 2f2a2a7..7919025 100644
--- a/priv/couch_js/util.c
+++ b/priv/couch_js/util.c
@@ -98,6 +98,8 @@
}
} else if(strcmp("-u", argv[i]) == 0) {
args->uri_file = argv[++i];
+ } else if(strcmp("--no-eval", argv[i]) == 0) {
+ args->no_eval = 1;
} else if(strcmp("--", argv[i]) == 0) {
i++;
break;
diff --git a/priv/couch_js/util.h b/priv/couch_js/util.h
index 3c71f69..062469d 100644
--- a/priv/couch_js/util.h
+++ b/priv/couch_js/util.h
@@ -16,6 +16,7 @@
#include <jsapi.h>
typedef struct {
+ int no_eval;
int use_http;
int use_test_funs;
int stack_size;
diff --git a/rebar.config b/rebar.config
deleted file mode 100644
index 43e2924..0000000
--- a/rebar.config
+++ /dev/null
@@ -1,2 +0,0 @@
-{erl_opts, [{platform_define, "win32", 'WINDOWS'}]}.
-{eunit_compile_opts, [{platform_define, "win32", 'WINDOWS'}]}.
diff --git a/rebar.config.script b/rebar.config.script
index 7d803b9..8d44099 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -36,7 +36,7 @@
false ->
string:strip(os:cmd("git describe --always"), right, $\n);
Version0 ->
- Version0
+ string:strip(Version0, right)
end,
CouchConfig = case filelib:is_file(os:getenv("COUCHDB_CONFIG")) of
@@ -132,7 +132,14 @@
BaseSpecs
end,
-AddConfig = [{port_specs, PortSpecs}].
+AddConfig = [
+ {port_specs, PortSpecs},
+ {erl_opts, [
+ {platform_define, "win32", 'WINDOWS'},
+ {d, 'COUCHDB_VERSION', Version}
+ ]},
+ {eunit_compile_opts, [{platform_define, "win32", 'WINDOWS'}]}
+].
lists:foldl(fun({K, V}, CfgAcc) ->
lists:keystore(K, 1, CfgAcc, {K, V})
diff --git a/src/couch_auth_cache.erl b/src/couch_auth_cache.erl
index 32d706d..9b00a9d 100644
--- a/src/couch_auth_cache.erl
+++ b/src/couch_auth_cache.erl
@@ -12,7 +12,7 @@
-module(couch_auth_cache).
-behaviour(gen_server).
--vsn(2).
+-vsn(3).
-behaviour(config_listener).
% public API
@@ -33,6 +33,8 @@
-define(BY_USER, auth_by_user_ets).
-define(BY_ATIME, auth_by_atime_ets).
+-define(RELISTEN_DELAY, 5000).
+
-record(state, {
max_cache_size = 0,
cache_size = 0,
@@ -242,7 +244,11 @@
{stop, Reason, State};
NewClosed ->
{noreply, reinit_cache(State#state{closed = NewClosed})}
- end.
+ end;
+handle_info(restart_config_listener, State) ->
+ ok = config:listen_for_changes(?MODULE, nil),
+ {noreply, State}.
+
terminate(_Reason, #state{event_listener = Listener}) ->
@@ -265,12 +271,10 @@
handle_config_change(_, _, _, _, _) ->
{ok, nil}.
-handle_config_terminate(_, stop, _) -> ok;
-handle_config_terminate(_, _, _) ->
- spawn(fun() ->
- timer:sleep(5000),
- config:listen_for_changes(?MODULE, nil)
- end).
+handle_config_terminate(_, stop, _) ->
+ ok;
+handle_config_terminate(_Server, _Reason, _State) ->
+ erlang:send_after(?RELISTEN_DELAY, whereis(?MODULE), restart_config_listener).
clear_cache(State) ->
exec_if_auth_db(fun(AuthDb) -> catch couch_db:close(AuthDb) end),
diff --git a/src/couch_compaction_daemon.erl b/src/couch_compaction_daemon.erl
index 7888eb4..8f95eb2 100644
--- a/src/couch_compaction_daemon.erl
+++ b/src/couch_compaction_daemon.erl
@@ -12,6 +12,7 @@
-module(couch_compaction_daemon).
-behaviour(gen_server).
+-vsn(1).
-behaviour(config_listener).
% public API
@@ -29,6 +30,8 @@
-define(CONFIG_ETS, couch_compaction_daemon_config).
+-define(RELISTEN_DELAY, 5000).
+
-record(state, {
loop_pid,
in_progress = []
@@ -98,7 +101,10 @@
handle_info({'EXIT', Pid, Reason}, #state{loop_pid = Pid} = State) ->
- {stop, {compaction_loop_died, Reason}, State}.
+ {stop, {compaction_loop_died, Reason}, State};
+handle_info(restart_config_listener, State) ->
+ ok = config:listen_for_changes(?MODULE, nil),
+ {noreply, State}.
terminate(_Reason, _State) ->
@@ -114,12 +120,10 @@
handle_config_change(_, _, _, _, _) ->
{ok, nil}.
-handle_config_terminate(_, stop, _) -> ok;
-handle_config_terminate(_, _, _) ->
- spawn(fun() ->
- timer:sleep(5000),
- config:listen_for_changes(?MODULE, nil)
- end).
+handle_config_terminate(_, stop, _) ->
+ ok;
+handle_config_terminate(_Server, _Reason, _State) ->
+ erlang:send_after(?RELISTEN_DELAY, whereis(?MODULE), restart_config_listener).
compact_loop(Parent) ->
{ok, _} = couch_server:all_databases(
diff --git a/src/couch_db.erl b/src/couch_db.erl
index 8260a5c..26c6ae7 100644
--- a/src/couch_db.erl
+++ b/src/couch_db.erl
@@ -1519,13 +1519,17 @@
normalize_dbname(DbName) when is_list(DbName) ->
normalize_dbname(list_to_binary(DbName));
normalize_dbname(DbName) when is_binary(DbName) ->
+ mem3:dbname(maybe_remove_extension(DbName)).
+
+maybe_remove_extension(DbName) ->
case filename:extension(DbName) of
<<".couch">> ->
- mem3:dbname(filename:rootname(DbName));
+ filename:rootname(DbName);
_ ->
- mem3:dbname(DbName)
+ DbName
end.
+
-spec dbname_suffix(list() | binary()) -> binary().
dbname_suffix(DbName) ->
@@ -1536,15 +1540,12 @@
validate_dbname(?l2b(DbName));
validate_dbname(DbName) when is_binary(DbName) ->
Normalized = normalize_dbname(DbName),
- case couch_db_plugin:validate_dbname(DbName, Normalized) of
- true ->
- ok;
- false ->
- validate_dbname_int(DbName, Normalized)
- end.
+ couch_db_plugin:validate_dbname(
+ DbName, Normalized, fun validate_dbname_int/2).
validate_dbname_int(DbName, Normalized) when is_binary(DbName) ->
- case re:run(DbName, ?DBNAME_REGEX, [{capture,none}, dollar_endonly]) of
+ DbNoExt = maybe_remove_extension(DbName),
+ case re:run(DbNoExt, ?DBNAME_REGEX, [{capture,none}, dollar_endonly]) of
match ->
ok;
nomatch ->
@@ -1563,23 +1564,23 @@
-include_lib("eunit/include/eunit.hrl").
setup() ->
- ok = meck:new(couch_db_plugin, [passthrough]),
- ok = meck:expect(couch_db_plugin, validate_dbname, fun(_, _) -> false end),
+ ok = meck:new(couch_epi, [passthrough]),
+ ok = meck:expect(couch_epi, decide, fun(_, _, _, _, _) -> no_decision end),
ok.
teardown(_) ->
- (catch meck:unload(couch_db_plugin)).
+ (catch meck:unload(couch_epi)).
validate_dbname_success_test_() ->
Cases =
- generate_cases_with_shards("long/co$mplex-/path+/_something")
+ generate_cases_with_shards("long/co$mplex-/path+/something")
++ generate_cases_with_shards("something")
++ lists:append(
[generate_cases_with_shards(?b2l(SystemDb))
|| SystemDb <- ?SYSTEM_DATABASES]),
{
foreach, fun setup/0, fun teardown/1,
- [{test_name(A), fun() -> should_pass_validate_dbname(A) end} || {_, A} <- Cases]
+ [should_pass_validate_dbname(A) || {_, A} <- Cases]
}.
validate_dbname_fail_test_() ->
@@ -1589,7 +1590,7 @@
++ generate_cases_with_shards("long/co$mplex-/path+/some.thing"),
{
foreach, fun setup/0, fun teardown/1,
- [{test_name(A), fun() -> should_fail_validate_dbname(A) end} || {_, A} <- Cases]
+ [should_fail_validate_dbname(A) || {_, A} <- Cases]
}.
normalize_dbname_test_() ->
diff --git a/src/couch_db_plugin.erl b/src/couch_db_plugin.erl
index 1a6d2ea..774e9e0 100644
--- a/src/couch_db_plugin.erl
+++ b/src/couch_db_plugin.erl
@@ -13,7 +13,7 @@
-module(couch_db_plugin).
-export([
- validate_dbname/2,
+ validate_dbname/3,
before_doc_update/2,
after_doc_read/2,
validate_docid/1,
@@ -29,10 +29,8 @@
%% API Function Definitions
%% ------------------------------------------------------------------
-validate_dbname(DbName, Normalized) ->
- Handle = couch_epi:get_handle(?SERVICE_ID),
- %% callbacks return true only if it specifically allow the given Id
- couch_epi:any(Handle, ?SERVICE_ID, validate_dbname, [DbName, Normalized], []).
+validate_dbname(DbName, Normalized, Default) ->
+ maybe_handle(validate_dbname, [DbName, Normalized], Default).
before_doc_update(#db{before_doc_update = Fun} = Db, Doc0) ->
case with_pipe(before_doc_update, [Doc0, Db]) of
@@ -70,3 +68,14 @@
do_apply(Func, Args, Opts) ->
Handle = couch_epi:get_handle(?SERVICE_ID),
couch_epi:apply(Handle, ?SERVICE_ID, Func, Args, Opts).
+
+maybe_handle(Func, Args, Default) ->
+ Handle = couch_epi:get_handle(?SERVICE_ID),
+ case couch_epi:decide(Handle, ?SERVICE_ID, Func, Args, []) of
+ no_decision when is_function(Default) ->
+ apply(Default, Args);
+ no_decision ->
+ Default;
+ {decided, Result} ->
+ Result
+ end.
diff --git a/src/couch_doc.erl b/src/couch_doc.erl
index 11063d9..af14038 100644
--- a/src/couch_doc.erl
+++ b/src/couch_doc.erl
@@ -169,6 +169,10 @@
validate_docid(<<"">>) ->
throw({illegal_docid, <<"Document id must not be empty">>});
+validate_docid(<<"_design/">>) ->
+ throw({illegal_docid, <<"Illegal document id `_design/`">>});
+validate_docid(<<"_local/">>) ->
+ throw({illegal_docid, <<"Illegal document id `_local/`">>});
validate_docid(Id) when is_binary(Id) ->
case couch_util:validate_utf8(Id) of
false -> throw({illegal_docid, <<"Document id must be valid UTF-8">>});
diff --git a/src/couch_external_manager.erl b/src/couch_external_manager.erl
index 1aeb39a..f131342 100644
--- a/src/couch_external_manager.erl
+++ b/src/couch_external_manager.erl
@@ -12,7 +12,7 @@
-module(couch_external_manager).
-behaviour(gen_server).
--vsn(2).
+-vsn(3).
-behaviour(config_listener).
-export([start_link/0, execute/2]).
@@ -23,6 +23,8 @@
-include_lib("couch/include/couch_db.hrl").
+-define(RELISTEN_DELAY, 5000).
+
start_link() ->
gen_server:start_link({local, couch_external_manager},
couch_external_manager, [], []).
@@ -41,12 +43,11 @@
handle_config_change(_, _, _, _, _) ->
{ok, nil}.
-handle_config_terminate(_, stop, _) -> ok;
-handle_config_terminate(_, _, _) ->
- spawn(fun() ->
- timer:sleep(5000),
- config:listen_for_changes(?MODULE, nil)
- end).
+handle_config_terminate(_, stop, _) ->
+ ok;
+handle_config_terminate(_Server, _Reason, _State) ->
+ erlang:send_after(?RELISTEN_DELAY, whereis(?MODULE), restart_config_listener).
+
% gen_server API
@@ -108,7 +109,12 @@
% Remove Pid from the handlers table so we don't try closing
% it a second time in terminate/2.
ets:match_delete(Handlers, {'_', Pid}),
- {stop, normal, Handlers}.
+ {stop, normal, Handlers};
+
+handle_info(restart_config_listener, State) ->
+ ok = config:listen_for_changes(?MODULE, nil),
+ {noreply, State}.
+
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
diff --git a/src/couch_external_server.erl b/src/couch_external_server.erl
index ff2e185..e2a5022 100644
--- a/src/couch_external_server.erl
+++ b/src/couch_external_server.erl
@@ -12,17 +12,16 @@
-module(couch_external_server).
-behaviour(gen_server).
--vsn(2).
--behaviour(config_listener).
+-vsn(3).
-export([start_link/2, stop/1, execute/2]).
-export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2, code_change/3]).
-% config_listener api
--export([handle_config_change/5, handle_config_terminate/3]).
-
-include_lib("couch/include/couch_db.hrl").
+-define(RELISTEN_DELAY, 5000).
+-define(CONFIG_SUBSCRIPTION, [{"couchdb", "os_process_timeout"}]).
+
% External API
start_link(Name, Command) ->
@@ -40,47 +39,52 @@
init([Name, Command]) ->
couch_log:info("EXTERNAL: Starting process for: ~s", [Name]),
couch_log:info("COMMAND: ~s", [Command]),
+ ok = config:subscribe_for_changes(?CONFIG_SUBSCRIPTION),
process_flag(trap_exit, true),
Timeout = list_to_integer(config:get("couchdb", "os_process_timeout",
"5000")),
{ok, Pid} = couch_os_process:start_link(Command, [{timeout, Timeout}]),
- ok = config:listen_for_changes(?MODULE, Pid),
- {ok, {Name, Command, Pid}}.
+ {ok, {Name, Command, Pid, whereis(config_event)}}.
-terminate(_Reason, {_Name, _Command, Pid}) ->
+terminate(_Reason, {_Name, _Command, Pid, _}) ->
couch_os_process:stop(Pid),
ok.
-handle_call({execute, JsonReq}, _From, {Name, Command, Pid}) ->
+handle_call({execute, JsonReq}, _From, {Name, Command, Pid, _}) ->
{reply, couch_os_process:prompt(Pid, JsonReq), {Name, Command, Pid}}.
handle_info({'EXIT', _Pid, normal}, State) ->
{noreply, State};
-handle_info({'EXIT', Pid, Reason}, {Name, Command, Pid}) ->
+handle_info({'EXIT', Pid, Reason}, {Name, Command, Pid, _}) ->
couch_log:info("EXTERNAL: Process for ~s exiting. (reason: ~w)",
[Name, Reason]),
- {stop, Reason, {Name, Command, Pid}}.
+ {stop, Reason, {Name, Command, Pid}};
+handle_info({config_change, "couchdb", "os_process_timeout", NewTimeout, _},
+ {_Name, _Command, Pid, _} = State) ->
+ couch_os_process:set_timeout(Pid, list_to_integer(NewTimeout)),
+ {noreply, State};
+handle_info({gen_event_EXIT, _Handler, _Reason}, State) ->
+ erlang:send_after(?RELISTEN_DELAY, self(), restart_config_listener),
+ {noreply, State};
+handle_info({'EXIT', Pid, _Reason}, {_, _, _, Pid} = State) ->
+ erlang:send_after(?RELISTEN_DELAY, self(), restart_config_listener),
+ {noreply, State};
+handle_info(restart_config_listener, {Name, Command, Pid, _} = State) ->
+ case whereis(config_event) of
+ undefined ->
+ erlang:send_after(?RELISTEN_DELAY, self(), restart_config_listener),
+ {noreply, State};
+ EventMgr ->
+ ok = config:subscribe_for_changes(?CONFIG_SUBSCRIPTION),
+ {noreply, {Name, Command, Pid, EventMgr}}
+ end.
-handle_cast(stop, {Name, Command, Pid}) ->
+handle_cast(stop, {Name, _Command, Pid, _} = State) ->
couch_log:info("EXTERNAL: Shutting down ~s", [Name]),
exit(Pid, normal),
- {stop, normal, {Name, Command, Pid}};
+ {stop, normal, State};
handle_cast(_Whatever, State) ->
{noreply, State}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
-
-
-handle_config_change("couchdb", "os_process_timeout", NewTimeout, _, Pid) ->
- couch_os_process:set_timeout(Pid, list_to_integer(NewTimeout)),
- {ok, Pid};
-handle_config_change(_, _, _, _, Pid) ->
- {ok, Pid}.
-
-handle_config_terminate(_, stop, _) -> ok;
-handle_config_terminate(_, _, _) ->
- spawn(fun() ->
- timer:sleep(5000),
- config:listen_for_changes(?MODULE, nil)
- end).
diff --git a/src/couch_file.erl b/src/couch_file.erl
index 8346b02..e7d9413 100644
--- a/src/couch_file.erl
+++ b/src/couch_file.erl
@@ -22,7 +22,12 @@
-define(SIZE_BLOCK, 16#1000). % 4 KiB
-define(READ_AHEAD, 2 * ?SIZE_BLOCK).
-define(IS_OLD_STATE(S), tuple_size(S) /= tuple_size(#file{})).
+-define(PREFIX_SIZE, 5).
+-define(DEFAULT_READ_COUNT, 1024).
+-type block_id() :: non_neg_integer().
+-type location() :: non_neg_integer().
+-type header_size() :: non_neg_integer().
-record(file, {
fd,
@@ -524,34 +529,83 @@
{stop, Reason, Fd}.
-find_header(_Fd, -1) ->
- no_valid_header;
find_header(Fd, Block) ->
case (catch load_header(Fd, Block)) of
{ok, Bin} ->
{ok, Bin};
_Error ->
- find_header(Fd, Block -1)
+ ReadCount = config:get_integer(
+ "couchdb", "find_header_read_count", ?DEFAULT_READ_COUNT),
+ find_header(Fd, Block -1, ReadCount)
end.
load_header(Fd, Block) ->
{ok, <<1, HeaderLen:32/integer, RestBlock/binary>>} =
file:pread(Fd, Block * ?SIZE_BLOCK, ?SIZE_BLOCK),
- TotalBytes = calculate_total_read_len(5, HeaderLen),
- case TotalBytes > byte_size(RestBlock) of
- false ->
- <<RawBin:TotalBytes/binary, _/binary>> = RestBlock;
- true ->
- {ok, Missing} = file:pread(
- Fd, (Block * ?SIZE_BLOCK) + 5 + byte_size(RestBlock),
- TotalBytes - byte_size(RestBlock)),
- RawBin = <<RestBlock/binary, Missing/binary>>
+ load_header(Fd, Block * ?SIZE_BLOCK, HeaderLen, RestBlock).
+
+load_header(Fd, Pos, HeaderLen) ->
+ load_header(Fd, Pos, HeaderLen, <<>>).
+
+load_header(Fd, Pos, HeaderLen, RestBlock) ->
+ TotalBytes = calculate_total_read_len(?PREFIX_SIZE, HeaderLen),
+ RawBin = case TotalBytes =< byte_size(RestBlock) of
+ true ->
+ <<RawBin0:TotalBytes/binary, _/binary>> = RestBlock,
+ RawBin0;
+ false ->
+ ReadStart = Pos + ?PREFIX_SIZE + byte_size(RestBlock),
+ ReadLen = TotalBytes - byte_size(RestBlock),
+ {ok, Missing} = file:pread(Fd, ReadStart, ReadLen),
+ <<RestBlock/binary, Missing/binary>>
end,
<<Md5Sig:16/binary, HeaderBin/binary>> =
- iolist_to_binary(remove_block_prefixes(5, RawBin)),
+ iolist_to_binary(remove_block_prefixes(?PREFIX_SIZE, RawBin)),
Md5Sig = couch_crypto:hash(md5, HeaderBin),
{ok, HeaderBin}.
+
+%% Read multiple block locations using a single file:pread/2.
+-spec find_header(file:fd(), block_id(), non_neg_integer()) ->
+ {ok, binary()} | no_valid_header.
+find_header(_Fd, Block, _ReadCount) when Block < 0 ->
+ no_valid_header;
+find_header(Fd, Block, ReadCount) ->
+ FirstBlock = max(0, Block - ReadCount + 1),
+ BlockLocations = [?SIZE_BLOCK*B || B <- lists:seq(FirstBlock, Block)],
+ {ok, DataL} = file:pread(Fd, [{L, ?PREFIX_SIZE} || L <- BlockLocations]),
+ %% Since BlockLocations are ordered from oldest to newest, we rely
+ %% on lists:foldl/3 to reverse the order, making HeaderLocations
+ %% correctly ordered from newest to oldest.
+ HeaderLocations = lists:foldl(fun
+ ({Loc, <<1, HeaderSize:32/integer>>}, Acc) ->
+ [{Loc, HeaderSize} | Acc];
+ (_, Acc) ->
+ Acc
+ end, [], lists:zip(BlockLocations, DataL)),
+ case find_newest_header(Fd, HeaderLocations) of
+ {ok, _Location, HeaderBin} ->
+ {ok, HeaderBin};
+ _ ->
+ ok = file:advise(
+ Fd, hd(BlockLocations), ReadCount * ?SIZE_BLOCK, dont_need),
+ NextBlock = hd(BlockLocations) div ?SIZE_BLOCK - 1,
+ find_header(Fd, NextBlock, ReadCount)
+ end.
+
+-spec find_newest_header(file:fd(), [{location(), header_size()}]) ->
+ {ok, location(), binary()} | not_found.
+find_newest_header(_Fd, []) ->
+ not_found;
+find_newest_header(Fd, [{Location, Size} | LocationSizes]) ->
+ case (catch load_header(Fd, Location, Size)) of
+ {ok, HeaderBin} ->
+ {ok, Location, HeaderBin};
+ _Error ->
+ find_newest_header(Fd, LocationSizes)
+ end.
+
+
maybe_read_more_iolist(Buffer, DataSize, _, _)
when DataSize =< byte_size(Buffer) ->
<<Data:DataSize/binary, _/binary>> = Buffer,
diff --git a/src/couch_httpd.erl b/src/couch_httpd.erl
index 2dd6ed2..7c4e573 100644
--- a/src/couch_httpd.erl
+++ b/src/couch_httpd.erl
@@ -170,7 +170,7 @@
AuthHandlers = lists:map(
fun(A) -> {auth_handler_name(A), make_arity_1_fun(A)} end, AuthenticationSrcs),
AuthenticationFuns = AuthHandlers ++ [
- {<<"local">>, fun couch_httpd_auth:party_mode_handler/1} %% should be last
+ fun couch_httpd_auth:party_mode_handler/1 %% must be last
],
ok = application:set_env(couch, auth_handlers, AuthenticationFuns).
diff --git a/src/couch_httpd_auth.erl b/src/couch_httpd_auth.erl
index 15d3ac6..ec7ede1 100644
--- a/src/couch_httpd_auth.erl
+++ b/src/couch_httpd_auth.erl
@@ -91,7 +91,7 @@
case AuthModule:get_user_creds(Req, User) of
nil ->
throw({unauthorized, <<"Name or password is incorrect.">>});
- {ok, UserProps, AuthCtx} ->
+ {ok, UserProps, _AuthCtx} ->
reject_if_totp(UserProps),
UserName = ?l2b(User),
Password = ?l2b(Pass),
@@ -298,7 +298,7 @@
UserName = ?l2b(extract_username(Form)),
Password = ?l2b(couch_util:get_value("password", Form, "")),
couch_log:debug("Attempt Login: ~s",[UserName]),
- {ok, UserProps, AuthCtx} = case AuthModule:get_user_creds(Req, UserName) of
+ {ok, UserProps, _AuthCtx} = case AuthModule:get_user_creds(Req, UserName) of
nil -> {ok, [], nil};
Result -> Result
end,
diff --git a/src/couch_httpd_vhost.erl b/src/couch_httpd_vhost.erl
index 05fc874..91a2476 100644
--- a/src/couch_httpd_vhost.erl
+++ b/src/couch_httpd_vhost.erl
@@ -12,6 +12,7 @@
-module(couch_httpd_vhost).
-behaviour(gen_server).
+-vsn(1).
-behaviour(config_listener).
-export([start_link/0, reload/0, get_state/0, dispatch_host/1]).
@@ -27,6 +28,7 @@
-define(SEPARATOR, $\/).
-define(MATCH_ALL, {bind, '*'}).
+-define(RELISTEN_DELAY, 5000).
-record(vhosts_state, {
vhosts,
@@ -355,6 +357,9 @@
handle_cast(_Msg, State) ->
{noreply, State}.
+handle_info(restart_config_listener, State) ->
+ ok = config:listen_for_changes(?MODULE, nil),
+ {noreply, State};
handle_info(_Info, State) ->
{noreply, State}.
@@ -374,12 +379,10 @@
handle_config_change(_, _, _, _, _) ->
{ok, nil}.
-handle_config_terminate(_, stop, _) -> ok;
-handle_config_terminate(_, _, _) ->
- spawn(fun() ->
- timer:sleep(5000),
- config:listen_for_changes(?MODULE, nil)
- end).
+handle_config_terminate(_, stop, _) ->
+ ok;
+handle_config_terminate(_Server, _Reason, _State) ->
+ erlang:send_after(?RELISTEN_DELAY, whereis(?MODULE), restart_config_listener).
load_conf() ->
%% get vhost globals
diff --git a/src/couch_os_daemons.erl b/src/couch_os_daemons.erl
index 409ba02..2c2c1a2 100644
--- a/src/couch_os_daemons.erl
+++ b/src/couch_os_daemons.erl
@@ -11,6 +11,7 @@
% the License.
-module(couch_os_daemons).
-behaviour(gen_server).
+-vsn(1).
-behaviour(config_listener).
-export([start_link/0, info/0, info/1]).
@@ -36,6 +37,7 @@
-define(PORT_OPTIONS, [stream, {line, 1024}, binary, exit_status, hide]).
-define(TIMEOUT, 5000).
+-define(RELISTEN_DELAY, 5000).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
@@ -183,6 +185,9 @@
[D] = ets:lookup(Table, Port),
true = ets:insert(Table, D#daemon{status=restarting, buf=nil}),
{noreply, Table};
+handle_info(restart_config_listener, State) ->
+ ok = config:listen_for_changes(?MODULE, nil),
+ {noreply, State};
handle_info(Msg, Table) ->
couch_log:error("Unexpected info message to ~p: ~p", [?MODULE, Msg]),
{stop, error, Table}.
@@ -195,12 +200,11 @@
gen_server:cast(?MODULE, {config_change, Section, Key}),
{ok, nil}.
-handle_config_terminate(_, stop, _) -> ok;
-handle_config_terminate(_, _, _) ->
- spawn(fun() ->
- timer:sleep(5000),
- config:listen_for_changes(?MODULE, nil)
- end).
+handle_config_terminate(_, stop, _) ->
+ ok;
+handle_config_terminate(_Server, _Reason, _State) ->
+ erlang:send_after(?RELISTEN_DELAY, whereis(?MODULE), restart_config_listener).
+
% Internal API
diff --git a/src/couch_os_process.erl b/src/couch_os_process.erl
index 4f15555..5852233 100644
--- a/src/couch_os_process.erl
+++ b/src/couch_os_process.erl
@@ -146,7 +146,7 @@
% gen_server API
init([Command, Options, PortOptions]) ->
PrivDir = couch_util:priv_dir(),
- Spawnkiller = filename:join(PrivDir, "couchspawnkillable"),
+ Spawnkiller = "\"" ++ filename:join(PrivDir, "couchspawnkillable") ++ "\"",
V = config:get("query_server_config", "os_process_idle_limit", "300"),
IdleLimit = list_to_integer(V) * 1000,
BaseProc = #os_proc{
diff --git a/src/couch_proc_manager.erl b/src/couch_proc_manager.erl
index 33cc1e5..04101f2 100644
--- a/src/couch_proc_manager.erl
+++ b/src/couch_proc_manager.erl
@@ -13,7 +13,7 @@
-module(couch_proc_manager).
-behaviour(gen_server).
-behaviour(config_listener).
--vsn(2).
+-vsn(3).
-export([
start_link/0,
@@ -43,6 +43,7 @@
-define(PROCS, couch_proc_manager_procs).
-define(WAITERS, couch_proc_manager_waiters).
-define(OPENING, couch_proc_manager_opening).
+-define(RELISTEN_DELAY, 5000).
-record(state, {
config,
@@ -131,35 +132,17 @@
{reply, ets:select_count(?PROCS, MatchSpec), State};
handle_call({get_proc, #doc{body={Props}}=DDoc, DDocKey}, From, State) ->
- {ClientPid, _} = From,
LangStr = couch_util:get_value(<<"language">>, Props, <<"javascript">>),
Lang = couch_util:to_binary(LangStr),
- IterFun = fun(Proc, Acc) ->
- case lists:member(DDocKey, Proc#proc_int.ddoc_keys) of
- true ->
- {stop, assign_proc(ClientPid, Proc)};
- false ->
- {ok, Acc}
- end
- end,
- TeachFun = fun(Proc0, Acc) ->
- try
- {ok, Proc1} = teach_ddoc(DDoc, DDocKey, Proc0),
- {stop, assign_proc(ClientPid, Proc1)}
- catch _:_ ->
- {ok, Acc}
- end
- end,
Client = #client{from=From, lang=Lang, ddoc=DDoc, ddoc_key=DDocKey},
- find_proc(State, Client, [IterFun, TeachFun]);
+ add_waiting_client(Client),
+ {noreply, flush_waiters(State, Lang)};
-handle_call({get_proc, Lang}, From, State) ->
- {ClientPid, _} = From,
- IterFun = fun(Proc, _Acc) ->
- {stop, assign_proc(ClientPid, Proc)}
- end,
- Client = #client{from=From, lang=couch_util:to_binary(Lang)},
- find_proc(State, Client, [IterFun]);
+handle_call({get_proc, LangStr}, From, State) ->
+ Lang = couch_util:to_binary(LangStr),
+ Client = #client{from=From, lang=Lang},
+ add_waiting_client(Client),
+ {noreply, flush_waiters(State, Lang)};
handle_call({ret_proc, #proc{client=Ref} = Proc}, _From, State) ->
erlang:demonitor(Ref, [flush]),
@@ -266,6 +249,11 @@
{noreply, State0}
end;
+
+handle_info(restart_config_listener, State) ->
+ ok = config:listen_for_changes(?MODULE, nil),
+ {noreply, State};
+
handle_info(_Msg, State) ->
{noreply, State}.
@@ -273,15 +261,11 @@
code_change(_OldVsn, #state{}=State, _Extra) ->
{ok, State}.
-handle_config_terminate(_, stop, _) -> ok;
-handle_config_terminate(_, _, _) ->
- spawn(fun() ->
- timer:sleep(5000),
- config:listen_for_changes(?MODULE, undefined),
- % Reload our config in case it changed in the last
- % five seconds.
- gen_server:cast(?MODULE, reload_config)
- end).
+handle_config_terminate(_, stop, _) ->
+ ok;
+handle_config_terminate(_Server, _Reason, _State) ->
+ gen_server:cast(?MODULE, reload_config),
+ erlang:send_after(?RELISTEN_DELAY, whereis(?MODULE), restart_config_listener).
handle_config_change("query_server_config", _, _, _, _) ->
gen_server:cast(?MODULE, reload_config),
@@ -290,54 +274,60 @@
{ok, undefined}.
-find_proc(State, Client, [Fun | FindFuns]) ->
- try iter_procs(Client#client.lang, Fun, undefined) of
- {not_found, _} ->
- find_proc(State, Client, FindFuns);
- {ok, Proc} ->
- {reply, {ok, Proc, State#state.config}, State}
+find_proc(#client{lang = Lang, ddoc_key = undefined}) ->
+ Pred = fun(_) ->
+ true
+ end,
+ find_proc(Lang, Pred);
+find_proc(#client{lang = Lang, ddoc = DDoc, ddoc_key = DDocKey} = Client) ->
+ Pred = fun(#proc_int{ddoc_keys = DDocKeys}) ->
+ lists:member(DDocKey, DDocKeys)
+ end,
+ case find_proc(Lang, Pred) of
+ not_found ->
+ case find_proc(Client#client{ddoc_key=undefined}) of
+ {ok, Proc} ->
+ teach_ddoc(DDoc, DDocKey, Proc);
+ Else ->
+ Else
+ end;
+ Else ->
+ Else
+ end.
+
+find_proc(Lang, Fun) ->
+ try iter_procs(Lang, Fun)
catch error:Reason ->
- couch_log:error("~p ~p ~p", [?MODULE, Reason, erlang:get_stacktrace()]),
- {reply, {error, Reason}, State}
- end;
-find_proc(State, Client, []) ->
- {noreply, maybe_spawn_proc(State, Client)}.
+ StackTrace = erlang:get_stacktrace(),
+ couch_log:error("~p ~p ~p", [?MODULE, Reason, StackTrace]),
+ {error, Reason}
+ end.
-iter_procs(Lang, Fun, Acc) when is_binary(Lang) ->
+iter_procs(Lang, Fun) when is_binary(Lang) ->
Pattern = #proc_int{lang=Lang, client=undefined, _='_'},
MSpec = [{Pattern, [], ['$_']}],
case ets:select_reverse(?PROCS, MSpec, 25) of
'$end_of_table' ->
- {not_found, Acc};
+ not_found;
Continuation ->
- iter_procs_int(Continuation, Fun, Acc)
+ iter_procs_int(Continuation, Fun)
end.
-iter_procs_int({[], Continuation0}, Fun, Acc) ->
+iter_procs_int({[], Continuation0}, Fun) ->
case ets:select_reverse(Continuation0) of
'$end_of_table' ->
- {not_found, Acc};
+ not_found;
Continuation1 ->
- iter_procs_int(Continuation1, Fun, Acc)
+ iter_procs_int(Continuation1, Fun)
end;
-iter_procs_int({[Proc | Rest], Continuation}, Fun, Acc0) ->
- case Fun(Proc, Acc0) of
- {ok, Acc1} ->
- iter_procs_int({Rest, Continuation}, Fun, Acc1);
- {stop, Acc1} ->
- {ok, Acc1}
- end.
-
-
-maybe_spawn_proc(State, Client) ->
- case dict:find(Client#client.lang, State#state.counts) of
- {ok, Count} when Count >= State#state.hard_limit ->
- add_waiting_client(Client),
- State;
- _ ->
- spawn_proc(State, Client)
+iter_procs_int({[Proc | Rest], Continuation}, Fun) ->
+ case Fun(Proc) of
+ true ->
+ {ok, Proc};
+ false ->
+ iter_procs_int({Rest, Continuation}, Fun)
end.
@@ -447,23 +437,13 @@
true = ets:update_element(?PROCS, Pid, [
{#proc_int.client, undefined}
]),
- maybe_assign_proc(State, ProcInt)
+ State
end;
false ->
remove_proc(State, ProcInt)
end,
flush_waiters(NewState, Lang).
-maybe_assign_proc(#state{} = State, ProcInt) ->
- #proc_int{lang = Lang} = ProcInt,
- case get_waiting_client(Lang) of
- #client{from = From} = Client ->
- Proc = assign_proc(Client, ProcInt#proc_int{client=undefined}),
- gen_server:reply(From, {ok, Proc, State#state.config}),
- State;
- undefined ->
- State
- end.
remove_proc(State, #proc_int{}=Proc) ->
ets:delete(?PROCS, Proc#proc_int.pid),
@@ -500,16 +480,27 @@
flush_waiters(State, Lang) ->
- case dict:fetch(Lang, State#state.counts) of
- Count when Count < State#state.hard_limit ->
- case get_waiting_client(Lang) of
- #client{} = Client ->
+ CanSpawn = can_spawn(State, Lang),
+ case get_waiting_client(Lang) of
+ #client{from = From} = Client ->
+ case find_proc(Client) of
+ {ok, ProcInt} ->
+ Proc = assign_proc(Client, ProcInt),
+ gen_server:reply(From, {ok, Proc, State#state.config}),
+ remove_waiting_client(Client),
+ flush_waiters(State, Lang);
+ {error, Error} ->
+ gen_server:reply(From, {error, Error}),
+ remove_waiting_client(Client),
+ flush_waiters(State, Lang);
+ not_found when CanSpawn ->
NewState = spawn_proc(State, Client),
+ remove_waiting_client(Client),
flush_waiters(NewState, Lang);
- undefined ->
+ not_found ->
State
end;
- _ ->
+ undefined ->
State
end.
@@ -523,11 +514,21 @@
'$end_of_table' ->
undefined;
{[#client{}=Client], _} ->
- ets:delete(?WAITERS, Client#client.timestamp),
Client
end.
+remove_waiting_client(#client{timestamp = Timestamp}) ->
+ ets:delete(?WAITERS, Timestamp).
+
+
+can_spawn(#state{hard_limit = HardLimit, counts = Counts}, Lang) ->
+ case dict:find(Lang, Counts) of
+ {ok, Count} -> Count < HardLimit;
+ error -> true
+ end.
+
+
get_proc_config() ->
Limit = config:get("query_server_config", "reduce_limit", "true"),
Timeout = config:get("couchdb", "os_process_timeout", "5000"),
diff --git a/src/couch_query_servers.erl b/src/couch_query_servers.erl
index a3d7a47..ea7628e 100644
--- a/src/couch_query_servers.erl
+++ b/src/couch_query_servers.erl
@@ -22,7 +22,7 @@
-export([with_ddoc_proc/2, proc_prompt/2, ddoc_prompt/3, ddoc_proc_prompt/3, json_doc/1]).
% For 210-os-proc-pool.t
--export([get_os_process/1, ret_os_process/1]).
+-export([get_os_process/1, get_ddoc_process/2, ret_os_process/1]).
-include_lib("couch/include/couch_db.hrl").
diff --git a/src/couch_server.erl b/src/couch_server.erl
index 2634bbf..417c791 100644
--- a/src/couch_server.erl
+++ b/src/couch_server.erl
@@ -13,7 +13,7 @@
-module(couch_server).
-behaviour(gen_server).
-behaviour(config_listener).
--vsn(2).
+-vsn(3).
-export([open/2,create/2,delete/2,get_version/0,get_version/1,get_uuid/0]).
-export([all_databases/0, all_databases/2]).
@@ -28,6 +28,7 @@
-include_lib("couch/include/couch_db.hrl").
-define(MAX_DBS_OPEN, 100).
+-define(RELISTEN_DELAY, 5000).
-record(server,{
root_dir = [],
@@ -44,10 +45,7 @@
couch:start().
get_version() ->
- case application:get_key(couch, vsn) of
- {ok, Version} -> Version;
- undefined -> "0.0.0"
- end.
+ ?COUCHDB_VERSION. %% Defined in rebar.config.script
get_version(short) ->
%% strip git hash from version string
[Version|_Rest] = string:tokens(get_version(), "+"),
@@ -121,9 +119,11 @@
maybe_add_sys_db_callbacks(DbName, Options) ->
DbsDbName = config:get("mem3", "shards_db", "_dbs"),
NodesDbName = config:get("mem3", "nodes_db", "_nodes"),
- IsReplicatorDb = path_ends_with(DbName, <<"_replicator">>),
- IsUsersDb = DbName ==config:get("couch_httpd_auth", "authentication_db", "_users") orelse
- path_ends_with(DbName, <<"_users">>),
+
+ IsReplicatorDb = path_ends_with(DbName, "_replicator"),
+ UsersDbSuffix = config:get("couchdb", "users_db_suffix", "_users"),
+ IsUsersDb = path_ends_with(DbName, "_users")
+ orelse path_ends_with(DbName, UsersDbSuffix),
if
DbName == DbsDbName ->
[sys_db | Options];
@@ -141,8 +141,10 @@
Options
end.
-path_ends_with(Path, Suffix) ->
- Suffix == couch_db:normalize_dbname(Path).
+path_ends_with(Path, Suffix) when is_binary(Suffix) ->
+ Suffix =:= couch_db:dbname_suffix(Path);
+path_ends_with(Path, Suffix) when is_list(Suffix) ->
+ path_ends_with(Path, ?l2b(Suffix)).
check_dbname(#server{}, DbName) ->
couch_db:validate_dbname(DbName).
@@ -233,13 +235,10 @@
handle_config_change(_, _, _, _, _) ->
{ok, nil}.
-handle_config_terminate(_, stop, _) -> ok;
-handle_config_terminate(_, _, _) ->
- spawn(fun() ->
- timer:sleep(5000),
- config:listen_for_changes(?MODULE, nil)
- end).
-
+handle_config_terminate(_, stop, _) ->
+ ok;
+handle_config_terminate(_Server, _Reason, _State) ->
+ erlang:send_after(?RELISTEN_DELAY, whereis(?MODULE), restart_config_listener).
all_databases() ->
@@ -527,6 +526,9 @@
[] ->
{noreply, Server}
end;
+handle_info(restart_config_listener, State) ->
+ ok = config:listen_for_changes(?MODULE, nil),
+ {noreply, State};
handle_info(Info, Server) ->
{stop, {unknown_message, Info}, Server}.
@@ -541,3 +543,89 @@
false -> Server#server{dbs_open=Server#server.dbs_open - 1};
true -> Server
end.
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+setup() ->
+ ok = meck:new(config, [passthrough]),
+ ok = meck:expect(config, get, fun config_get/3),
+ ok.
+
+teardown(_) ->
+ (catch meck:unload(config)).
+
+config_get("couchdb", "users_db_suffix", _) -> "users_db";
+config_get(_, _, _) -> undefined.
+
+maybe_add_sys_db_callbacks_pass_test_() ->
+ SysDbCases = [
+ "shards/00000000-3fffffff/foo/users_db.1415960794.couch",
+ "shards/00000000-3fffffff/foo/users_db.1415960794",
+ "shards/00000000-3fffffff/foo/users_db",
+ "shards/00000000-3fffffff/users_db.1415960794.couch",
+ "shards/00000000-3fffffff/users_db.1415960794",
+ "shards/00000000-3fffffff/users_db",
+
+ "shards/00000000-3fffffff/_users.1415960794.couch",
+ "shards/00000000-3fffffff/_users.1415960794",
+ "shards/00000000-3fffffff/_users",
+
+ "foo/users_db.couch",
+ "foo/users_db",
+ "users_db.couch",
+ "users_db",
+ "foo/_users.couch",
+ "foo/_users",
+ "_users.couch",
+ "_users",
+
+ "shards/00000000-3fffffff/foo/_replicator.1415960794.couch",
+ "shards/00000000-3fffffff/foo/_replicator.1415960794",
+ "shards/00000000-3fffffff/_replicator",
+ "foo/_replicator.couch",
+ "foo/_replicator",
+ "_replicator.couch",
+ "_replicator"
+ ],
+
+ NonSysDbCases = [
+ "shards/00000000-3fffffff/foo/mydb.1415960794.couch",
+ "shards/00000000-3fffffff/foo/mydb.1415960794",
+ "shards/00000000-3fffffff/mydb",
+ "foo/mydb.couch",
+ "foo/mydb",
+ "mydb.couch",
+ "mydb"
+ ],
+ {
+ foreach, fun setup/0, fun teardown/1,
+ [
+ [should_add_sys_db_callbacks(C) || C <- SysDbCases]
+ ++
+ [should_add_sys_db_callbacks(?l2b(C)) || C <- SysDbCases]
+ ++
+ [should_not_add_sys_db_callbacks(C) || C <- NonSysDbCases]
+ ++
+ [should_not_add_sys_db_callbacks(?l2b(C)) || C <- NonSysDbCases]
+ ]
+ }.
+
+should_add_sys_db_callbacks(DbName) ->
+ {test_name(DbName), ?_test(begin
+ Options = maybe_add_sys_db_callbacks(DbName, [other_options]),
+ ?assert(lists:member(sys_db, Options)),
+ ok
+ end)}.
+should_not_add_sys_db_callbacks(DbName) ->
+ {test_name(DbName), ?_test(begin
+ Options = maybe_add_sys_db_callbacks(DbName, [other_options]),
+ ?assertNot(lists:member(sys_db, Options)),
+ ok
+ end)}.
+
+test_name(DbName) ->
+ lists:flatten(io_lib:format("~p", [DbName])).
+
+
+-endif.
diff --git a/src/couch_sup.erl b/src/couch_sup.erl
index f7a4344..8dcaf1d 100644
--- a/src/couch_sup.erl
+++ b/src/couch_sup.erl
@@ -12,6 +12,7 @@
-module(couch_sup).
-behaviour(supervisor).
+-vsn(1).
-behaviour(config_listener).
@@ -35,7 +36,6 @@
notify_started(),
notify_uris(),
write_uris(),
- ok = config:listen_for_changes(?MODULE, nil),
Resp;
Else ->
notify_error(Else),
@@ -47,6 +47,14 @@
couch_log:info("Starting ~s", [?MODULE]),
{ok, {{one_for_one,10, 60}, [
{
+ config_listener_mon,
+ {config_listener_mon, start_link, [?MODULE, nil]},
+ permanent,
+ 5000,
+ worker,
+ [config_listener_mon]
+ },
+ {
couch_primary_services,
{couch_primary_sup, start_link, []},
permanent,
@@ -76,12 +84,8 @@
handle_config_change(_, _, _, _, _) ->
{ok, nil}.
-handle_config_terminate(_, stop, _) -> ok;
-handle_config_terminate(_, _, _) ->
- spawn(fun() ->
- timer:sleep(5000),
- config:listen_for_changes(?MODULE, undefined)
- end).
+handle_config_terminate(_Server, _Reason, _State) ->
+ ok.
notify_starting() ->
couch_log:info("Apache CouchDB ~s is starting.~n", [
diff --git a/src/couch_uuids.erl b/src/couch_uuids.erl
index 5e1fda9..0553243 100644
--- a/src/couch_uuids.erl
+++ b/src/couch_uuids.erl
@@ -13,7 +13,7 @@
-include_lib("couch/include/couch_db.hrl").
-behaviour(gen_server).
--vsn(2).
+-vsn(3).
-behaviour(config_listener).
-export([start/0, stop/0]).
@@ -25,6 +25,8 @@
% config_listener api
-export([handle_config_change/5, handle_config_terminate/3]).
+-define(RELISTEN_DELAY, 5000).
+
start() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
@@ -77,6 +79,9 @@
handle_cast(_Msg, State) ->
{noreply, State}.
+handle_info(restart_config_listener, State) ->
+ ok = config:listen_for_changes(?MODULE, nil),
+ {noreply, State};
handle_info(_Info, State) ->
{noreply, State}.
@@ -88,15 +93,11 @@
handle_config_change(_, _, _, _, _) ->
{ok, nil}.
-handle_config_terminate(_, stop, _) -> ok;
-handle_config_terminate(_, _, _) ->
- spawn(fun() ->
- timer:sleep(5000),
- config:listen_for_changes(?MODULE, undefined),
- % Reload our config in case it changed in the last
- % five seconds.
- gen_server:cast(?MODULE, change)
- end).
+handle_config_terminate(_, stop, _) ->
+ ok;
+handle_config_terminate(_Server, _Reason, _State) ->
+ gen_server:cast(?MODULE, change),
+ erlang:send_after(?RELISTEN_DELAY, whereis(?MODULE), restart_config_listener).
new_prefix() ->
couch_util:to_hex((crypto:rand_bytes(13))).
diff --git a/src/test_util.erl b/src/test_util.erl
index 88cfc63..3c4d170 100644
--- a/src/test_util.erl
+++ b/src/test_util.erl
@@ -220,7 +220,7 @@
start(Module, ExtraApps, []).
start(Module, ExtraApps, Options) ->
- Apps = start_applications([config, ioq, couch_epi | ExtraApps]),
+ Apps = start_applications([config, couch_log, ioq, couch_epi | ExtraApps]),
ToMock = [config, couch_stats] -- proplists:get_value(dont_mock, Options, []),
mock(ToMock),
#test_context{module = Module, mocked = ToMock, started = Apps}.
diff --git a/test/couch_db_plugin_tests.erl b/test/couch_db_plugin_tests.erl
index 337207e..ea9b230 100644
--- a/test/couch_db_plugin_tests.erl
+++ b/test/couch_db_plugin_tests.erl
@@ -52,9 +52,10 @@
teardown(Ctx) ->
couch_tests:teardown(Ctx).
-validate_dbname({true, _Db}, _) -> true;
-validate_dbname({false, _Db}, _) -> false;
-validate_dbname({fail, _Db}, _) -> throw(validate_dbname).
+validate_dbname({true, _Db}, _) -> {decided, true};
+validate_dbname({false, _Db}, _) -> {decided, false};
+validate_dbname({fail, _Db}, _) -> throw(validate_dbname);
+validate_dbname({pass, _Db}, _) -> no_decision.
before_doc_update({fail, _Doc}, _Db) -> throw(before_doc_update);
before_doc_update({true, Doc}, Db) -> [{true, [before_doc_update|Doc]}, Db];
@@ -82,44 +83,52 @@
{
setup, fun setup/0, fun teardown/1,
[
- fun validate_dbname_match/0,
- fun validate_dbname_no_match/0,
- fun validate_dbname_throw/0,
+ {"validate_dbname_match", fun validate_dbname_match/0},
+ {"validate_dbname_no_match", fun validate_dbname_no_match/0},
+ {"validate_dbname_throw", fun validate_dbname_throw/0},
+ {"validate_dbname_pass", fun validate_dbname_pass/0},
- fun before_doc_update_match/0,
- fun before_doc_update_no_match/0,
- fun before_doc_update_throw/0,
+ {"before_doc_update_match", fun before_doc_update_match/0},
+ {"before_doc_update_no_match", fun before_doc_update_no_match/0},
+ {"before_doc_update_throw", fun before_doc_update_throw/0},
- fun after_doc_read_match/0,
- fun after_doc_read_no_match/0,
- fun after_doc_read_throw/0,
+ {"after_doc_read_match", fun after_doc_read_match/0},
+ {"after_doc_read_no_match", fun after_doc_read_no_match/0},
+ {"after_doc_read_throw", fun after_doc_read_throw/0},
- fun validate_docid_match/0,
- fun validate_docid_no_match/0,
- fun validate_docid_throw/0,
+ {"validate_docid_match", fun validate_docid_match/0},
+ {"validate_docid_no_match", fun validate_docid_no_match/0},
+ {"validate_docid_throw", fun validate_docid_throw/0},
- fun check_is_admin_match/0,
- fun check_is_admin_no_match/0,
- fun check_is_admin_throw/0,
+ {"check_is_admin_match", fun check_is_admin_match/0},
+ {"check_is_admin_no_match", fun check_is_admin_no_match/0},
+ {"check_is_admin_throw", fun check_is_admin_throw/0},
- fun on_delete_match/0,
- fun on_delete_no_match/0,
- fun on_delete_throw/0
+ {"on_delete_match", fun on_delete_match/0},
+ {"on_delete_no_match", fun on_delete_no_match/0},
+ {"on_delete_throw", fun on_delete_throw/0}
]
}
}.
validate_dbname_match() ->
- ?assert(couch_db_plugin:validate_dbname({true, [db]}, db)).
+ ?assert(couch_db_plugin:validate_dbname(
+ {true, [db]}, db, fun(_, _) -> pass end)).
validate_dbname_no_match() ->
- ?assertNot(couch_db_plugin:validate_dbname({false, [db]}, db)).
+ ?assertNot(couch_db_plugin:validate_dbname(
+ {false, [db]}, db, fun(_, _) -> pass end)).
validate_dbname_throw() ->
?assertThrow(
validate_dbname,
- couch_db_plugin:validate_dbname({fail, [db]}, db)).
+ couch_db_plugin:validate_dbname(
+ {fail, [db]}, db, fun(_, _) -> pass end)).
+
+validate_dbname_pass() ->
+ ?assertEqual(pass, couch_db_plugin:validate_dbname(
+ {pass, [db]}, db, fun(_, _) -> pass end)).
before_doc_update_match() ->
?assertMatch(
diff --git a/test/couch_doc_tests.erl b/test/couch_doc_tests.erl
index 2025c25..fce4ff7 100644
--- a/test/couch_doc_tests.erl
+++ b/test/couch_doc_tests.erl
@@ -72,6 +72,34 @@
couch_doc:len_doc_to_multi_part_stream(Boundary, JsonBytes, Atts, true),
ok.
+validate_docid_test_() ->
+ {setup,
+ fun() ->
+ ok = meck:new(couch_db_plugin, [passthrough]),
+ meck:expect(couch_db_plugin, validate_docid, fun(_) -> false end)
+ end,
+ fun(_) ->
+ meck:unload(couch_db_plugin)
+ end,
+ [
+ ?_assertEqual(ok, couch_doc:validate_docid(<<"idx">>)),
+ ?_assertEqual(ok, couch_doc:validate_docid(<<"_design/idx">>)),
+ ?_assertEqual(ok, couch_doc:validate_docid(<<"_local/idx">>)),
+ ?_assertThrow({illegal_docid, _},
+ couch_doc:validate_docid(<<>>)),
+ ?_assertThrow({illegal_docid, _},
+ couch_doc:validate_docid(<<16#80>>)),
+ ?_assertThrow({illegal_docid, _},
+ couch_doc:validate_docid(<<"_idx">>)),
+ ?_assertThrow({illegal_docid, _},
+ couch_doc:validate_docid(<<"_">>)),
+ ?_assertThrow({illegal_docid, _},
+ couch_doc:validate_docid(<<"_design/">>)),
+ ?_assertThrow({illegal_docid, _},
+ couch_doc:validate_docid(<<"_local/">>))
+ ]
+ }.
+
request(start) ->
{ok, Doc} = file:read_file(?REQUEST_FIXTURE),
{Doc, fun() -> request(stop) end};
diff --git a/test/couchdb_auth_tests.erl b/test/couchdb_auth_tests.erl
index 9fb4ceb..0d3cb79 100644
--- a/test/couchdb_auth_tests.erl
+++ b/test/couchdb_auth_tests.erl
@@ -27,7 +27,7 @@
auth_test_() ->
Tests = [
fun should_return_username_on_post_to_session/2,
- fun should_return_authenticated_field/2,
+ fun should_not_return_authenticated_field/2,
fun should_return_list_of_handlers/2
],
{
@@ -58,21 +58,21 @@
proplists:get_value(<<"name">>, Json)
end).
-should_return_authenticated_field(_PortType, Url) ->
- ?_assertEqual(<<"local">>,
+should_not_return_authenticated_field(_PortType, Url) ->
+ ?_assertThrow({not_found, _},
begin
couch_util:get_nested_json_value(session(Url), [
<<"info">>, <<"authenticated">>])
end).
should_return_list_of_handlers(backdoor, Url) ->
- ?_assertEqual([<<"oauth">>,<<"cookie">>,<<"default">>, <<"local">>],
+ ?_assertEqual([<<"oauth">>,<<"cookie">>,<<"default">>],
begin
couch_util:get_nested_json_value(session(Url), [
<<"info">>, <<"authentication_handlers">>])
end);
should_return_list_of_handlers(clustered, Url) ->
- ?_assertEqual([<<"cookie">>,<<"default">>,<<"local">>],
+ ?_assertEqual([<<"cookie">>,<<"default">>],
begin
couch_util:get_nested_json_value(session(Url), [
<<"info">>, <<"authentication_handlers">>])
diff --git a/test/couchdb_mrview_tests.erl b/test/couchdb_mrview_tests.erl
new file mode 100644
index 0000000..fa9ba70
--- /dev/null
+++ b/test/couchdb_mrview_tests.erl
@@ -0,0 +1,165 @@
+% 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_mrview_tests).
+
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("couch/include/couch_db.hrl").
+
+
+
+-define(DDOC, {[
+ {<<"_id">>, <<"_design/foo">>},
+ {<<"shows">>, {[
+ {<<"bar">>, <<"function(doc, req) {return '<h1>wosh</h1>';}">>}
+ ]}},
+ {<<"updates">>, {[
+ {<<"report">>, <<"function(doc, req) {"
+ "var data = JSON.parse(req.body); "
+ "return ['test', data];"
+ "}">>}
+ ]}}
+]}).
+
+-define(USER, "admin").
+-define(PASS, "pass").
+-define(AUTH, {basic_auth, {?USER, ?PASS}}).
+
+
+start() ->
+ Ctx = test_util:start_couch([chttpd]),
+ ok = config:set("admins", ?USER, ?PASS, _Persist=false),
+ Ctx.
+
+setup(PortType) ->
+ ok = meck:new(mochiweb_socket, [passthrough]),
+ ok = meck:expect(mochiweb_socket, recv, fun mochiweb_socket_recv/3),
+
+ DbName = ?tempdb(),
+ ok = create_db(PortType, DbName),
+
+ Host = host_url(PortType),
+ upload_ddoc(Host, ?b2l(DbName)),
+ {Host, ?b2l(DbName)}.
+
+teardown(Ctx) ->
+ ok = config:delete("admins", ?USER, _Persist=false),
+ test_util:stop_couch(Ctx).
+
+teardown(PortType, {_Host, DbName}) ->
+ (catch meck:unload(mochiweb_socket)),
+ delete_db(PortType, ?l2b(DbName)),
+ ok.
+
+mrview_update_test_() ->
+ {
+ "Check show functionality",
+ {
+ setup,
+ fun start/0, fun teardown/1,
+ [
+ make_test_case(clustered, [fun should_return_invalid_request_body/2]),
+ make_test_case(backdoor, [fun should_return_invalid_request_body/2])
+ ]
+ }
+ }.
+
+make_test_case(Mod, Funs) ->
+ {
+ lists:flatten(io_lib:format("~s", [Mod])),
+ {foreachx, fun setup/1, fun teardown/2, [{Mod, Fun} || Fun <- Funs]}
+ }.
+
+should_return_invalid_request_body(PortType, {Host, DbName}) ->
+ ?_test(begin
+ ok = create_doc(PortType, ?l2b(DbName), <<"doc_id">>, {[]}),
+ ReqUrl = Host ++ "/" ++ DbName ++ "/_design/foo/_update/report/doc_id",
+ {ok, Status, _Headers, Body} =
+ test_request:post(ReqUrl, [?AUTH], <<"{truncated}">>),
+ {Props} = jiffy:decode(Body),
+ ?assertEqual(
+ <<"bad_request">>, couch_util:get_value(<<"error">>, Props)),
+ ?assertEqual(
+ <<"Invalid request body">>, couch_util:get_value(<<"reason">>, Props)),
+ ?assertEqual(400, Status),
+ ok
+ end).
+
+create_doc(backdoor, DbName, Id, Body) ->
+ JsonDoc = couch_util:json_apply_field({<<"_id">>, Id}, Body),
+ Doc = couch_doc:from_json_obj(JsonDoc),
+ {ok, Db} = couch_db:open(DbName, [?ADMIN_CTX]),
+ {ok, _} = couch_db:update_docs(Db, [Doc]),
+ couch_db:ensure_full_commit(Db),
+ couch_db:close(Db);
+create_doc(clustered, DbName, Id, Body) ->
+ JsonDoc = couch_util:json_apply_field({<<"_id">>, Id}, Body),
+ Doc = couch_doc:from_json_obj(JsonDoc),
+ {ok, _} = fabric:update_docs(DbName, [Doc], [?ADMIN_CTX]),
+ ok.
+
+create_db(backdoor, DbName) ->
+ {ok, Db} = couch_db:create(DbName, [?ADMIN_CTX]),
+ couch_db:close(Db);
+create_db(clustered, DbName) ->
+ {ok, Status, _, _} = test_request:put(db_url(DbName), [?AUTH], ""),
+ assert_success(create_db, Status),
+ ok.
+
+delete_db(backdoor, DbName) ->
+ couch_server:delete(DbName, [?ADMIN_CTX]);
+delete_db(clustered, DbName) ->
+ {ok, Status, _, _} = test_request:delete(db_url(DbName), [?AUTH]),
+ assert_success(delete_db, Status),
+ ok.
+
+assert_success(create_db, Status) ->
+ ?assert(lists:member(Status, [201, 202]));
+assert_success(delete_db, Status) ->
+ ?assert(lists:member(Status, [200, 202])).
+
+
+host_url(PortType) ->
+ "http://" ++ bind_address(PortType) ++ ":" ++ port(PortType).
+
+bind_address(PortType) ->
+ config:get(section(PortType), "bind_address", "127.0.0.1").
+
+section(backdoor) -> "http";
+section(clustered) -> "chttpd".
+
+db_url(DbName) when is_binary(DbName) ->
+ db_url(binary_to_list(DbName));
+db_url(DbName) when is_list(DbName) ->
+ host_url(clustered) ++ "/" ++ DbName.
+
+port(clustered) ->
+ integer_to_list(mochiweb_socket_server:get(chttpd, port));
+port(backdoor) ->
+ integer_to_list(mochiweb_socket_server:get(couch_httpd, port)).
+
+
+upload_ddoc(Host, DbName) ->
+ Url = Host ++ "/" ++ DbName ++ "/_design/foo",
+ Body = couch_util:json_encode(?DDOC),
+ {ok, 201, _Resp, _Body} = test_request:put(Url, [?AUTH], Body),
+ ok.
+
+mochiweb_socket_recv(Sock, Len, Timeout) ->
+ case meck:passthrough([Sock, Len, Timeout]) of
+ {ok, <<"{truncated}">>} ->
+ {error, closed};
+ {ok, Data} ->
+ {ok, Data};
+ Else ->
+ Else
+ end.
diff --git a/test/couchdb_os_proc_pool.erl b/test/couchdb_os_proc_pool.erl
index a698070..f14af68 100644
--- a/test/couchdb_os_proc_pool.erl
+++ b/test/couchdb_os_proc_pool.erl
@@ -15,26 +15,33 @@
-include_lib("couch/include/couch_eunit.hrl").
-include_lib("couch/include/couch_db.hrl").
--define(TIMEOUT, 3000).
+-define(TIMEOUT, 1000).
-start() ->
- Ctx = test_util:start_couch(),
- config:set("query_server_config", "os_process_limit", "3", false),
- timer:sleep(100), %% we need to wait to let gen_server:cast finish
- Ctx.
+setup() ->
+ ok = couch_proc_manager:reload(),
+ ok = setup_config().
+teardown(_) ->
+ ok.
os_proc_pool_test_() ->
{
"OS processes pool tests",
{
setup,
- fun start/0, fun test_util:stop_couch/1,
- [
- should_block_new_proc_on_full_pool(),
- should_free_slot_on_proc_unexpected_exit()
- ]
+ fun test_util:start_couch/0, fun test_util:stop_couch/1,
+ {
+ foreach,
+ fun setup/0, fun teardown/1,
+ [
+ should_block_new_proc_on_full_pool(),
+ should_free_slot_on_proc_unexpected_exit(),
+ should_reuse_known_proc(),
+% should_process_waiting_queue_as_fifo(),
+ should_reduce_pool_on_idle_os_procs()
+ ]
+ }
}
}.
@@ -115,6 +122,113 @@
end).
+should_reuse_known_proc() ->
+ ?_test(begin
+ Client1 = spawn_client(<<"ddoc1">>),
+ Client2 = spawn_client(<<"ddoc2">>),
+
+ ?assertEqual(ok, ping_client(Client1)),
+ ?assertEqual(ok, ping_client(Client2)),
+
+ Proc1 = get_client_proc(Client1, "1"),
+ Proc2 = get_client_proc(Client2, "2"),
+ ?assertNotEqual(Proc1#proc.pid, Proc2#proc.pid),
+
+ ?assertEqual(ok, stop_client(Client1)),
+ ?assertEqual(ok, stop_client(Client2)),
+ ?assert(is_process_alive(Proc1#proc.pid)),
+ ?assert(is_process_alive(Proc2#proc.pid)),
+
+ Client1Again = spawn_client(<<"ddoc1">>),
+ ?assertEqual(ok, ping_client(Client1Again)),
+ Proc1Again = get_client_proc(Client1Again, "1-again"),
+ ?assertEqual(Proc1#proc.pid, Proc1Again#proc.pid),
+ ?assertNotEqual(Proc1#proc.client, Proc1Again#proc.client),
+ ?assertEqual(ok, stop_client(Client1Again))
+ end).
+
+
+%should_process_waiting_queue_as_fifo() ->
+% ?_test(begin
+% Client1 = spawn_client(<<"ddoc1">>),
+% Client2 = spawn_client(<<"ddoc2">>),
+% Client3 = spawn_client(<<"ddoc3">>),
+% Client4 = spawn_client(<<"ddoc4">>),
+% timer:sleep(100),
+% Client5 = spawn_client(<<"ddoc5">>),
+%
+% ?assertEqual(ok, ping_client(Client1)),
+% ?assertEqual(ok, ping_client(Client2)),
+% ?assertEqual(ok, ping_client(Client3)),
+% ?assertEqual(timeout, ping_client(Client4)),
+% ?assertEqual(timeout, ping_client(Client5)),
+%
+% Proc1 = get_client_proc(Client1, "1"),
+% ?assertEqual(ok, stop_client(Client1)),
+% ?assertEqual(ok, ping_client(Client4)),
+% Proc4 = get_client_proc(Client4, "4"),
+%
+% ?assertNotEqual(Proc4#proc.client, Proc1#proc.client),
+% ?assertEqual(Proc1#proc.pid, Proc4#proc.pid),
+% ?assertEqual(timeout, ping_client(Client5)),
+%
+% ?assertEqual(ok, stop_client(Client2)),
+% ?assertEqual(ok, stop_client(Client3)),
+% ?assertEqual(ok, stop_client(Client4)),
+% ?assertEqual(ok, stop_client(Client5))
+% end).
+
+
+should_reduce_pool_on_idle_os_procs() ->
+ ?_test(begin
+ %% os_process_idle_limit is in sec
+ config:set("query_server_config",
+ "os_process_idle_limit", "1", false),
+ ok = confirm_config("os_process_idle_limit", "1"),
+
+ Client1 = spawn_client(<<"ddoc1">>),
+ Client2 = spawn_client(<<"ddoc2">>),
+ Client3 = spawn_client(<<"ddoc3">>),
+
+ ?assertEqual(ok, ping_client(Client1)),
+ ?assertEqual(ok, ping_client(Client2)),
+ ?assertEqual(ok, ping_client(Client3)),
+
+ ?assertEqual(3, couch_proc_manager:get_proc_count()),
+
+ ?assertEqual(ok, stop_client(Client1)),
+ ?assertEqual(ok, stop_client(Client2)),
+ ?assertEqual(ok, stop_client(Client3)),
+
+ timer:sleep(1200),
+ ?assertEqual(1, couch_proc_manager:get_proc_count())
+ end).
+
+
+setup_config() ->
+ config:set("query_server_config", "os_process_limit", "3", false),
+ config:set("query_server_config", "os_process_soft_limit", "2", false),
+ ok = confirm_config("os_process_soft_limit", "2").
+
+confirm_config(Key, Value) ->
+ confirm_config(Key, Value, 0).
+
+confirm_config(Key, Value, Count) ->
+ case config:get("query_server_config", Key) of
+ Value ->
+ ok;
+ _ when Count > 10 ->
+ erlang:error({config_setup, [
+ {module, ?MODULE},
+ {line, ?LINE},
+ {value, timeout}
+ ]});
+ _ ->
+ %% we need to wait to let gen_server:cast finish
+ timer:sleep(10),
+ confirm_config(Key, Value, Count + 1)
+ end.
+
spawn_client() ->
Parent = self(),
Ref = make_ref(),
@@ -124,6 +238,17 @@
end),
{Pid, Ref}.
+spawn_client(DDocId) ->
+ Parent = self(),
+ Ref = make_ref(),
+ Pid = spawn(fun() ->
+ DDocKey = {DDocId, <<"1-abcdefgh">>},
+ DDoc = #doc{body={[]}},
+ Proc = couch_query_servers:get_ddoc_process(DDoc, DDocKey),
+ loop(Parent, Ref, Proc)
+ end),
+ {Pid, Ref}.
+
ping_client({Pid, Ref}) ->
Pid ! ping,
receive