| % 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). |
| |
| -export([open/2,create/2,delete/2,get_version/0,get_version/1,get_uuid/0]). |
| -export([all_databases/0, all_databases/2]). |
| -export([init/1, handle_call/3,sup_start_link/0]). |
| -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]). |
| |
| -include("couch_db.hrl"). |
| |
| -record(server,{ |
| root_dir = [], |
| dbname_regexp, |
| max_dbs_open=100, |
| dbs_open=0, |
| start_time="" |
| }). |
| |
| dev_start() -> |
| couch:stop(), |
| up_to_date = make:all([load, debug_info]), |
| couch:start(). |
| |
| get_version() -> |
| Apps = application:loaded_applications(), |
| case lists:keysearch(couch, 1, Apps) of |
| {value, {_, _, Vsn}} -> |
| Vsn; |
| false -> |
| "0.0.0" |
| end. |
| get_version(short) -> |
| %% strip git hash from version string |
| [Version|_Rest] = string:tokens(get_version(), "+"), |
| Version. |
| |
| |
| get_uuid() -> |
| case couch_config:get("couchdb", "uuid", nil) of |
| nil -> |
| UUID = couch_uuids:random(), |
| couch_config:set("couchdb", "uuid", ?b2l(UUID)), |
| UUID; |
| UUID -> ?l2b(UUID) |
| end. |
| |
| get_stats() -> |
| {ok, #server{start_time=Time,dbs_open=Open}} = |
| gen_server:call(couch_server, get_server), |
| [{start_time, ?l2b(Time)}, {dbs_open, Open}]. |
| |
| sup_start_link() -> |
| gen_server:start_link({local, couch_server}, couch_server, [], []). |
| |
| open(DbName, Options0) -> |
| Options = maybe_add_sys_db_callbacks(DbName, Options0), |
| case gen_server:call(couch_server, {open, DbName, Options}, infinity) of |
| {ok, Db} -> |
| Ctx = couch_util:get_value(user_ctx, Options, #user_ctx{}), |
| {ok, Db#db{user_ctx=Ctx}}; |
| Error -> |
| Error |
| end. |
| |
| create(DbName, Options0) -> |
| Options = maybe_add_sys_db_callbacks(DbName, Options0), |
| case gen_server:call(couch_server, {create, DbName, Options}, infinity) of |
| {ok, Db} -> |
| Ctx = couch_util:get_value(user_ctx, Options, #user_ctx{}), |
| {ok, Db#db{user_ctx=Ctx}}; |
| Error -> |
| Error |
| end. |
| |
| delete(DbName, Options) -> |
| gen_server:call(couch_server, {delete, DbName, Options}, infinity). |
| |
| 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) -> |
| case couch_config:get("replicator", "db", "_replicator") of |
| DbName -> |
| [ |
| {before_doc_update, fun couch_replicator_manager:before_doc_update/2}, |
| {after_doc_read, fun couch_replicator_manager:after_doc_read/2}, |
| sys_db | Options |
| ]; |
| _ -> |
| case couch_config:get("couch_httpd_auth", "authentication_db", "_users") of |
| DbName -> |
| [ |
| {before_doc_update, fun couch_users_db:before_doc_update/2}, |
| {after_doc_read, fun couch_users_db:after_doc_read/2}, |
| sys_db | Options |
| ]; |
| _ -> |
| Options |
| end |
| end. |
| |
| check_dbname(#server{dbname_regexp=RegExp}, DbName) -> |
| case re:run(DbName, RegExp, [{capture, none}]) of |
| nomatch -> |
| case DbName of |
| "_users" -> ok; |
| "_replicator" -> ok; |
| _Else -> |
| {error, illegal_database_name, DbName} |
| end; |
| match -> |
| ok |
| end. |
| |
| is_admin(User, ClearPwd) -> |
| case couch_config:get("admins", User) of |
| "-hashed-" ++ HashedPwdAndSalt -> |
| [HashedPwd, Salt] = string:tokens(HashedPwdAndSalt, ","), |
| couch_util:to_hex(crypto:sha(ClearPwd ++ Salt)) == HashedPwd; |
| _Else -> |
| false |
| end. |
| |
| has_admins() -> |
| couch_config:get("admins") /= []. |
| |
| get_full_filename(Server, DbName) -> |
| filename:join([Server#server.root_dir, "./" ++ DbName ++ ".couch"]). |
| |
| hash_admin_passwords() -> |
| hash_admin_passwords(true). |
| |
| hash_admin_passwords(Persist) -> |
| lists:foreach( |
| fun({User, ClearPassword}) -> |
| HashedPassword = couch_passwords:hash_admin_password(ClearPassword), |
| couch_config:set("admins", User, ?b2l(HashedPassword), Persist) |
| end, couch_passwords:get_unhashed_admins()). |
| |
| init([]) -> |
| % 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 = couch_config:get("couchdb", "database_dir", "."), |
| MaxDbsOpen = list_to_integer( |
| couch_config:get("couchdb", "max_dbs_open")), |
| Self = self(), |
| ok = couch_config:register( |
| fun("couchdb", "database_dir") -> |
| exit(Self, config_change) |
| end), |
| ok = couch_config:register( |
| fun("couchdb", "max_dbs_open", Max) -> |
| gen_server:call(couch_server, |
| {set_max_dbs_open, list_to_integer(Max)}) |
| end), |
| ok = couch_file:init_delete_dir(RootDir), |
| hash_admin_passwords(), |
| ok = couch_config:register( |
| fun("admins", _Key, _Value, Persist) -> |
| % spawn here so couch_config doesn't try to call itself |
| spawn(fun() -> hash_admin_passwords(Persist) end) |
| end, false), |
| {ok, RegExp} = re:compile("^[a-z][a-z0-9\\_\\$()\\+\\-\\/]*$"), |
| ets:new(couch_dbs_by_name, [set, private, named_table]), |
| ets:new(couch_dbs_by_pid, [set, private, named_table]), |
| ets:new(couch_dbs_by_lru, [ordered_set, private, named_table]), |
| ets:new(couch_sys_dbs, [set, private, named_table]), |
| process_flag(trap_exit, true), |
| {ok, #server{root_dir=RootDir, |
| dbname_regexp=RegExp, |
| max_dbs_open=MaxDbsOpen, |
| start_time=couch_util:rfc1123_date()}}. |
| |
| terminate(_Reason, _Srv) -> |
| lists:foreach( |
| fun({_, {_, Pid, _}}) -> |
| couch_util:shutdown_sync(Pid) |
| end, |
| ets:tab2list(couch_dbs_by_name)). |
| |
| 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, get_server), |
| NormRoot = couch_util:normpath(Root), |
| FinalAcc = try |
| filelib:fold_files(Root, "^[a-z0-9\\_\\$()\\+\\-]*[\\.]couch$", true, |
| fun(Filename, AccIn) -> |
| NormFilename = couch_util:normpath(Filename), |
| case NormFilename -- NormRoot of |
| [$/ | RelativeFilename] -> ok; |
| RelativeFilename -> ok |
| end, |
| case Fun(?l2b(filename:rootname(RelativeFilename, ".couch")), AccIn) of |
| {ok, NewAcc} -> NewAcc; |
| {stop, NewAcc} -> throw({stop, Fun, NewAcc}) |
| end |
| end, Acc0) |
| catch throw:{stop, Fun, Acc1} -> |
| Acc1 |
| end, |
| {ok, FinalAcc}. |
| |
| |
| maybe_close_lru_db(#server{dbs_open=NumOpen, max_dbs_open=MaxOpen}=Server) |
| when NumOpen < MaxOpen -> |
| {ok, Server}; |
| maybe_close_lru_db(#server{dbs_open=NumOpen}=Server) -> |
| % must free up the lru db. |
| case try_close_lru(now()) of |
| ok -> |
| {ok, Server#server{dbs_open=NumOpen - 1}}; |
| Error -> Error |
| end. |
| |
| try_close_lru(StartTime) -> |
| LruTime = get_lru(), |
| if LruTime > StartTime -> |
| % this means we've looped through all our opened dbs and found them |
| % all in use. |
| {error, all_dbs_active}; |
| true -> |
| [{_, DbName}] = ets:lookup(couch_dbs_by_lru, LruTime), |
| [{_, {opened, MainPid, LruTime}}] = ets:lookup(couch_dbs_by_name, DbName), |
| case couch_db:is_idle(MainPid) of |
| true -> |
| ok = shutdown_idle_db(DbName, MainPid, LruTime); |
| false -> |
| % this still has referrers. Go ahead and give it a current lru time |
| % and try the next one in the table. |
| NewLruTime = now(), |
| true = ets:insert(couch_dbs_by_name, {DbName, {opened, MainPid, NewLruTime}}), |
| true = ets:insert(couch_dbs_by_pid, {MainPid, DbName}), |
| true = ets:delete(couch_dbs_by_lru, LruTime), |
| true = ets:insert(couch_dbs_by_lru, {NewLruTime, DbName}), |
| try_close_lru(StartTime) |
| end |
| end. |
| |
| get_lru() -> |
| get_lru(ets:first(couch_dbs_by_lru)). |
| |
| get_lru(LruTime) -> |
| [{LruTime, DbName}] = ets:lookup(couch_dbs_by_lru, LruTime), |
| case ets:member(couch_sys_dbs, DbName) of |
| false -> |
| LruTime; |
| true -> |
| [{_, {opened, MainPid, _}}] = ets:lookup(couch_dbs_by_name, DbName), |
| case couch_db:is_idle(MainPid) of |
| true -> |
| NextLru = ets:next(couch_dbs_by_lru, LruTime), |
| ok = shutdown_idle_db(DbName, MainPid, LruTime), |
| get_lru(NextLru); |
| false -> |
| get_lru(ets:next(couch_dbs_by_lru, LruTime)) |
| end |
| end. |
| |
| shutdown_idle_db(DbName, MainPid, LruTime) -> |
| couch_util:shutdown_sync(MainPid), |
| true = ets:delete(couch_dbs_by_lru, LruTime), |
| true = ets:delete(couch_dbs_by_name, DbName), |
| true = ets:delete(couch_dbs_by_pid, MainPid), |
| true = ets:delete(couch_sys_dbs, DbName), |
| ok. |
| |
| open_async(Server, From, DbName, Filepath, Options) -> |
| Parent = self(), |
| Opener = spawn_link(fun() -> |
| Res = couch_db:start_link(DbName, Filepath, Options), |
| gen_server:call( |
| Parent, {open_result, DbName, Res, Options}, infinity |
| ), |
| unlink(Parent), |
| case Res of |
| {ok, DbReader} -> |
| unlink(DbReader); |
| _ -> |
| ok |
| end |
| end), |
| true = ets:insert(couch_dbs_by_name, {DbName, {opening, Opener, [From]}}), |
| true = ets:insert(couch_dbs_by_pid, {Opener, DbName}), |
| DbsOpen = case lists:member(sys_db, Options) of |
| true -> |
| true = ets:insert(couch_sys_dbs, {DbName, true}), |
| Server#server.dbs_open; |
| false -> |
| Server#server.dbs_open + 1 |
| end, |
| Server#server{dbs_open = DbsOpen}. |
| |
| handle_call({set_max_dbs_open, Max}, _From, Server) -> |
| {reply, ok, Server#server{max_dbs_open=Max}}; |
| handle_call(get_server, _From, Server) -> |
| {reply, {ok, Server}, Server}; |
| handle_call({open_result, DbName, {ok, OpenedDbPid}, Options}, _From, Server) -> |
| link(OpenedDbPid), |
| [{DbName, {opening,Opener,Froms}}] = ets:lookup(couch_dbs_by_name, DbName), |
| lists:foreach(fun({FromPid,_}=From) -> |
| gen_server:reply(From, |
| catch couch_db:open_ref_counted(OpenedDbPid, FromPid)) |
| end, Froms), |
| LruTime = now(), |
| true = ets:insert(couch_dbs_by_name, |
| {DbName, {opened, OpenedDbPid, LruTime}}), |
| true = ets:delete(couch_dbs_by_pid, Opener), |
| true = ets:insert(couch_dbs_by_pid, {OpenedDbPid, DbName}), |
| true = ets:insert(couch_dbs_by_lru, {LruTime, DbName}), |
| case lists:member(create, Options) of |
| true -> |
| couch_db_update_notifier:notify({created, DbName}); |
| false -> |
| ok |
| end, |
| {reply, ok, Server}; |
| handle_call({open_result, DbName, {error, eexist}, Options}, From, Server) -> |
| handle_call({open_result, DbName, file_exists, Options}, From, Server); |
| handle_call({open_result, DbName, Error, Options}, _From, Server) -> |
| [{DbName, {opening,Opener,Froms}}] = ets:lookup(couch_dbs_by_name, DbName), |
| lists:foreach(fun(From) -> |
| gen_server:reply(From, Error) |
| end, Froms), |
| true = ets:delete(couch_dbs_by_name, DbName), |
| true = ets:delete(couch_dbs_by_pid, Opener), |
| DbsOpen = case lists:member(sys_db, Options) of |
| true -> |
| true = ets:delete(couch_sys_dbs, DbName), |
| Server#server.dbs_open; |
| false -> |
| Server#server.dbs_open - 1 |
| end, |
| {reply, ok, Server#server{dbs_open = DbsOpen}}; |
| handle_call({open, DbName, Options}, {FromPid,_}=From, Server) -> |
| LruTime = now(), |
| case ets:lookup(couch_dbs_by_name, DbName) of |
| [] -> |
| open_db(DbName, Server, Options, From); |
| [{_, {opening, Opener, Froms}}] -> |
| true = ets:insert(couch_dbs_by_name, {DbName, {opening, Opener, [From|Froms]}}), |
| {noreply, Server}; |
| [{_, {opened, MainPid, PrevLruTime}}] -> |
| true = ets:insert(couch_dbs_by_name, {DbName, {opened, MainPid, LruTime}}), |
| true = ets:delete(couch_dbs_by_lru, PrevLruTime), |
| true = ets:insert(couch_dbs_by_lru, {LruTime, DbName}), |
| {reply, couch_db:open_ref_counted(MainPid, FromPid), Server} |
| end; |
| handle_call({create, DbName, Options}, From, Server) -> |
| case ets:lookup(couch_dbs_by_name, DbName) of |
| [] -> |
| open_db(DbName, Server, [create | Options], From); |
| [_AlreadyRunningDb] -> |
| {reply, file_exists, Server} |
| end; |
| handle_call({delete, DbName, _Options}, _From, Server) -> |
| DbNameList = binary_to_list(DbName), |
| case check_dbname(Server, DbNameList) of |
| ok -> |
| FullFilepath = get_full_filename(Server, DbNameList), |
| UpdateState = |
| case ets:lookup(couch_dbs_by_name, DbName) of |
| [] -> false; |
| [{_, {opening, Pid, Froms}}] -> |
| couch_util:shutdown_sync(Pid), |
| true = ets:delete(couch_dbs_by_name, DbName), |
| true = ets:delete(couch_dbs_by_pid, Pid), |
| [gen_server:reply(F, not_found) || F <- Froms], |
| true; |
| [{_, {opened, Pid, LruTime}}] -> |
| couch_util:shutdown_sync(Pid), |
| true = ets:delete(couch_dbs_by_name, DbName), |
| true = ets:delete(couch_dbs_by_pid, Pid), |
| true = ets:delete(couch_dbs_by_lru, LruTime), |
| true |
| end, |
| Server2 = case UpdateState of |
| true -> |
| DbsOpen = case ets:member(couch_sys_dbs, DbName) of |
| true -> |
| true = ets:delete(couch_sys_dbs, DbName), |
| Server#server.dbs_open; |
| false -> |
| Server#server.dbs_open - 1 |
| end, |
| Server#server{dbs_open = DbsOpen}; |
| false -> |
| Server |
| end, |
| |
| %% Delete any leftover .compact files. If we don't do this a subsequent |
| %% request for this DB will try to open the .compact file and use it. |
| couch_file:delete(Server#server.root_dir, FullFilepath ++ ".compact"), |
| |
| case couch_file:delete(Server#server.root_dir, FullFilepath) of |
| ok -> |
| couch_db_update_notifier:notify({deleted, DbName}), |
| {reply, ok, Server2}; |
| {error, enoent} -> |
| {reply, not_found, Server2}; |
| Else -> |
| {reply, Else, Server2} |
| end; |
| Error -> |
| {reply, Error, Server} |
| end. |
| |
| handle_cast(Msg, _Server) -> |
| exit({unknown_cast_message, Msg}). |
| |
| code_change(_OldVsn, State, _Extra) -> |
| {ok, State}. |
| |
| handle_info({'EXIT', _Pid, config_change}, Server) -> |
| {noreply, shutdown, Server}; |
| handle_info({'EXIT', Pid, Reason}, Server) -> |
| Server2 = case ets:lookup(couch_dbs_by_pid, Pid) of |
| [{Pid, DbName}] -> |
| |
| % If the Pid is known, the name should be as well. |
| % If not, that's an error, which is why there is no [] clause. |
| case ets:lookup(couch_dbs_by_name, DbName) of |
| [{_, {opening, Pid, Froms}}] -> |
| Msg = case Reason of |
| snappy_nif_not_loaded -> |
| io_lib:format( |
| "To open the database `~s`, Apache CouchDB " |
| "must be built with Erlang OTP R13B04 or higher.", |
| [DbName] |
| ); |
| true -> |
| io_lib:format("Error opening database ~p: ~p", [DbName, Reason]) |
| end, |
| ?LOG_ERROR(Msg, []), |
| lists:foreach( |
| fun(F) -> gen_server:reply(F, {bad_otp_release, Msg}) end, |
| Froms |
| ); |
| [{_, {opened, Pid, LruTime}}] -> |
| ?LOG_ERROR( |
| "Unexpected exit of database process ~p [~p]: ~p", |
| [Pid, DbName, Reason] |
| ), |
| true = ets:delete(couch_dbs_by_lru, LruTime) |
| end, |
| |
| true = ets:delete(couch_dbs_by_pid, DbName), |
| true = ets:delete(couch_dbs_by_name, DbName), |
| |
| case ets:lookup(couch_sys_dbs, DbName) of |
| [{DbName, _}] -> |
| true = ets:delete(couch_sys_dbs, DbName), |
| Server; |
| [] -> |
| Server#server{dbs_open = Server#server.dbs_open - 1} |
| end |
| end, |
| {noreply, Server2}; |
| handle_info(Error, _Server) -> |
| ?LOG_ERROR("Unexpected message, restarting couch_server: ~p", [Error]), |
| exit(kill). |
| |
| open_db(DbName, Server, Options, From) -> |
| DbNameList = binary_to_list(DbName), |
| case check_dbname(Server, DbNameList) of |
| ok -> |
| Filepath = get_full_filename(Server, DbNameList), |
| case lists:member(sys_db, Options) of |
| true -> |
| {noreply, open_async(Server, From, DbName, Filepath, Options)}; |
| false -> |
| case maybe_close_lru_db(Server) of |
| {ok, Server2} -> |
| {noreply, open_async(Server2, From, DbName, Filepath, Options)}; |
| CloseError -> |
| {reply, CloseError, Server} |
| end |
| end; |
| Error -> |
| {reply, Error, Server} |
| end. |