diff options
Diffstat (limited to 'src/couch/src/couch_server.erl')
-rw-r--r-- | src/couch/src/couch_server.erl | 1097 |
1 files changed, 0 insertions, 1097 deletions
diff --git a/src/couch/src/couch_server.erl b/src/couch/src/couch_server.erl deleted file mode 100644 index 74217894c..000000000 --- a/src/couch/src/couch_server.erl +++ /dev/null @@ -1,1097 +0,0 @@ -% 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(couch_server). --behaviour(gen_server). --behaviour(config_listener). --vsn(3). - --export([open/2, create/2, delete/2, get_version/0, get_version/1, get_git_sha/0, get_uuid/0]). --export([all_databases/0, all_databases/2]). --export([init/1, handle_call/3, sup_start_link/1]). --export([handle_cast/2, code_change/3, handle_info/2, terminate/2]). --export([dev_start/0, is_admin/2, has_admins/0, get_stats/0]). --export([close_db_if_idle/1]). --export([delete_compaction_files/1]). --export([is_compacting/1]). --export([exists/1]). --export([get_engine_extensions/0]). --export([get_engine_path/2]). --export([lock/2, unlock/1]). --export([db_updated/1]). --export([num_servers/0, couch_server/1, couch_dbs_pid_to_name/1, couch_dbs/1]). --export([aggregate_queue_len/0, get_spidermonkey_version/0]). --export([names/0]). - -% config_listener api --export([handle_config_change/5, handle_config_terminate/3]). - --include_lib("couch/include/couch_db.hrl"). --include("couch_server_int.hrl"). - --define(MAX_DBS_OPEN, 500). --define(RELISTEN_DELAY, 5000). --define(DEFAULT_ENGINE, "couch"). - --record(server, { - root_dir = [], - engines = [], - max_dbs_open = ?MAX_DBS_OPEN, - dbs_open = 0, - start_time = "", - update_lru_on_read = true, - lru = couch_lru:new(), - couch_dbs, - couch_dbs_pid_to_name, - couch_dbs_locks -}). - -dev_start() -> - couch:stop(), - up_to_date = make:all([load, debug_info]), - couch:start(). - -get_version() -> - %% Defined in rebar.config.script - ?COUCHDB_VERSION. -get_version(short) -> - %% strip git hash from version string - [Version | _Rest] = string:tokens(get_version(), "+"), - Version. - -get_git_sha() -> ?COUCHDB_GIT_SHA. - -get_uuid() -> - case config:get("couchdb", "uuid", undefined) of - undefined -> - UUID = couch_uuids:random(), - config:set("couchdb", "uuid", ?b2l(UUID)), - UUID; - UUID -> - ?l2b(UUID) - end. - -get_stats() -> - Fun = fun(N, {TimeAcc, OpenAcc}) -> - {ok, #server{start_time = Time, dbs_open = Open}} = - gen_server:call(couch_server(N), get_server), - {max(Time, TimeAcc), Open + OpenAcc} - end, - {Time, Open} = - lists:foldl(Fun, {0, 0}, lists:seq(1, num_servers())), - [{start_time, ?l2b(Time)}, {dbs_open, Open}]. - -get_spidermonkey_version() -> list_to_binary(?COUCHDB_SPIDERMONKEY_VERSION). - -sup_start_link(N) -> - gen_server:start_link({local, couch_server(N)}, couch_server, [N], []). - -open(DbName, Options) -> - try - validate_open_or_create(DbName, Options), - open_int(DbName, Options) - catch - throw:{?MODULE, Error} -> - Error - end. - -open_int(DbName, Options0) -> - Ctx = couch_util:get_value(user_ctx, Options0, #user_ctx{}), - case ets:lookup(couch_dbs(DbName), DbName) of - [#entry{db = Db0, lock = Lock} = Entry] when Lock =/= locked -> - update_lru(DbName, Entry#entry.db_options), - {ok, Db1} = couch_db:incref(Db0), - couch_db:set_user_ctx(Db1, Ctx); - _ -> - Options = maybe_add_sys_db_callbacks(DbName, Options0), - Timeout = couch_util:get_value(timeout, Options, infinity), - Create = couch_util:get_value(create_if_missing, Options, false), - case gen_server:call(couch_server(DbName), {open, DbName, Options}, Timeout) of - {ok, Db0} -> - {ok, Db1} = couch_db:incref(Db0), - couch_db:set_user_ctx(Db1, Ctx); - {not_found, no_db_file} when Create -> - couch_log:warning("creating missing database: ~s", [DbName]), - couch_server:create(DbName, Options); - Error -> - Error - end - end. - -update_lru(DbName, Options) -> - case config:get_boolean("couchdb", "update_lru_on_read", false) of - true -> - case lists:member(sys_db, Options) of - false -> gen_server:cast(couch_server(DbName), {update_lru, DbName}); - true -> ok - end; - false -> - ok - end. - -create(DbName, Options) -> - try - validate_open_or_create(DbName, Options), - create_int(DbName, Options) - catch - throw:{?MODULE, Error} -> - Error - end. - -create_int(DbName, Options0) -> - Options = maybe_add_sys_db_callbacks(DbName, Options0), - couch_partition:validate_dbname(DbName, Options), - case gen_server:call(couch_server(DbName), {create, DbName, Options}, infinity) of - {ok, Db0} -> - Ctx = couch_util:get_value(user_ctx, Options, #user_ctx{}), - {ok, Db1} = couch_db:incref(Db0), - couch_db:set_user_ctx(Db1, Ctx); - Error -> - Error - end. - -delete(DbName, Options) -> - gen_server:call(couch_server(DbName), {delete, DbName, Options}, infinity). - -exists(DbName) -> - RootDir = config:get("couchdb", "database_dir", "."), - Engines = get_configured_engines(), - Possible = get_possible_engines(DbName, RootDir, Engines), - Possible /= []. - -delete_compaction_files(DbName) -> - delete_compaction_files(DbName, []). - -delete_compaction_files(DbName, DelOpts) when is_list(DbName) -> - RootDir = config:get("couchdb", "database_dir", "."), - lists:foreach( - fun({Ext, Engine}) -> - FPath = make_filepath(RootDir, DbName, Ext), - couch_db_engine:delete_compaction_files(Engine, RootDir, FPath, DelOpts) - end, - get_configured_engines() - ), - ok; -delete_compaction_files(DbName, DelOpts) when is_binary(DbName) -> - delete_compaction_files(?b2l(DbName), DelOpts). - -is_compacting(DbName) -> - lists:any( - fun({_, Engine}) -> - couch_db_engine:is_compacting(Engine, DbName) - end, - get_configured_engines() - ). - -maybe_add_sys_db_callbacks(DbName, Options) when is_binary(DbName) -> - maybe_add_sys_db_callbacks(?b2l(DbName), Options); -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"), - UsersDbSuffix = config:get("couchdb", "users_db_suffix", "_users"), - IsUsersDb = - path_ends_with(DbName, "_users") orelse - path_ends_with(DbName, UsersDbSuffix), - if - DbName == DbsDbName -> - [ - {before_doc_update, fun mem3_bdu:before_doc_update/3}, - sys_db - | Options - ]; - DbName == NodesDbName -> - [sys_db | Options]; - IsReplicatorDb -> - [ - {before_doc_update, fun couch_replicator_docs:before_doc_update/3}, - {after_doc_read, fun couch_replicator_docs:after_doc_read/2}, - sys_db - | Options - ]; - IsUsersDb -> - [ - {before_doc_update, fun couch_users_db:before_doc_update/3}, - {after_doc_read, fun couch_users_db:after_doc_read/2}, - sys_db - | Options - ]; - true -> - Options - end. - -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(DbName) -> - couch_db:validate_dbname(DbName). - -is_admin(User, ClearPwd) -> - case config:get("admins", User) of - "-hashed-" ++ HashedPwdAndSalt -> - [HashedPwd, Salt] = string:tokens(HashedPwdAndSalt, ","), - couch_util:to_hex(crypto:hash(sha, ClearPwd ++ Salt)) == HashedPwd; - _Else -> - false - end. - -has_admins() -> - config:get("admins") /= []. - -hash_admin_passwords() -> - hash_admin_passwords(true). - -hash_admin_passwords(Persist) -> - lists:foreach( - fun({User, ClearPassword}) -> - HashedPassword = couch_passwords:hash_admin_password(ClearPassword), - config:set("admins", User, ?b2l(HashedPassword), Persist) - end, - couch_passwords:get_unhashed_admins() - ). - -close_db_if_idle(DbName) -> - case ets:lookup(couch_dbs(DbName), DbName) of - [#entry{}] -> - gen_server:cast(couch_server(DbName), {close_db_if_idle, DbName}); - [] -> - ok - end. - -init([N]) -> - couch_util:set_mqd_off_heap(?MODULE), - couch_util:set_process_priority(?MODULE, high), - - % Mark pluggable storage engines as a supported feature - config:enable_feature('pluggable-storage-engines'), - - % Mark partitioned databases as a supported feature - config:enable_feature(partitioned), - - % Mark being able to receive documents with an _access property as a supported feature - config:enable_feature('access-ready'), - - % Mark if fips is enabled - case - erlang:function_exported(crypto, info_fips, 0) andalso - crypto:info_fips() == enabled - of - true -> - config:enable_feature('fips'); - false -> - ok - end, - - % read config and register for configuration changes - - % just stop if one of the config settings change. couch_server_sup - % will restart us and then we will pick up the new settings. - - RootDir = config:get("couchdb", "database_dir", "."), - Engines = get_configured_engines(), - MaxDbsOpen = config:get_integer("couchdb", "max_dbs_open", ?MAX_DBS_OPEN), - UpdateLruOnRead = config:get_boolean( - "couchdb", "update_lru_on_read", false - ), - ok = config:listen_for_changes(?MODULE, N), - ok = couch_file:init_delete_dir(RootDir), - hash_admin_passwords(), - ets:new(couch_dbs(N), [ - set, - protected, - named_table, - {keypos, #entry.name}, - {read_concurrency, true} - ]), - ets:new(couch_dbs_pid_to_name(N), [set, protected, named_table]), - ets:new(couch_dbs_locks(N), [ - set, - public, - named_table, - {read_concurrency, true} - ]), - process_flag(trap_exit, true), - {ok, #server{ - root_dir = RootDir, - engines = Engines, - max_dbs_open = per_couch_server(MaxDbsOpen), - update_lru_on_read = UpdateLruOnRead, - start_time = couch_util:rfc1123_date(), - couch_dbs = couch_dbs(N), - couch_dbs_pid_to_name = couch_dbs_pid_to_name(N), - couch_dbs_locks = couch_dbs_locks(N) - }}. - -terminate(Reason, Srv) -> - couch_log:error( - "couch_server terminating with ~p, state ~2048p", - [ - Reason, - Srv#server{lru = redacted} - ] - ), - ets:foldl( - fun(#entry{db = Db}, _) -> - % Filter out any entry records for open_async - % processes that haven't finished. - if - Db == undefined -> ok; - true -> couch_util:shutdown_sync(couch_db:get_pid(Db)) - end - end, - nil, - couch_dbs(Srv) - ), - ok. - -handle_config_change("couchdb", "database_dir", _, _, _) -> - exit(whereis(couch_server), config_change), - remove_handler; -handle_config_change("couchdb", "update_lru_on_read", "true", _, N) -> - gen_server:call(couch_server(N), {set_update_lru_on_read, true}), - {ok, N}; -handle_config_change("couchdb", "update_lru_on_read", _, _, N) -> - gen_server:call(couch_server(N), {set_update_lru_on_read, false}), - {ok, N}; -handle_config_change("couchdb", "max_dbs_open", Max0, _, N) when is_list(Max0) -> - Max1 = per_couch_server(list_to_integer(Max0)), - gen_server:call(couch_server(N), {set_max_dbs_open, Max1}), - {ok, N}; -handle_config_change("couchdb", "max_dbs_open", _, _, N) -> - Max = per_couch_server(?MAX_DBS_OPEN), - gen_server:call(couch_server(N), {set_max_dbs_open, Max}), - {ok, N}; -handle_config_change("couchdb_engines", _, _, _, N) -> - gen_server:call(couch_server(N), reload_engines), - {ok, N}; -handle_config_change("admins", _, _, Persist, N) -> - % spawn here so couch event manager doesn't deadlock - spawn(fun() -> hash_admin_passwords(Persist) end), - {ok, N}; -handle_config_change("httpd", "authentication_handlers", _, _, N) -> - couch_httpd:stop(), - {ok, N}; -handle_config_change("httpd", "bind_address", _, _, N) -> - couch_httpd:stop(), - {ok, N}; -handle_config_change("httpd", "port", _, _, N) -> - couch_httpd:stop(), - {ok, N}; -handle_config_change("httpd", "max_connections", _, _, N) -> - couch_httpd:stop(), - {ok, N}; -handle_config_change(_, _, _, _, N) -> - {ok, N}. - -handle_config_terminate(_, stop, _) -> - ok; -handle_config_terminate(_Server, _Reason, N) -> - erlang:send_after(?RELISTEN_DELAY, whereis(?MODULE), {restart_config_listener, N}). - -per_couch_server(X) -> - erlang:max(1, X div num_servers()). - -all_databases() -> - {ok, DbList} = all_databases( - fun(DbName, Acc) -> {ok, [DbName | Acc]} end, [] - ), - {ok, lists:usort(DbList)}. - -all_databases(Fun, Acc0) -> - {ok, #server{root_dir = Root}} = gen_server:call(couch_server_1, get_server), - NormRoot = couch_util:normpath(Root), - Extensions = get_engine_extensions(), - ExtRegExp = "(" ++ string:join(Extensions, "|") ++ ")", - RegExp = - % stock CouchDB name regex - "^[a-z0-9\\_\\$()\\+\\-]*" - % optional shard timestamp - "(\\.[0-9]{10,})?" - % filename extension - "\\." ++ ExtRegExp ++ "$", - FinalAcc = - try - couch_util:fold_files( - Root, - RegExp, - true, - fun(Filename, AccIn) -> - NormFilename = couch_util:normpath(Filename), - case NormFilename -- NormRoot of - [$/ | RelativeFilename] -> ok; - RelativeFilename -> ok - end, - Ext = filename:extension(RelativeFilename), - case Fun(?l2b(filename:rootname(RelativeFilename, Ext)), AccIn) of - {ok, NewAcc} -> NewAcc; - {stop, NewAcc} -> throw({stop, Fun, NewAcc}) - end - end, - Acc0 - ) - catch - throw:{stop, Fun, Acc1} -> - Acc1 - end, - {ok, FinalAcc}. - -make_room(Server, Options) -> - case lists:member(sys_db, Options) of - false -> maybe_close_lru_db(Server); - true -> {ok, Server} - end. - -maybe_close_lru_db(#server{dbs_open = NumOpen, max_dbs_open = MaxOpen} = Server) when - NumOpen < MaxOpen --> - {ok, Server}; -maybe_close_lru_db(#server{lru = Lru} = Server) -> - case couch_lru:close(Lru) of - {true, NewLru} -> - {ok, db_closed(Server#server{lru = NewLru}, [])}; - false -> - {error, all_dbs_active} - end. - -open_async(Server, From, DbName, Options) -> - NoLRUServer = Server#server{ - lru = redacted - }, - Parent = self(), - T0 = os:timestamp(), - Opener = spawn_link(fun() -> - Res = open_async_int(NoLRUServer, DbName, Options), - IsSuccess = - case Res of - {ok, _} -> true; - _ -> false - end, - case IsSuccess andalso lists:member(create, Options) of - true -> - couch_event:notify(DbName, created); - false -> - ok - end, - gen_server:call(Parent, {open_result, DbName, Res}, infinity), - unlink(Parent), - case IsSuccess of - true -> - % Track latency times for successful opens - Diff = timer:now_diff(os:timestamp(), T0) / 1000, - couch_stats:update_histogram([couchdb, db_open_time], Diff); - false -> - % Log unsuccessful open results - couch_log:info("open_result error ~p for ~s", [Res, DbName]) - end - end), - ReqType = - case lists:member(create, Options) of - true -> create; - false -> open - end, - true = ets:insert(couch_dbs(Server), #entry{ - name = DbName, - pid = Opener, - lock = locked, - waiters = [From], - req_type = ReqType, - db_options = Options - }), - true = ets:insert(couch_dbs_pid_to_name(Server), {Opener, DbName}), - db_opened(Server, Options). - -open_async_int(Server, DbName, Options) -> - DbNameList = binary_to_list(DbName), - case check_dbname(DbNameList) of - ok -> - case get_engine(Server, DbNameList, Options) of - {ok, {Module, FilePath}} -> - couch_db:start_link(Module, DbName, FilePath, Options); - Error2 -> - Error2 - end; - Error1 -> - Error1 - end. - -handle_call(close_lru, _From, #server{lru = Lru} = Server) -> - case couch_lru:close(Lru) of - {true, NewLru} -> - {reply, ok, db_closed(Server#server{lru = NewLru}, [])}; - false -> - {reply, {error, all_dbs_active}, Server} - end; -handle_call(open_dbs_count, _From, Server) -> - {reply, Server#server.dbs_open, Server}; -handle_call({set_update_lru_on_read, UpdateOnRead}, _From, Server) -> - {reply, ok, Server#server{update_lru_on_read = UpdateOnRead}}; -handle_call({set_max_dbs_open, Max}, _From, Server) -> - {reply, ok, Server#server{max_dbs_open = Max}}; -handle_call(reload_engines, _From, Server) -> - {reply, ok, Server#server{engines = get_configured_engines()}}; -handle_call(get_server, _From, Server) -> - {reply, {ok, Server}, Server}; -handle_call({open_result, DbName, {ok, Db}}, {Opener, _}, Server) -> - true = ets:delete(couch_dbs_pid_to_name(Server), Opener), - DbPid = couch_db:get_pid(Db), - case ets:lookup(couch_dbs(Server), DbName) of - [] -> - % db was deleted during async open - exit(DbPid, kill), - {reply, ok, Server}; - [#entry{pid = Opener, req_type = ReqType, waiters = Waiters} = Entry] -> - link(DbPid), - [gen_server:reply(Waiter, {ok, Db}) || Waiter <- Waiters], - % Cancel the creation request if it exists. - case ReqType of - {create, DbName, _Options, CrFrom} -> - gen_server:reply(CrFrom, file_exists); - _ -> - ok - end, - true = ets:insert(couch_dbs(Server), #entry{ - name = DbName, - db = Db, - pid = DbPid, - lock = unlocked, - db_options = Entry#entry.db_options, - start_time = couch_db:get_instance_start_time(Db) - }), - true = ets:insert(couch_dbs_pid_to_name(Server), {DbPid, DbName}), - Lru = - case couch_db:is_system_db(Db) of - false -> - couch_lru:insert(DbName, Server#server.lru); - true -> - Server#server.lru - end, - {reply, ok, Server#server{lru = Lru}}; - [#entry{}] -> - % A mismatched opener pid means that this open_result message - % was in our mailbox but is now stale. Mostly ignore - % it except to ensure that the db pid is super dead. - exit(couch_db:get_pid(Db), kill), - {reply, ok, Server} - end; -handle_call({open_result, DbName, {error, eexist}}, From, Server) -> - handle_call({open_result, DbName, file_exists}, From, Server); -handle_call({open_result, DbName, Error}, {Opener, _}, Server) -> - case ets:lookup(couch_dbs(Server), DbName) of - [] -> - % db was deleted during async open - {reply, ok, Server}; - [#entry{pid = Opener, req_type = ReqType, waiters = Waiters} = Entry] -> - [gen_server:reply(Waiter, Error) || Waiter <- Waiters], - true = ets:delete(couch_dbs(Server), DbName), - true = ets:delete(couch_dbs_pid_to_name(Server), Opener), - NewServer = - case ReqType of - {create, DbName, Options, CrFrom} -> - open_async(Server, CrFrom, DbName, Options); - _ -> - Server - end, - {reply, ok, db_closed(NewServer, Entry#entry.db_options)}; - [#entry{}] -> - % A mismatched pid means that this open_result message - % was in our mailbox and is now stale. Ignore it. - {reply, ok, Server} - end; -handle_call({open, DbName, Options}, From, Server) -> - case ets:lookup(couch_dbs(Server), DbName) of - [] -> - case make_room(Server, Options) of - {ok, Server2} -> - {noreply, open_async(Server2, From, DbName, Options)}; - CloseError -> - {reply, CloseError, Server} - end; - [#entry{waiters = Waiters} = Entry] when is_list(Waiters) -> - true = ets:insert(couch_dbs(Server), Entry#entry{waiters = [From | Waiters]}), - NumWaiters = length(Waiters), - if - NumWaiters =< 10 orelse NumWaiters rem 10 /= 0 -> - ok; - true -> - Fmt = "~b clients waiting to open db ~s", - couch_log:info(Fmt, [length(Waiters), DbName]) - end, - {noreply, Server}; - [#entry{db = Db}] -> - {reply, {ok, Db}, Server} - end; -handle_call({create, DbName, Options}, From, Server) -> - case ets:lookup(couch_dbs(Server), DbName) of - [] -> - case make_room(Server, Options) of - {ok, Server2} -> - CrOptions = [create | Options], - {noreply, open_async(Server2, From, DbName, CrOptions)}; - CloseError -> - {reply, CloseError, Server} - end; - [#entry{req_type = open} = Entry] -> - % We're trying to create a database while someone is in - % the middle of trying to open it. We allow one creator - % to wait while we figure out if it'll succeed. - CrOptions = [create | Options], - Req = {create, DbName, CrOptions, From}, - true = ets:insert(couch_dbs(Server), Entry#entry{req_type = Req}), - {noreply, Server}; - [_AlreadyRunningDb] -> - {reply, file_exists, Server} - end; -handle_call({delete, DbName, Options}, _From, Server) -> - DbNameList = binary_to_list(DbName), - case check_dbname(DbNameList) of - ok -> - Server2 = - case ets:lookup(couch_dbs(Server), DbName) of - [] -> - Server; - [#entry{pid = Pid, waiters = Waiters} = Entry] when is_list(Waiters) -> - true = ets:delete(couch_dbs(Server), DbName), - true = ets:delete(couch_dbs_pid_to_name(Server), Pid), - exit(Pid, kill), - [gen_server:reply(Waiter, not_found) || Waiter <- Waiters], - db_closed(Server, Entry#entry.db_options); - [#entry{pid = Pid} = Entry] -> - true = ets:delete(couch_dbs(Server), DbName), - true = ets:delete(couch_dbs_pid_to_name(Server), Pid), - exit(Pid, kill), - db_closed(Server, Entry#entry.db_options) - end, - - couch_db_plugin:on_delete(DbName, Options), - - DelOpt = [{context, delete} | Options], - - % Make sure and remove all compaction data - delete_compaction_files(DbNameList, Options), - - {ok, {Engine, FilePath}} = get_engine(Server, DbNameList), - RootDir = Server#server.root_dir, - case couch_db_engine:delete(Engine, RootDir, FilePath, DelOpt) of - ok -> - couch_event:notify(DbName, deleted), - {reply, ok, Server2}; - {error, enoent} -> - {reply, not_found, Server2}; - Else -> - {reply, Else, Server2} - end; - Error -> - {reply, Error, Server} - end; -handle_call({db_updated, Db}, _From, Server0) -> - DbName = couch_db:name(Db), - StartTime = couch_db:get_instance_start_time(Db), - Server = - try ets:lookup_element(couch_dbs(Server0), DbName, #entry.start_time) of - StartTime -> - true = ets:update_element(couch_dbs(Server0), DbName, {#entry.db, Db}), - Lru = - case couch_db:is_system_db(Db) of - false -> couch_lru:update(DbName, Server0#server.lru); - true -> Server0#server.lru - end, - Server0#server{lru = Lru}; - _ -> - Server0 - catch - _:_ -> - Server0 - end, - {reply, ok, Server}. - -handle_cast({update_lru, DbName}, #server{lru = Lru, update_lru_on_read = true} = Server) -> - {noreply, Server#server{lru = couch_lru:update(DbName, Lru)}}; -handle_cast({update_lru, _DbName}, Server) -> - {noreply, Server}; -handle_cast({close_db_if_idle, DbName}, Server) -> - case ets:update_element(couch_dbs(Server), DbName, {#entry.lock, locked}) of - true -> - [#entry{db = Db, db_options = DbOpts}] = ets:lookup(couch_dbs(Server), DbName), - case couch_db:is_idle(Db) of - true -> - DbPid = couch_db:get_pid(Db), - true = ets:delete(couch_dbs(Server), DbName), - true = ets:delete(couch_dbs_pid_to_name(Server), DbPid), - exit(DbPid, kill), - {noreply, db_closed(Server, DbOpts)}; - false -> - true = ets:update_element( - couch_dbs(Server), DbName, {#entry.lock, unlocked} - ), - {noreply, Server} - end; - false -> - {noreply, Server} - end; -handle_cast(Msg, Server) -> - {stop, {unknown_cast_message, Msg}, Server}. - -code_change(_OldVsn, #server{} = State, _Extra) -> - {ok, State}. - -handle_info({'EXIT', _Pid, config_change}, Server) -> - {stop, config_change, Server}; -handle_info({'EXIT', Pid, Reason}, Server) -> - case ets:lookup(couch_dbs_pid_to_name(Server), Pid) of - [{Pid, DbName}] -> - [#entry{waiters = Waiters} = Entry] = ets:lookup(couch_dbs(Server), DbName), - if - Reason /= snappy_nif_not_loaded -> - ok; - true -> - Msg = io_lib:format( - "To open the database `~s`, Apache CouchDB " - "must be built with Erlang OTP R13B04 or higher.", - [DbName] - ), - couch_log:error(Msg, []) - end, - % We kill databases on purpose so there's no reason - % to log that fact. So we restrict logging to "interesting" - % reasons. - if - Reason == normal orelse Reason == killed -> ok; - true -> couch_log:info("db ~s died with reason ~p", [DbName, Reason]) - end, - if - not is_list(Waiters) -> ok; - true -> [gen_server:reply(Waiter, Reason) || Waiter <- Waiters] - end, - true = ets:delete(couch_dbs(Server), DbName), - true = ets:delete(couch_dbs_pid_to_name(Server), Pid), - {noreply, db_closed(Server, Entry#entry.db_options)}; - [] -> - {noreply, Server} - end; -handle_info({restart_config_listener, N}, State) -> - ok = config:listen_for_changes(?MODULE, N), - {noreply, State}; -handle_info(Info, Server) -> - {stop, {unknown_message, Info}, Server}. - -db_opened(Server, Options) -> - case lists:member(sys_db, Options) of - false -> Server#server{dbs_open = Server#server.dbs_open + 1}; - true -> Server - end. - -db_closed(Server, Options) -> - case lists:member(sys_db, Options) of - false -> Server#server{dbs_open = Server#server.dbs_open - 1}; - true -> Server - end. - -validate_open_or_create(DbName, Options) -> - case check_dbname(DbName) of - ok -> - ok; - DbNameError -> - throw({?MODULE, DbNameError}) - end, - - case check_engine(Options) of - ok -> - ok; - EngineError -> - throw({?MODULE, EngineError}) - end, - - case ets:lookup(couch_dbs_locks(DbName), DbName) of - [] -> - ok; - [{DbName, Reason}] -> - throw({?MODULE, {error, {locked, Reason}}}) - end. - -get_configured_engines() -> - ConfigEntries = config:get("couchdb_engines"), - Engines = lists:flatmap( - fun({Extension, ModuleStr}) -> - try - [{Extension, list_to_atom(ModuleStr)}] - catch - _T:_R -> - [] - end - end, - ConfigEntries - ), - case Engines of - [] -> - [{"couch", couch_bt_engine}]; - Else -> - Else - end. - -get_engine(Server, DbName, Options) -> - #server{ - root_dir = RootDir, - engines = Engines - } = Server, - case couch_util:get_value(engine, Options) of - Ext when is_binary(Ext) -> - ExtStr = binary_to_list(Ext), - case lists:keyfind(ExtStr, 1, Engines) of - {ExtStr, Engine} -> - Path = make_filepath(RootDir, DbName, ExtStr), - {ok, {Engine, Path}}; - false -> - {error, {invalid_engine_extension, Ext}} - end; - _ -> - get_engine(Server, DbName) - end. - -get_engine(Server, DbName) -> - #server{ - root_dir = RootDir, - engines = Engines - } = Server, - Possible = get_possible_engines(DbName, RootDir, Engines), - case Possible of - [] -> - get_default_engine(Server, DbName); - [Engine] -> - {ok, Engine}; - _ -> - erlang:error(engine_conflict) - end. - -get_possible_engines(DbName, RootDir, Engines) -> - lists:foldl( - fun({Extension, Engine}, Acc) -> - Path = make_filepath(RootDir, DbName, Extension), - case couch_db_engine:exists(Engine, Path) of - true -> - [{Engine, Path} | Acc]; - false -> - Acc - end - end, - [], - Engines - ). - -get_default_engine(Server, DbName) -> - #server{ - root_dir = RootDir, - engines = Engines - } = Server, - Default = {couch_bt_engine, make_filepath(RootDir, DbName, "couch")}, - Extension = config:get("couchdb", "default_engine", ?DEFAULT_ENGINE), - case lists:keyfind(Extension, 1, Engines) of - {Extension, Module} -> - {ok, {Module, make_filepath(RootDir, DbName, Extension)}}; - false -> - Fmt = - "Invalid storage engine extension ~s," - " configured engine extensions are: ~s", - Exts = [E || {E, _} <- Engines], - Args = [Extension, string:join(Exts, ", ")], - couch_log:error(Fmt, Args), - {ok, Default} - end. - -make_filepath(RootDir, DbName, Extension) when is_binary(RootDir) -> - make_filepath(binary_to_list(RootDir), DbName, Extension); -make_filepath(RootDir, DbName, Extension) when is_binary(DbName) -> - make_filepath(RootDir, binary_to_list(DbName), Extension); -make_filepath(RootDir, DbName, Extension) when is_binary(Extension) -> - make_filepath(RootDir, DbName, binary_to_list(Extension)); -make_filepath(RootDir, DbName, Extension) -> - filename:join([RootDir, "./" ++ DbName ++ "." ++ Extension]). - -get_engine_extensions() -> - case config:get("couchdb_engines") of - [] -> - ["couch"]; - Entries -> - [Ext || {Ext, _Mod} <- Entries] - end. - -check_engine(Options) -> - case couch_util:get_value(engine, Options) of - Ext when is_binary(Ext) -> - ExtStr = binary_to_list(Ext), - Extensions = get_engine_extensions(), - case lists:member(ExtStr, Extensions) of - true -> - ok; - false -> - {error, {invalid_engine_extension, Ext}} - end; - _ -> - ok - end. - -get_engine_path(DbName, Engine) when is_binary(DbName), is_atom(Engine) -> - RootDir = config:get("couchdb", "database_dir", "."), - case lists:keyfind(Engine, 2, get_configured_engines()) of - {Ext, Engine} -> - {ok, make_filepath(RootDir, DbName, Ext)}; - false -> - {error, {invalid_engine, Engine}} - end. - -lock(DbName, Reason) when is_binary(DbName), is_binary(Reason) -> - case ets:lookup(couch_dbs(DbName), DbName) of - [] -> - true = ets:insert(couch_dbs_locks(DbName), {DbName, Reason}), - ok; - [#entry{}] -> - {error, already_opened} - end. - -unlock(DbName) when is_binary(DbName) -> - true = ets:delete(couch_dbs_locks(DbName), DbName), - ok. - -db_updated(Db) -> - DbName = couch_db:name(Db), - gen_server:call(couch_server(DbName), {db_updated, Db}, infinity). - -couch_server(Arg) -> - name("couch_server", Arg). - -couch_dbs(Arg) -> - name("couch_dbs", Arg). - -couch_dbs_pid_to_name(Arg) -> - name("couch_dbs_pid_to_name", Arg). - -couch_dbs_locks(Arg) -> - name("couch_dbs_locks", Arg). - -name("couch_dbs", #server{} = Server) -> - Server#server.couch_dbs; -name("couch_dbs_pid_to_name", #server{} = Server) -> - Server#server.couch_dbs_pid_to_name; -name("couch_dbs_locks", #server{} = Server) -> - Server#server.couch_dbs_locks; -name(BaseName, DbName) when is_list(DbName) -> - name(BaseName, ?l2b(DbName)); -name(BaseName, DbName) when is_binary(DbName) -> - N = 1 + erlang:phash2(DbName, num_servers()), - name(BaseName, N); -name(BaseName, N) when is_integer(N), N > 0 -> - list_to_atom(BaseName ++ "_" ++ integer_to_list(N)). - -num_servers() -> - erlang:system_info(schedulers). - -aggregate_queue_len() -> - N = num_servers(), - Names = [couch_server(I) || I <- lists:seq(1, N)], - MQs = [ - process_info(whereis(Name), message_queue_len) - || Name <- Names - ], - lists:sum([X || {_, X} <- MQs]). - -names() -> - N = couch_server:num_servers(), - [couch_server:couch_server(I) || I <- lists:seq(1, N)]. - --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). - -setup_all() -> - ok = meck:new(config, [passthrough]), - ok = meck:expect(config, get, fun config_get/3), - ok. - -teardown_all(_) -> - meck:unload(). - -config_get("couchdb", "users_db_suffix", _) -> "users_db"; -config_get(_, _, _) -> undefined. - -maybe_add_sys_db_callbacks_pass_test_() -> - { - setup, - fun setup_all/0, - fun teardown_all/1, - [ - fun should_add_sys_db_callbacks/0, - fun should_not_add_sys_db_callbacks/0 - ] - }. - -should_add_sys_db_callbacks() -> - Cases = [ - "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" - ], - lists:foreach( - fun(DbName) -> - check_case(DbName, true), - check_case(?l2b(DbName), true) - end, - Cases - ). - -should_not_add_sys_db_callbacks() -> - Cases = [ - "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" - ], - lists:foreach( - fun(DbName) -> - check_case(DbName, false), - check_case(?l2b(DbName), false) - end, - Cases - ). - -check_case(DbName, IsAdded) -> - Options = maybe_add_sys_db_callbacks(DbName, [other_options]), - ?assertEqual(IsAdded, lists:member(sys_db, Options)). - --endif. |