From 444920e2be3d7714c7a8a4c29c28313e97deef8f Mon Sep 17 00:00:00 2001 From: Nick Vatamaniuc Date: Tue, 14 Apr 2020 17:43:04 -0400 Subject: Allow using cached security and revs_limit properties By default, transactions are used to check metadata, and possibly reopen the db, to get a current db handle. However, if a `max_age` option is provided and db handle was checked less than `max_age` milliseconds ago, use properties from that cached handle instead. The main use of this feature be in pluggable authorization handlers where it might be necessary to inspect the security doc multiple times for the same request before a final decision is made. `revs_limit/1` was updated as well, mainly for consistency since it is almost identical to `get_security/1`. --- src/fabric/src/fabric2_db.erl | 36 ++++++++++++++++++++++++------- src/fabric/src/fabric2_fdb.erl | 8 +++++-- src/fabric/test/fabric2_db_misc_tests.erl | 34 ++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src/fabric/src/fabric2_db.erl b/src/fabric/src/fabric2_db.erl index 9b9efdac2..b94226190 100644 --- a/src/fabric/src/fabric2_db.erl +++ b/src/fabric/src/fabric2_db.erl @@ -50,7 +50,9 @@ get_instance_start_time/1, get_pid/1, get_revs_limit/1, + get_revs_limit/2, get_security/1, + get_security/2, get_update_seq/1, get_user_ctx/1, get_uuid/1, @@ -500,17 +502,21 @@ get_pid(#{}) -> get_revs_limit(#{} = Db) -> - #{revs_limit := RevsLimit} = fabric2_fdb:transactional(Db, fun(TxDb) -> - fabric2_fdb:ensure_current(TxDb) - end), - RevsLimit. + get_revs_limit(Db, []). + + +get_revs_limit(#{} = Db, Opts) -> + CurrentDb = get_cached_db(Db, Opts), + maps:get(revs_limit, CurrentDb). get_security(#{} = Db) -> - #{security_doc := SecDoc} = fabric2_fdb:transactional(Db, fun(TxDb) -> - fabric2_fdb:ensure_current(TxDb) - end), - SecDoc. + get_security(Db, []). + + +get_security(#{} = Db, Opts) -> + CurrentDb = get_cached_db(Db, Opts), + maps:get(security_doc, CurrentDb). get_update_seq(#{} = Db) -> @@ -2037,3 +2043,17 @@ open_json_doc(Db, DocId, OpenOpts, DocOpts) -> {ok, #doc{} = Doc} -> [{doc, couch_doc:to_json_obj(Doc, DocOpts)}] end. + + +get_cached_db(#{} = Db, Opts) when is_list(Opts) -> + MaxAge = fabric2_util:get_value(max_age, Opts, 0), + Now = erlang:monotonic_time(millisecond), + Age = Now - maps:get(check_current_ts, Db), + case Age < MaxAge of + true -> + Db; + false -> + fabric2_fdb:transactional(Db, fun(TxDb) -> + fabric2_fdb:ensure_current(TxDb) + end) + end. diff --git a/src/fabric/src/fabric2_fdb.erl b/src/fabric/src/fabric2_fdb.erl index d96c3ae60..bc0a8458c 100644 --- a/src/fabric/src/fabric2_fdb.erl +++ b/src/fabric/src/fabric2_fdb.erl @@ -231,6 +231,7 @@ create(#{} = Db0, Options) -> revs_limit => 1000, security_doc => {[]}, user_ctx => UserCtx, + check_current_ts => erlang:monotonic_time(millisecond), validate_doc_update_funs => [], before_doc_update => undefined, @@ -272,6 +273,7 @@ open(#{} = Db0, Options) -> security_doc => {[]}, user_ctx => UserCtx, + check_current_ts => erlang:monotonic_time(millisecond), % Place holders until we implement these % bits. @@ -1273,8 +1275,10 @@ check_db_version(#{} = Db, CheckDbVersion) -> case erlfdb:wait(erlfdb:get(Tx, DbVersionKey)) of DbVersion -> put(?PDICT_CHECKED_DB_IS_CURRENT, true), - on_commit(Tx, fun() -> fabric2_server:store(Db) end), - Db; + Now = erlang:monotonic_time(millisecond), + Db1 = Db#{check_current_ts := Now}, + on_commit(Tx, fun() -> fabric2_server:store(Db1) end), + Db1; _NewDBVersion -> fabric2_server:remove(maps:get(name, Db)), throw({?MODULE, reopen}) diff --git a/src/fabric/test/fabric2_db_misc_tests.erl b/src/fabric/test/fabric2_db_misc_tests.erl index 19599823e..9c95ca565 100644 --- a/src/fabric/test/fabric2_db_misc_tests.erl +++ b/src/fabric/test/fabric2_db_misc_tests.erl @@ -38,6 +38,7 @@ misc_test_() -> ?TDEF(accessors), ?TDEF(set_revs_limit), ?TDEF(set_security), + ?TDEF(get_security_cached), ?TDEF(is_system_db), ?TDEF(validate_dbname), ?TDEF(validate_doc_ids), @@ -113,6 +114,30 @@ set_security({DbName, Db, _}) -> ?assertEqual(SecObj, fabric2_db:get_security(Db2)). +get_security_cached({DbName, Db, _}) -> + OldSecObj = fabric2_db:get_security(Db), + SecObj = {[ + {<<"admins">>, {[ + {<<"names">>, [<<"foo1">>]}, + {<<"roles">>, []} + ]}} + ]}, + + % Set directly so we don't auto-update the local cache + {ok, Db1} = fabric2_db:open(DbName, [?ADMIN_CTX]), + ?assertMatch({ok, #{}}, fabric2_fdb:transactional(Db1, fun(TxDb) -> + fabric2_fdb:set_config(TxDb, security_doc, SecObj) + end)), + + {ok, Db2} = fabric2_db:open(DbName, [?ADMIN_CTX]), + ?assertEqual(OldSecObj, fabric2_db:get_security(Db2, [{max_age, 1000}])), + + timer:sleep(100), + ?assertEqual(SecObj, fabric2_db:get_security(Db2, [{max_age, 50}])), + + ?assertEqual(ok, fabric2_db:set_security(Db2, OldSecObj)). + + is_system_db({DbName, Db, _}) -> ?assertEqual(false, fabric2_db:is_system_db(Db)), ?assertEqual(false, fabric2_db:is_system_db_name("foo")), @@ -305,12 +330,19 @@ metadata_bump({DbName, _, _}) -> erlfdb:wait(erlfdb:get(Tx, ?METADATA_VERSION_KEY)) end), + % Save timetamp before ensure_current/1 is called + TsBeforeEnsureCurrent = erlang:monotonic_time(millisecond), + % Perform a random operation which calls ensure_current {ok, _} = fabric2_db:get_db_info(Db), % Check that db handle in the cache got the new metadata version + % and that check_current_ts was updated CachedDb = fabric2_server:fetch(DbName, undefined), - ?assertMatch(#{md_version := NewMDVersion}, CachedDb). + ?assertMatch(#{ + md_version := NewMDVersion, + check_current_ts := Ts + } when Ts >= TsBeforeEnsureCurrent, CachedDb). db_version_bump({DbName, _, _}) -> -- cgit v1.2.1