% 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(application). -export([start/0,start/1,start/2,stop/0,stop/1,restart/0]). -export([open/2,create/2,delete/1,all_databases/0,get_version/0]). -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,remote_restart/0]). -include("couch_db.hrl"). -record(server,{ root_dir = [], dbname_regexp, remote_restart=[], max_dbs_open=100, current_dbs_open=0 }). start() -> start(["couch.ini"]). start(IniFiles) -> couch_server_sup:start_link(IniFiles). start(_Type, _Args) -> start(). restart() -> stop(), start(). stop() -> couch_server_sup:stop(). stop(_Reason) -> stop(). dev_start() -> stop(), up_to_date = make:all([load, debug_info]), start(). get_version() -> Apps = application:loaded_applications(), case lists:keysearch(couch, 1, Apps) of {value, {_, _, Vsn}} -> Vsn; false -> "0.0.0" end. sup_start_link() -> gen_server:start_link({local, couch_server}, couch_server, [], []). open(DbName, Options) -> gen_server:call(couch_server, {open, DbName, Options}). create(DbName, Options) -> gen_server:call(couch_server, {create, DbName, Options}). delete(DbName) -> gen_server:call(couch_server, {delete, DbName}). remote_restart() -> gen_server:call(couch_server, remote_restart). check_dbname(#server{dbname_regexp=RegExp}, DbName) -> case regexp:match(DbName, RegExp) of nomatch -> {error, illegal_database_name}; _Match -> ok end. get_full_filename(Server, DbName) -> filename:join([Server#server.root_dir, "./" ++ DbName ++ ".couch"]). 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", "RootDirectory"}, "."), Options = couch_config:get({"CouchDB", "ServerOptions"}, []), Self = self(), ok = couch_config:register( fun({"CouchDB", "RootDirectory"}) -> exit(Self, config_change); ({"CouchDB", "ServerOptions"}) -> exit(Self, config_change) end), {ok, RegExp} = regexp:parse("^[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]), process_flag(trap_exit, true), MaxDbsOpen = proplists:get_value(max_dbs_open, Options), RemoteRestart = proplists:get_value(remote_restart, Options), {ok, #server{root_dir=RootDir, dbname_regexp=RegExp, max_dbs_open=MaxDbsOpen, remote_restart=RemoteRestart}}. terminate(_Reason, _Server) -> ok. all_databases() -> {ok, Root} = gen_server:call(couch_server, get_root), Filenames = filelib:fold_files(Root, "^[a-z0-9\\_\\$()\\+\\-]*[\\.]couch$", true, fun(Filename, AccIn) -> case Filename -- Root of [$/ | RelativeFilename] -> ok; RelativeFilename -> ok end, [filename:rootname(RelativeFilename, ".couch") | AccIn] end, []), {ok, Filenames}. maybe_close_lru_db(#server{current_dbs_open=NumOpen, max_dbs_open=MaxOpen}=Server) when NumOpen < MaxOpen -> {ok, Server}; maybe_close_lru_db(#server{current_dbs_open=NumOpen}=Server) -> % must free up the lru db. case try_close_lru(now()) of ok -> {ok, Server#server{current_dbs_open=NumOpen-1}}; Error -> Error end. try_close_lru(StartTime) -> LruTime = ets:first(couch_dbs_by_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), [{_, {MainPid, LruTime}}] = ets:lookup(couch_dbs_by_name, DbName), case couch_db:num_refs(MainPid) of 0 -> exit(MainPid, kill), receive {'EXIT', MainPid, _Reason} -> ok end, true = ets:delete(couch_dbs_by_lru, LruTime), true = ets:delete(couch_dbs_by_name, DbName), true = ets:delete(couch_dbs_by_pid, MainPid), ok; _NumRefs -> % 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, {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. handle_call(get_server, _From, Server) -> {reply, Server, Server}; handle_call(get_root, _From, #server{root_dir=Root}=Server) -> {reply, {ok, Root}, Server}; handle_call({open, DbName, Options}, {FromPid,_}, Server) -> case check_dbname(Server, DbName) of ok -> Filepath = get_full_filename(Server, DbName), LruTime = now(), case ets:lookup(couch_dbs_by_name, DbName) of [] -> case maybe_close_lru_db(Server) of {ok, Server2} -> case couch_db:start_link(DbName, Filepath, Options) of {ok, MainPid} -> true = ets:insert(couch_dbs_by_name, {DbName, {MainPid, LruTime}}), true = ets:insert(couch_dbs_by_pid, {MainPid, DbName}), true = ets:insert(couch_dbs_by_lru, {LruTime, DbName}), DbsOpen = Server2#server.current_dbs_open + 1, {reply, couch_db:open_ref_counted(MainPid, FromPid), Server2#server{current_dbs_open=DbsOpen}}; Error -> {reply, Error, Server2} end; CloseError -> {reply, CloseError, Server} end; [{_, {MainPid, PrevLruTime}}] -> true = ets:insert(couch_dbs_by_name, {DbName, {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; Error -> {reply, Error, Server} end; handle_call({create, DbName, Options}, {FromPid,_}, Server) -> case check_dbname(Server, DbName) of ok -> Filepath = get_full_filename(Server, DbName), case ets:lookup(couch_dbs_by_name, DbName) of [] -> case couch_db:start_link(DbName, Filepath, [create|Options]) of {ok, MainPid} -> LruTime = now(), true = ets:insert(couch_dbs_by_name, {DbName, {MainPid, LruTime}}), true = ets:insert(couch_dbs_by_pid, {MainPid, DbName}), true = ets:insert(couch_dbs_by_lru, {LruTime, DbName}), DbsOpen = Server#server.current_dbs_open + 1, {reply, couch_db:open_ref_counted(MainPid, FromPid), Server#server{current_dbs_open=DbsOpen}}; Error -> {reply, Error, Server} end; [_AlreadyRunningDb] -> {reply, {error, file_exists}, Server} end; Error -> {reply, Error, Server} end; handle_call({delete, DbName}, _From, Server) -> case check_dbname(Server, DbName) of ok -> FullFilepath = get_full_filename(Server, DbName), Server2 = case ets:lookup(couch_dbs_by_name, DbName) of [] -> Server; [{_, {Pid, LruTime}}] -> exit(Pid, kill), receive {'EXIT', Pid, _Reason} -> ok end, true = ets:delete(couch_dbs_by_name, DbName), true = ets:delete(couch_dbs_by_pid, Pid), true = ets:delete(couch_dbs_by_lru, LruTime), DbsOpen = Server#server.current_dbs_open - 1, Server#server{current_dbs_open=DbsOpen} end, case file:delete(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_call(remote_restart, _From, #server{remote_restart=false}=Server) -> {reply, ok, Server}; handle_call(remote_restart, _From, #server{remote_restart=true}=Server) -> exit(couch_server_sup, restart), {reply, ok, Server}. handle_cast(Msg, _Server) -> exit({unknown_cast_message, Msg}). code_change(_OldVsn, State, _Extra) -> {ok, State}. handle_info({'EXIT', Pid, _Reason}, Server) -> [{Pid, DbName}] = ets:lookup(couch_dbs_by_pid, Pid), true = ets:delete(couch_dbs_by_pid, Pid), true = ets:delete(couch_dbs_by_name, DbName), {noreply, Server}; handle_info(Info, _Server) -> exit({unknown_message, Info}).