1% Licensed under the Apache License, Version 2.0 (the "License"); you may not 2% use this file except in compliance with the License. You may obtain a copy of 3% the License at 4% 5% http://www.apache.org/licenses/LICENSE-2.0 6% 7% Unless required by applicable law or agreed to in writing, software 8% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10% License for the specific language governing permissions and limitations under 11% the License. 12 13-module(couch_server). 14-behaviour(gen_server). 15-behaviour(config_listener). 16-vsn(3). 17 18-export([open/2,create/2,delete/2,get_version/0,get_version/1,get_git_sha/0,get_uuid/0]). 19-export([all_databases/0, all_databases/2]). 20-export([init/1, handle_call/3,sup_start_link/1]). 21-export([handle_cast/2,code_change/3,handle_info/2,terminate/2]). 22-export([dev_start/0,is_admin/2,has_admins/0,get_stats/0]). 23-export([close_db_if_idle/1]). 24-export([delete_compaction_files/1]). 25-export([exists/1]). 26-export([get_engine_extensions/0]). 27-export([get_engine_path/2]). 28-export([lock/2, unlock/1]). 29-export([db_updated/1]). 30-export([num_servers/0, couch_server/1, couch_dbs_pid_to_name/1, couch_dbs/1]). 31-export([aggregate_queue_len/0,get_spidermonkey_version/0]). 32 33% config_listener api 34-export([handle_config_change/5, handle_config_terminate/3]). 35 36-include_lib("couch/include/couch_db.hrl"). 37-include("couch_server_int.hrl"). 38 39-define(MAX_DBS_OPEN, 500). 40-define(RELISTEN_DELAY, 5000). 41-define(DEFAULT_ENGINE, "couch"). 42 43-record(server,{ 44 root_dir = [], 45 engines = [], 46 max_dbs_open=?MAX_DBS_OPEN, 47 dbs_open=0, 48 start_time="", 49 update_lru_on_read=true, 50 lru = couch_lru:new(), 51 couch_dbs, 52 couch_dbs_pid_to_name, 53 couch_dbs_locks 54 }). 55 56dev_start() -> 57 couch:stop(), 58 up_to_date = make:all([load, debug_info]), 59 couch:start(). 60 61get_version() -> 62 ?COUCHDB_VERSION. %% Defined in rebar.config.script 63get_version(short) -> 64 %% strip git hash from version string 65 [Version|_Rest] = string:tokens(get_version(), "+"), 66 Version. 67 68get_git_sha() -> ?COUCHDB_GIT_SHA. 69 70get_uuid() -> 71 case config:get("couchdb", "uuid", undefined) of 72 undefined -> 73 UUID = couch_uuids:random(), 74 config:set("couchdb", "uuid", ?b2l(UUID)), 75 UUID; 76 UUID -> ?l2b(UUID) 77 end. 78 79get_stats() -> 80 Fun = fun(N, {TimeAcc, OpenAcc}) -> 81 {ok, #server{start_time=Time,dbs_open=Open}} = 82 gen_server:call(couch_server(N), get_server), 83 {max(Time, TimeAcc), Open + OpenAcc} end, 84 {Time, Open} = 85 lists:foldl(Fun, {0, 0}, lists:seq(1, num_servers())), 86 [{start_time, ?l2b(Time)}, {dbs_open, Open}]. 87 88get_spidermonkey_version() -> list_to_binary(?COUCHDB_SPIDERMONKEY_VERSION). 89 90sup_start_link(N) -> 91 gen_server:start_link({local, couch_server(N)}, couch_server, [N], []). 92 93open(DbName, Options) -> 94 try 95 validate_open_or_create(DbName, Options), 96 open_int(DbName, Options) 97 catch throw:{?MODULE, Error} -> 98 Error 99 end. 100 101open_int(DbName, Options0) -> 102 Ctx = couch_util:get_value(user_ctx, Options0, #user_ctx{}), 103 case ets:lookup(couch_dbs(DbName), DbName) of 104 [#entry{db = Db0, lock = Lock} = Entry] when Lock =/= locked -> 105 update_lru(DbName, Entry#entry.db_options), 106 {ok, Db1} = couch_db:incref(Db0), 107 couch_db:set_user_ctx(Db1, Ctx); 108 _ -> 109 Options = maybe_add_sys_db_callbacks(DbName, Options0), 110 Timeout = couch_util:get_value(timeout, Options, infinity), 111 Create = couch_util:get_value(create_if_missing, Options, false), 112 case gen_server:call(couch_server(DbName), {open, DbName, Options}, Timeout) of 113 {ok, Db0} -> 114 {ok, Db1} = couch_db:incref(Db0), 115 couch_db:set_user_ctx(Db1, Ctx); 116 {not_found, no_db_file} when Create -> 117 couch_log:warning("creating missing database: ~s", [DbName]), 118 couch_server:create(DbName, Options); 119 Error -> 120 Error 121 end 122 end. 123 124update_lru(DbName, Options) -> 125 case config:get_boolean("couchdb", "update_lru_on_read", false) of 126 true -> 127 case lists:member(sys_db, Options) of 128 false -> gen_server:cast(couch_server(DbName), {update_lru, DbName}); 129 true -> ok 130 end; 131 false -> 132 ok 133 end. 134 135 136create(DbName, Options) -> 137 try 138 validate_open_or_create(DbName, Options), 139 create_int(DbName, Options) 140 catch throw:{?MODULE, Error} -> 141 Error 142 end. 143 144create_int(DbName, Options0) -> 145 Options = maybe_add_sys_db_callbacks(DbName, Options0), 146 couch_partition:validate_dbname(DbName, Options), 147 case gen_server:call(couch_server(DbName), {create, DbName, Options}, infinity) of 148 {ok, Db0} -> 149 Ctx = couch_util:get_value(user_ctx, Options, #user_ctx{}), 150 {ok, Db1} = couch_db:incref(Db0), 151 couch_db:set_user_ctx(Db1, Ctx); 152 Error -> 153 Error 154 end. 155 156delete(DbName, Options) -> 157 gen_server:call(couch_server(DbName), {delete, DbName, Options}, infinity). 158 159 160exists(DbName) -> 161 RootDir = config:get("couchdb", "database_dir", "."), 162 Engines = get_configured_engines(), 163 Possible = get_possible_engines(DbName, RootDir, Engines), 164 Possible /= []. 165 166 167delete_compaction_files(DbName) -> 168 delete_compaction_files(DbName, []). 169 170delete_compaction_files(DbName, DelOpts) when is_list(DbName) -> 171 RootDir = config:get("couchdb", "database_dir", "."), 172 lists:foreach(fun({Ext, Engine}) -> 173 FPath = make_filepath(RootDir, DbName, Ext), 174 couch_db_engine:delete_compaction_files(Engine, RootDir, FPath, DelOpts) 175 end, get_configured_engines()), 176 ok; 177delete_compaction_files(DbName, DelOpts) when is_binary(DbName) -> 178 delete_compaction_files(?b2l(DbName), DelOpts). 179 180maybe_add_sys_db_callbacks(DbName, Options) when is_binary(DbName) -> 181 maybe_add_sys_db_callbacks(?b2l(DbName), Options); 182maybe_add_sys_db_callbacks(DbName, Options) -> 183 DbsDbName = config:get("mem3", "shards_db", "_dbs"), 184 NodesDbName = config:get("mem3", "nodes_db", "_nodes"), 185 186 IsReplicatorDb = path_ends_with(DbName, "_replicator"), 187 UsersDbSuffix = config:get("couchdb", "users_db_suffix", "_users"), 188 IsUsersDb = path_ends_with(DbName, "_users") 189 orelse path_ends_with(DbName, UsersDbSuffix), 190 if 191 DbName == DbsDbName -> 192 [{before_doc_update, fun mem3_bdu:before_doc_update/3}, 193 sys_db | Options]; 194 DbName == NodesDbName -> 195 [sys_db | Options]; 196 IsReplicatorDb -> 197 [{before_doc_update, fun couch_replicator_docs:before_doc_update/3}, 198 {after_doc_read, fun couch_replicator_docs:after_doc_read/2}, 199 sys_db | Options]; 200 IsUsersDb -> 201 [{before_doc_update, fun couch_users_db:before_doc_update/3}, 202 {after_doc_read, fun couch_users_db:after_doc_read/2}, 203 sys_db | Options]; 204 true -> 205 Options 206 end. 207 208path_ends_with(Path, Suffix) when is_binary(Suffix) -> 209 Suffix =:= couch_db:dbname_suffix(Path); 210path_ends_with(Path, Suffix) when is_list(Suffix) -> 211 path_ends_with(Path, ?l2b(Suffix)). 212 213check_dbname(DbName) -> 214 couch_db:validate_dbname(DbName). 215 216is_admin(User, ClearPwd) -> 217 case config:get("admins", User) of 218 "-hashed-" ++ HashedPwdAndSalt -> 219 [HashedPwd, Salt] = string:tokens(HashedPwdAndSalt, ","), 220 couch_util:to_hex(crypto:hash(sha, ClearPwd ++ Salt)) == HashedPwd; 221 _Else -> 222 false 223 end. 224 225has_admins() -> 226 config:get("admins") /= []. 227 228hash_admin_passwords() -> 229 hash_admin_passwords(true). 230 231hash_admin_passwords(Persist) -> 232 lists:foreach( 233 fun({User, ClearPassword}) -> 234 HashedPassword = couch_passwords:hash_admin_password(ClearPassword), 235 config:set("admins", User, ?b2l(HashedPassword), Persist) 236 end, couch_passwords:get_unhashed_admins()). 237 238close_db_if_idle(DbName) -> 239 case ets:lookup(couch_dbs(DbName), DbName) of 240 [#entry{}] -> 241 gen_server:cast(couch_server(DbName), {close_db_if_idle, DbName}); 242 [] -> 243 ok 244 end. 245 246 247init([N]) -> 248 couch_util:set_mqd_off_heap(?MODULE), 249 couch_util:set_process_priority(?MODULE, high), 250 251 % Mark pluggable storage engines as a supported feature 252 config:enable_feature('pluggable-storage-engines'), 253 254 % Mark partitioned databases as a supported feature 255 config:enable_feature(partitioned), 256 257 % Mark being able to receive documents with an _access property as a supported feature 258 config:enable_feature('access-ready'), 259 260 % Mark if fips is enabled 261 case 262 erlang:function_exported(crypto, info_fips, 0) andalso 263 crypto:info_fips() == enabled of 264 true -> 265 config:enable_feature('fips'); 266 false -> 267 ok 268 end, 269 270 % read config and register for configuration changes 271 272 % just stop if one of the config settings change. couch_server_sup 273 % will restart us and then we will pick up the new settings. 274 275 RootDir = config:get("couchdb", "database_dir", "."), 276 Engines = get_configured_engines(), 277 MaxDbsOpen = config:get_integer("couchdb", "max_dbs_open", ?MAX_DBS_OPEN), 278 UpdateLruOnRead = config:get_boolean( 279 "couchdb", "update_lru_on_read", false), 280 ok = config:listen_for_changes(?MODULE, N), 281 ok = couch_file:init_delete_dir(RootDir), 282 hash_admin_passwords(), 283 ets:new(couch_dbs(N), [ 284 set, 285 protected, 286 named_table, 287 {keypos, #entry.name}, 288 {read_concurrency, true} 289 ]), 290 ets:new(couch_dbs_pid_to_name(N), [set, protected, named_table]), 291 ets:new(couch_dbs_locks(N), [ 292 set, 293 public, 294 named_table, 295 {read_concurrency, true} 296 ]), 297 process_flag(trap_exit, true), 298 {ok, #server{root_dir=RootDir, 299 engines = Engines, 300 max_dbs_open=per_couch_server(MaxDbsOpen), 301 update_lru_on_read=UpdateLruOnRead, 302 start_time=couch_util:rfc1123_date(), 303 couch_dbs=couch_dbs(N), 304 couch_dbs_pid_to_name=couch_dbs_pid_to_name(N), 305 couch_dbs_locks=couch_dbs_locks(N)}}. 306 307terminate(Reason, Srv) -> 308 couch_log:error("couch_server terminating with ~p, state ~2048p", 309 [Reason, 310 Srv#server{lru = redacted}]), 311 ets:foldl(fun(#entry{db = Db}, _) -> 312 % Filter out any entry records for open_async 313 % processes that haven't finished. 314 if Db == undefined -> ok; true -> 315 couch_util:shutdown_sync(couch_db:get_pid(Db)) 316 end 317 end, nil, couch_dbs(Srv)), 318 ok. 319 320handle_config_change("couchdb", "database_dir", _, _, _) -> 321 exit(whereis(couch_server), config_change), 322 remove_handler; 323handle_config_change("couchdb", "update_lru_on_read", "true", _, N) -> 324 gen_server:call(couch_server(N),{set_update_lru_on_read,true}), 325 {ok, N}; 326handle_config_change("couchdb", "update_lru_on_read", _, _, N) -> 327 gen_server:call(couch_server(N),{set_update_lru_on_read,false}), 328 {ok, N}; 329handle_config_change("couchdb", "max_dbs_open", Max0, _, N) when is_list(Max0) -> 330 Max1 = per_couch_server(list_to_integer(Max0)), 331 gen_server:call(couch_server(N),{set_max_dbs_open,Max1}), 332 {ok, N}; 333handle_config_change("couchdb", "max_dbs_open", _, _, N) -> 334 Max = per_couch_server(?MAX_DBS_OPEN), 335 gen_server:call(couch_server(N),{set_max_dbs_open,Max}), 336 {ok, N}; 337handle_config_change("couchdb_engines", _, _, _, N) -> 338 gen_server:call(couch_server(N), reload_engines), 339 {ok, N}; 340handle_config_change("admins", _, _, Persist, N) -> 341 % spawn here so couch event manager doesn't deadlock 342 spawn(fun() -> hash_admin_passwords(Persist) end), 343 {ok, N}; 344handle_config_change("httpd", "authentication_handlers", _, _, N) -> 345 couch_httpd:stop(), 346 {ok, N}; 347handle_config_change("httpd", "bind_address", _, _, N) -> 348 couch_httpd:stop(), 349 {ok, N}; 350handle_config_change("httpd", "port", _, _, N) -> 351 couch_httpd:stop(), 352 {ok, N}; 353handle_config_change("httpd", "max_connections", _, _, N) -> 354 couch_httpd:stop(), 355 {ok, N}; 356handle_config_change(_, _, _, _, N) -> 357 {ok, N}. 358 359handle_config_terminate(_, stop, _) -> 360 ok; 361handle_config_terminate(_Server, _Reason, N) -> 362 erlang:send_after(?RELISTEN_DELAY, whereis(?MODULE), {restart_config_listener, N}). 363 364 365per_couch_server(X) -> 366 erlang:max(1, X div num_servers()). 367 368 369all_databases() -> 370 {ok, DbList} = all_databases( 371 fun(DbName, Acc) -> {ok, [DbName | Acc]} end, []), 372 {ok, lists:usort(DbList)}. 373 374all_databases(Fun, Acc0) -> 375 {ok, #server{root_dir=Root}} = gen_server:call(couch_server_1, get_server), 376 NormRoot = couch_util:normpath(Root), 377 Extensions = get_engine_extensions(), 378 ExtRegExp = "(" ++ string:join(Extensions, "|") ++ ")", 379 RegExp = 380 "^[a-z0-9\\_\\$()\\+\\-]*" % stock CouchDB name regex 381 "(\\.[0-9]{10,})?" % optional shard timestamp 382 "\\." ++ ExtRegExp ++ "$", % filename extension 383 FinalAcc = try 384 couch_util:fold_files(Root, 385 RegExp, 386 true, 387 fun(Filename, AccIn) -> 388 NormFilename = couch_util:normpath(Filename), 389 case NormFilename -- NormRoot of 390 [$/ | RelativeFilename] -> ok; 391 RelativeFilename -> ok 392 end, 393 Ext = filename:extension(RelativeFilename), 394 case Fun(?l2b(filename:rootname(RelativeFilename, Ext)), AccIn) of 395 {ok, NewAcc} -> NewAcc; 396 {stop, NewAcc} -> throw({stop, Fun, NewAcc}) 397 end 398 end, Acc0) 399 catch throw:{stop, Fun, Acc1} -> 400 Acc1 401 end, 402 {ok, FinalAcc}. 403 404 405make_room(Server, Options) -> 406 case lists:member(sys_db, Options) of 407 false -> maybe_close_lru_db(Server); 408 true -> {ok, Server} 409 end. 410 411maybe_close_lru_db(#server{dbs_open=NumOpen, max_dbs_open=MaxOpen}=Server) 412 when NumOpen < MaxOpen -> 413 {ok, Server}; 414maybe_close_lru_db(#server{lru=Lru}=Server) -> 415 case couch_lru:close(Lru) of 416 {true, NewLru} -> 417 {ok, db_closed(Server#server{lru = NewLru}, [])}; 418 false -> 419 {error, all_dbs_active} 420 end. 421 422open_async(Server, From, DbName, Options) -> 423 NoLRUServer = Server#server{ 424 lru = redacted 425 }, 426 Parent = self(), 427 T0 = os:timestamp(), 428 Opener = spawn_link(fun() -> 429 Res = open_async_int(NoLRUServer, DbName, Options), 430 IsSuccess = case Res of 431 {ok, _} -> true; 432 _ -> false 433 end, 434 case IsSuccess andalso lists:member(create, Options) of 435 true -> 436 couch_event:notify(DbName, created); 437 false -> 438 ok 439 end, 440 gen_server:call(Parent, {open_result, DbName, Res}, infinity), 441 unlink(Parent), 442 case IsSuccess of 443 true -> 444 % Track latency times for successful opens 445 Diff = timer:now_diff(os:timestamp(), T0) / 1000, 446 couch_stats:update_histogram([couchdb, db_open_time], Diff); 447 false -> 448 % Log unsuccessful open results 449 couch_log:info("open_result error ~p for ~s", [Res, DbName]) 450 end 451 end), 452 ReqType = case lists:member(create, Options) of 453 true -> create; 454 false -> open 455 end, 456 true = ets:insert(couch_dbs(Server), #entry{ 457 name = DbName, 458 pid = Opener, 459 lock = locked, 460 waiters = [From], 461 req_type = ReqType, 462 db_options = Options 463 }), 464 true = ets:insert(couch_dbs_pid_to_name(Server), {Opener, DbName}), 465 db_opened(Server, Options). 466 467open_async_int(Server, DbName, Options) -> 468 DbNameList = binary_to_list(DbName), 469 case check_dbname(DbNameList) of 470 ok -> 471 case get_engine(Server, DbNameList, Options) of 472 {ok, {Module, FilePath}} -> 473 couch_db:start_link(Module, DbName, FilePath, Options); 474 Error2 -> 475 Error2 476 end; 477 Error1 -> 478 Error1 479 end. 480 481handle_call(close_lru, _From, #server{lru=Lru} = Server) -> 482 case couch_lru:close(Lru) of 483 {true, NewLru} -> 484 {reply, ok, db_closed(Server#server{lru = NewLru}, [])}; 485 false -> 486 {reply, {error, all_dbs_active}, Server} 487 end; 488handle_call(open_dbs_count, _From, Server) -> 489 {reply, Server#server.dbs_open, Server}; 490handle_call({set_update_lru_on_read, UpdateOnRead}, _From, Server) -> 491 {reply, ok, Server#server{update_lru_on_read=UpdateOnRead}}; 492handle_call({set_max_dbs_open, Max}, _From, Server) -> 493 {reply, ok, Server#server{max_dbs_open=Max}}; 494handle_call(reload_engines, _From, Server) -> 495 {reply, ok, Server#server{engines = get_configured_engines()}}; 496handle_call(get_server, _From, Server) -> 497 {reply, {ok, Server}, Server}; 498handle_call({open_result, DbName, {ok, Db}}, {Opener, _}, Server) -> 499 true = ets:delete(couch_dbs_pid_to_name(Server), Opener), 500 DbPid = couch_db:get_pid(Db), 501 case ets:lookup(couch_dbs(Server), DbName) of 502 [] -> 503 % db was deleted during async open 504 exit(DbPid, kill), 505 {reply, ok, Server}; 506 [#entry{pid = Opener, req_type = ReqType, waiters = Waiters} = Entry] -> 507 link(DbPid), 508 [gen_server:reply(Waiter, {ok, Db}) || Waiter <- Waiters], 509 % Cancel the creation request if it exists. 510 case ReqType of 511 {create, DbName, _Options, CrFrom} -> 512 gen_server:reply(CrFrom, file_exists); 513 _ -> 514 ok 515 end, 516 true = ets:insert(couch_dbs(Server), #entry{ 517 name = DbName, 518 db = Db, 519 pid = DbPid, 520 lock = unlocked, 521 db_options = Entry#entry.db_options, 522 start_time = couch_db:get_instance_start_time(Db) 523 }), 524 true = ets:insert(couch_dbs_pid_to_name(Server), {DbPid, DbName}), 525 Lru = case couch_db:is_system_db(Db) of 526 false -> 527 couch_lru:insert(DbName, Server#server.lru); 528 true -> 529 Server#server.lru 530 end, 531 {reply, ok, Server#server{lru = Lru}}; 532 [#entry{}] -> 533 % A mismatched opener pid means that this open_result message 534 % was in our mailbox but is now stale. Mostly ignore 535 % it except to ensure that the db pid is super dead. 536 exit(couch_db:get_pid(Db), kill), 537 {reply, ok, Server} 538 end; 539handle_call({open_result, DbName, {error, eexist}}, From, Server) -> 540 handle_call({open_result, DbName, file_exists}, From, Server); 541handle_call({open_result, DbName, Error}, {Opener, _}, Server) -> 542 case ets:lookup(couch_dbs(Server), DbName) of 543 [] -> 544 % db was deleted during async open 545 {reply, ok, Server}; 546 [#entry{pid = Opener, req_type = ReqType, waiters = Waiters} = Entry] -> 547 [gen_server:reply(Waiter, Error) || Waiter <- Waiters], 548 true = ets:delete(couch_dbs(Server), DbName), 549 true = ets:delete(couch_dbs_pid_to_name(Server), Opener), 550 NewServer = case ReqType of 551 {create, DbName, Options, CrFrom} -> 552 open_async(Server, CrFrom, DbName, Options); 553 _ -> 554 Server 555 end, 556 {reply, ok, db_closed(NewServer, Entry#entry.db_options)}; 557 [#entry{}] -> 558 % A mismatched pid means that this open_result message 559 % was in our mailbox and is now stale. Ignore it. 560 {reply, ok, Server} 561 end; 562handle_call({open, DbName, Options}, From, Server) -> 563 case ets:lookup(couch_dbs(Server), DbName) of 564 [] -> 565 case make_room(Server, Options) of 566 {ok, Server2} -> 567 {noreply, open_async(Server2, From, DbName, Options)}; 568 CloseError -> 569 {reply, CloseError, Server} 570 end; 571 [#entry{waiters = Waiters} = Entry] when is_list(Waiters) -> 572 true = ets:insert(couch_dbs(Server), Entry#entry{waiters = [From | Waiters]}), 573 NumWaiters = length(Waiters), 574 if NumWaiters =< 10 orelse NumWaiters rem 10 /= 0 -> ok; true -> 575 Fmt = "~b clients waiting to open db ~s", 576 couch_log:info(Fmt, [length(Waiters), DbName]) 577 end, 578 {noreply, Server}; 579 [#entry{db = Db}] -> 580 {reply, {ok, Db}, Server} 581 end; 582handle_call({create, DbName, Options}, From, Server) -> 583 case ets:lookup(couch_dbs(Server), DbName) of 584 [] -> 585 case make_room(Server, Options) of 586 {ok, Server2} -> 587 CrOptions = [create | Options], 588 {noreply, open_async(Server2, From, DbName, CrOptions)}; 589 CloseError -> 590 {reply, CloseError, Server} 591 end; 592 [#entry{req_type = open} = Entry] -> 593 % We're trying to create a database while someone is in 594 % the middle of trying to open it. We allow one creator 595 % to wait while we figure out if it'll succeed. 596 CrOptions = [create | Options], 597 Req = {create, DbName, CrOptions, From}, 598 true = ets:insert(couch_dbs(Server), Entry#entry{req_type = Req}), 599 {noreply, Server}; 600 [_AlreadyRunningDb] -> 601 {reply, file_exists, Server} 602 end; 603handle_call({delete, DbName, Options}, _From, Server) -> 604 DbNameList = binary_to_list(DbName), 605 case check_dbname(DbNameList) of 606 ok -> 607 Server2 = 608 case ets:lookup(couch_dbs(Server), DbName) of 609 [] -> Server; 610 [#entry{pid = Pid, waiters = Waiters} = Entry] when is_list(Waiters) -> 611 true = ets:delete(couch_dbs(Server), DbName), 612 true = ets:delete(couch_dbs_pid_to_name(Server), Pid), 613 exit(Pid, kill), 614 [gen_server:reply(Waiter, not_found) || Waiter <- Waiters], 615 db_closed(Server, Entry#entry.db_options); 616 [#entry{pid = Pid} = Entry] -> 617 true = ets:delete(couch_dbs(Server), DbName), 618 true = ets:delete(couch_dbs_pid_to_name(Server), Pid), 619 exit(Pid, kill), 620 db_closed(Server, Entry#entry.db_options) 621 end, 622 623 couch_db_plugin:on_delete(DbName, Options), 624 625 DelOpt = [{context, delete} | Options], 626 627 % Make sure and remove all compaction data 628 delete_compaction_files(DbNameList, Options), 629 630 {ok, {Engine, FilePath}} = get_engine(Server, DbNameList), 631 RootDir = Server#server.root_dir, 632 case couch_db_engine:delete(Engine, RootDir, FilePath, DelOpt) of 633 ok -> 634 couch_event:notify(DbName, deleted), 635 {reply, ok, Server2}; 636 {error, enoent} -> 637 {reply, not_found, Server2}; 638 Else -> 639 {reply, Else, Server2} 640 end; 641 Error -> 642 {reply, Error, Server} 643 end; 644handle_call({db_updated, Db}, _From, Server0) -> 645 DbName = couch_db:name(Db), 646 StartTime = couch_db:get_instance_start_time(Db), 647 Server = try ets:lookup_element(couch_dbs(Server0), DbName, #entry.start_time) of 648 StartTime -> 649 true = ets:update_element(couch_dbs(Server0), DbName, {#entry.db, Db}), 650 Lru = case couch_db:is_system_db(Db) of 651 false -> couch_lru:update(DbName, Server0#server.lru); 652 true -> Server0#server.lru 653 end, 654 Server0#server{lru = Lru}; 655 _ -> 656 Server0 657 catch _:_ -> 658 Server0 659 end, 660 {reply, ok, Server}. 661 662handle_cast({update_lru, DbName}, #server{lru = Lru, update_lru_on_read=true} = Server) -> 663 {noreply, Server#server{lru = couch_lru:update(DbName, Lru)}}; 664handle_cast({update_lru, _DbName}, Server) -> 665 {noreply, Server}; 666handle_cast({close_db_if_idle, DbName}, Server) -> 667 case ets:update_element(couch_dbs(Server), DbName, {#entry.lock, locked}) of 668 true -> 669 [#entry{db = Db, db_options = DbOpts}] = ets:lookup(couch_dbs(Server), DbName), 670 case couch_db:is_idle(Db) of 671 true -> 672 DbPid = couch_db:get_pid(Db), 673 true = ets:delete(couch_dbs(Server), DbName), 674 true = ets:delete(couch_dbs_pid_to_name(Server), DbPid), 675 exit(DbPid, kill), 676 {noreply, db_closed(Server, DbOpts)}; 677 false -> 678 true = ets:update_element( 679 couch_dbs(Server), DbName, {#entry.lock, unlocked}), 680 {noreply, Server} 681 end; 682 false -> 683 {noreply, Server} 684 end; 685 686handle_cast(Msg, Server) -> 687 {stop, {unknown_cast_message, Msg}, Server}. 688 689code_change(_OldVsn, #server{}=State, _Extra) -> 690 {ok, State}. 691 692handle_info({'EXIT', _Pid, config_change}, Server) -> 693 {stop, config_change, Server}; 694handle_info({'EXIT', Pid, Reason}, Server) -> 695 case ets:lookup(couch_dbs_pid_to_name(Server), Pid) of 696 [{Pid, DbName}] -> 697 [#entry{waiters = Waiters} = Entry] = ets:lookup(couch_dbs(Server), DbName), 698 if Reason /= snappy_nif_not_loaded -> ok; true -> 699 Msg = io_lib:format("To open the database `~s`, Apache CouchDB " 700 "must be built with Erlang OTP R13B04 or higher.", [DbName]), 701 couch_log:error(Msg, []) 702 end, 703 % We kill databases on purpose so there's no reason 704 % to log that fact. So we restrict logging to "interesting" 705 % reasons. 706 if Reason == normal orelse Reason == killed -> ok; true -> 707 couch_log:info("db ~s died with reason ~p", [DbName, Reason]) 708 end, 709 if not is_list(Waiters) -> ok; true -> 710 [gen_server:reply(Waiter, Reason) || Waiter <- Waiters] 711 end, 712 true = ets:delete(couch_dbs(Server), DbName), 713 true = ets:delete(couch_dbs_pid_to_name(Server), Pid), 714 {noreply, db_closed(Server, Entry#entry.db_options)}; 715 [] -> 716 {noreply, Server} 717 end; 718handle_info({restart_config_listener, N}, State) -> 719 ok = config:listen_for_changes(?MODULE, N), 720 {noreply, State}; 721handle_info(Info, Server) -> 722 {stop, {unknown_message, Info}, Server}. 723 724db_opened(Server, Options) -> 725 case lists:member(sys_db, Options) of 726 false -> Server#server{dbs_open=Server#server.dbs_open + 1}; 727 true -> Server 728 end. 729 730db_closed(Server, Options) -> 731 case lists:member(sys_db, Options) of 732 false -> Server#server{dbs_open=Server#server.dbs_open - 1}; 733 true -> Server 734 end. 735 736validate_open_or_create(DbName, Options) -> 737 case check_dbname(DbName) of 738 ok -> 739 ok; 740 DbNameError -> 741 throw({?MODULE, DbNameError}) 742 end, 743 744 case check_engine(Options) of 745 ok -> 746 ok; 747 EngineError -> 748 throw({?MODULE, EngineError}) 749 end, 750 751 case ets:lookup(couch_dbs_locks(DbName), DbName) of 752 [] -> 753 ok; 754 [{DbName, Reason}] -> 755 throw({?MODULE, {error, {locked, Reason}}}) 756 end. 757 758get_configured_engines() -> 759 ConfigEntries = config:get("couchdb_engines"), 760 Engines = lists:flatmap(fun({Extension, ModuleStr}) -> 761 try 762 [{Extension, list_to_atom(ModuleStr)}] 763 catch _T:_R -> 764 [] 765 end 766 end, ConfigEntries), 767 case Engines of 768 [] -> 769 [{"couch", couch_bt_engine}]; 770 Else -> 771 Else 772 end. 773 774 775get_engine(Server, DbName, Options) -> 776 #server{ 777 root_dir = RootDir, 778 engines = Engines 779 } = Server, 780 case couch_util:get_value(engine, Options) of 781 Ext when is_binary(Ext) -> 782 ExtStr = binary_to_list(Ext), 783 case lists:keyfind(ExtStr, 1, Engines) of 784 {ExtStr, Engine} -> 785 Path = make_filepath(RootDir, DbName, ExtStr), 786 {ok, {Engine, Path}}; 787 false -> 788 {error, {invalid_engine_extension, Ext}} 789 end; 790 _ -> 791 get_engine(Server, DbName) 792 end. 793 794 795get_engine(Server, DbName) -> 796 #server{ 797 root_dir = RootDir, 798 engines = Engines 799 } = Server, 800 Possible = get_possible_engines(DbName, RootDir, Engines), 801 case Possible of 802 [] -> 803 get_default_engine(Server, DbName); 804 [Engine] -> 805 {ok, Engine}; 806 _ -> 807 erlang:error(engine_conflict) 808 end. 809 810 811get_possible_engines(DbName, RootDir, Engines) -> 812 lists:foldl(fun({Extension, Engine}, Acc) -> 813 Path = make_filepath(RootDir, DbName, Extension), 814 case couch_db_engine:exists(Engine, Path) of 815 true -> 816 [{Engine, Path} | Acc]; 817 false -> 818 Acc 819 end 820 end, [], Engines). 821 822 823get_default_engine(Server, DbName) -> 824 #server{ 825 root_dir = RootDir, 826 engines = Engines 827 } = Server, 828 Default = {couch_bt_engine, make_filepath(RootDir, DbName, "couch")}, 829 Extension = config:get("couchdb", "default_engine", ?DEFAULT_ENGINE), 830 case lists:keyfind(Extension, 1, Engines) of 831 {Extension, Module} -> 832 {ok, {Module, make_filepath(RootDir, DbName, Extension)}}; 833 false -> 834 Fmt = "Invalid storage engine extension ~s," 835 " configured engine extensions are: ~s", 836 Exts = [E || {E, _} <- Engines], 837 Args = [Extension, string:join(Exts, ", ")], 838 couch_log:error(Fmt, Args), 839 {ok, Default} 840 end. 841 842 843make_filepath(RootDir, DbName, Extension) when is_binary(RootDir) -> 844 make_filepath(binary_to_list(RootDir), DbName, Extension); 845make_filepath(RootDir, DbName, Extension) when is_binary(DbName) -> 846 make_filepath(RootDir, binary_to_list(DbName), Extension); 847make_filepath(RootDir, DbName, Extension) when is_binary(Extension) -> 848 make_filepath(RootDir, DbName, binary_to_list(Extension)); 849make_filepath(RootDir, DbName, Extension) -> 850 filename:join([RootDir, "./" ++ DbName ++ "." ++ Extension]). 851 852 853get_engine_extensions() -> 854 case config:get("couchdb_engines") of 855 [] -> 856 ["couch"]; 857 Entries -> 858 [Ext || {Ext, _Mod} <- Entries] 859 end. 860 861 862check_engine(Options) -> 863 case couch_util:get_value(engine, Options) of 864 Ext when is_binary(Ext) -> 865 ExtStr = binary_to_list(Ext), 866 Extensions = get_engine_extensions(), 867 case lists:member(ExtStr, Extensions) of 868 true -> 869 ok; 870 false -> 871 {error, {invalid_engine_extension, Ext}} 872 end; 873 _ -> 874 ok 875 end. 876 877 878get_engine_path(DbName, Engine) when is_binary(DbName), is_atom(Engine) -> 879 RootDir = config:get("couchdb", "database_dir", "."), 880 case lists:keyfind(Engine, 2, get_configured_engines()) of 881 {Ext, Engine} -> 882 {ok, make_filepath(RootDir, DbName, Ext)}; 883 false -> 884 {error, {invalid_engine, Engine}} 885 end. 886 887lock(DbName, Reason) when is_binary(DbName), is_binary(Reason) -> 888 case ets:lookup(couch_dbs(DbName), DbName) of 889 [] -> 890 true = ets:insert(couch_dbs_locks(DbName), {DbName, Reason}), 891 ok; 892 [#entry{}] -> 893 {error, already_opened} 894 end. 895 896unlock(DbName) when is_binary(DbName) -> 897 true = ets:delete(couch_dbs_locks(DbName), DbName), 898 ok. 899 900 901db_updated(Db) -> 902 DbName = couch_db:name(Db), 903 gen_server:call(couch_server(DbName), {db_updated, Db}, infinity). 904 905 906couch_server(Arg) -> 907 name("couch_server", Arg). 908 909 910couch_dbs(Arg) -> 911 name("couch_dbs", Arg). 912 913 914couch_dbs_pid_to_name(Arg) -> 915 name("couch_dbs_pid_to_name", Arg). 916 917 918couch_dbs_locks(Arg) -> 919 name("couch_dbs_locks", Arg). 920 921 922name("couch_dbs", #server{} = Server) -> 923 Server#server.couch_dbs; 924 925name("couch_dbs_pid_to_name", #server{} = Server) -> 926 Server#server.couch_dbs_pid_to_name; 927 928name("couch_dbs_locks", #server{} = Server) -> 929 Server#server.couch_dbs_locks; 930 931name(BaseName, DbName) when is_list(DbName) -> 932 name(BaseName, ?l2b(DbName)); 933 934name(BaseName, DbName) when is_binary(DbName) -> 935 N = 1 + erlang:phash2(DbName, num_servers()), 936 name(BaseName, N); 937 938name(BaseName, N) when is_integer(N), N > 0 -> 939 list_to_atom(BaseName ++ "_" ++ integer_to_list(N)). 940 941 942num_servers() -> 943 erlang:system_info(schedulers). 944 945 946aggregate_queue_len() -> 947 N = num_servers(), 948 Names = [couch_server(I) || I <- lists:seq(1, N)], 949 MQs = [process_info(whereis(Name), message_queue_len) || 950 Name <- Names], 951 lists:sum([X || {_, X} <- MQs]). 952 953 954-ifdef(TEST). 955-include_lib("eunit/include/eunit.hrl"). 956 957setup_all() -> 958 ok = meck:new(config, [passthrough]), 959 ok = meck:expect(config, get, fun config_get/3), 960 ok. 961 962teardown_all(_) -> 963 meck:unload(). 964 965config_get("couchdb", "users_db_suffix", _) -> "users_db"; 966config_get(_, _, _) -> undefined. 967 968maybe_add_sys_db_callbacks_pass_test_() -> 969 { 970 setup, 971 fun setup_all/0, 972 fun teardown_all/1, 973 [ 974 fun should_add_sys_db_callbacks/0, 975 fun should_not_add_sys_db_callbacks/0 976 ] 977 }. 978 979should_add_sys_db_callbacks() -> 980 Cases = [ 981 "shards/00000000-3fffffff/foo/users_db.1415960794.couch", 982 "shards/00000000-3fffffff/foo/users_db.1415960794", 983 "shards/00000000-3fffffff/foo/users_db", 984 "shards/00000000-3fffffff/users_db.1415960794.couch", 985 "shards/00000000-3fffffff/users_db.1415960794", 986 "shards/00000000-3fffffff/users_db", 987 988 "shards/00000000-3fffffff/_users.1415960794.couch", 989 "shards/00000000-3fffffff/_users.1415960794", 990 "shards/00000000-3fffffff/_users", 991 992 "foo/users_db.couch", 993 "foo/users_db", 994 "users_db.couch", 995 "users_db", 996 "foo/_users.couch", 997 "foo/_users", 998 "_users.couch", 999 "_users", 1000 1001 "shards/00000000-3fffffff/foo/_replicator.1415960794.couch", 1002 "shards/00000000-3fffffff/foo/_replicator.1415960794", 1003 "shards/00000000-3fffffff/_replicator", 1004 "foo/_replicator.couch", 1005 "foo/_replicator", 1006 "_replicator.couch", 1007 "_replicator" 1008 ], 1009 lists:foreach(fun(DbName) -> 1010 check_case(DbName, true), 1011 check_case(?l2b(DbName), true) 1012 end, Cases). 1013 1014should_not_add_sys_db_callbacks() -> 1015 Cases = [ 1016 "shards/00000000-3fffffff/foo/mydb.1415960794.couch", 1017 "shards/00000000-3fffffff/foo/mydb.1415960794", 1018 "shards/00000000-3fffffff/mydb", 1019 "foo/mydb.couch", 1020 "foo/mydb", 1021 "mydb.couch", 1022 "mydb" 1023 ], 1024 lists:foreach(fun(DbName) -> 1025 check_case(DbName, false), 1026 check_case(?l2b(DbName), false) 1027 end, Cases). 1028 1029check_case(DbName, IsAdded) -> 1030 Options = maybe_add_sys_db_callbacks(DbName, [other_options]), 1031 ?assertEqual(IsAdded, lists:member(sys_db, Options)). 1032 1033-endif. 1034