You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ei...@apache.org on 2020/05/07 13:24:35 UTC
[couchdb] branch aegis_expiring_cache updated (d529bc5 -> 8e91e63)
This is an automated email from the ASF dual-hosted git repository.
eiri pushed a change to branch aegis_expiring_cache
in repository https://gitbox.apache.org/repos/asf/couchdb.git.
discard d529bc5 Convert key cache to be LRU
omit 2b39a34 Fix aegis_server multidb test, allow waiters to properly accumulate
omit 2136bb0 Switch to db UUID for cache's key
omit 9413100 Remove example key manager, add noop key manager, make it default
omit 2ec4bc8 Change key manager response on disabled encryption to `false`.
omit 1f05a44 Make example key manager use pbkdf2 to derive root key
omit d17604b Convert aegis_key_manager into gen server
omit c971c3e Store unwrappers Ref as a control for DbKey legitimacy
omit 25022fd Support disabling encryption per database on key manager level
omit 07178a9 Pass db options to generate_key and rename WrappedKey to AegisConfig
omit 6069181 Change rebar deps build order to allow aegis find fabric2.hrl header file
omit 0880fdc Address review comments
omit 061b6b7 Refactoring: extract maybe_spawn_unwrapper into own function
omit b384ddf Rename clients to openers
omit 0b96aba Rename unwrap_key to maybe_rewrap_key to clarify fun propose
omit a3574fe Make encrypt/decrypt workers to reply client directly
omit 7b78636 Formatting: move private functions into own section
omit f8ac2b2 Extract aegis_keywrap parts in shim of key manager
omit 80d75e8 Return error if can't unwrap a key
omit a76694c Fail tests on gen_server timeout
omit 9916ac2 Make fabric depend on aegis
omit 5ad9226 Make aegis into app and add key cache server
omit a8c9d8e Move rebar.config.script from couch to aegis
omit 14b1e25 Fix typo in configure
omit 34c5b85 Add encryption for database values
add 2ba98a8 Return better responses for endpoints which are not implemented
add 1be2363 Fix POST _all_docs/queries endpoint
add e71a77d Do not allow editing _security in _user database
add 1d6799f Start running chttpd eunit tests
add 0d1cf61 Support soft-deletion in fabric level
add ec12e1f Support soft-deletion in chttpd level
add 6c1d7a9 Merge pull request #2666 from apache/db-softdeletion
add d6ec993 Compress doc bodies and attachments
add a14f62d Add mango_plugin
add 396a3b5 Merge pull request #2767 from cloudant/prototype/fdb-layer-mango-plugin
add 56137f3 Fix job removal notifications
add daf1082 Fix division by zero
add cbad08d Make 'make check' run all the passing FDB tests on this branch
add 2bb0ccd Fix incorrect usage of couch_epi in mango plugin
add aad871b Merge pull request #2775 from cloudant/mango-plugin-fixup
add d4bc3a5 Fix flaky fabric2_index test
add 742c64e Fix index updater configuration keys
add 247b809 Rename variables to indicate transaction state
add 3c0a017 Move process_db/1 to match the logical progression
add 3e1c822 Update to use `fabric2_db:get_design_docs/1`
add 7bc9148 Extend fabric2_index callbacks for index cleanup
add e0d0391 Implement couch_views:cleanup_indices/2
add 4275a49 Implement _view_cleanup for FoundationDB
add 7aeb54b Optionally cleanup stale indices automatically
add 2e5a556 Remove jobs on index cleanup
add 30fdef7 Remove failed view jobs
add 7575152 Implement couch_views_cleanup_test.erl
add 9da549e Integrate emilio - erang linter
add 6bc6f9c Merge pull request #2789 from cloudant/fdb-integrate-emilio
add 3636451 Enable configurable binary chunk size
add 27cbad7 report changes stats intermittently (#2777)
add 0eb1a73 Refactor expiring cache FDB interface
add 4098c12 Clean up old expiry key on update insert
add 4e2f18c Merge keys from rebar.config
add a527ad1 Merge pull request #2783 from cloudant/merge-rebar-config
add f71c4c0 Allow using cached security and revs_limit properties
add 0b8dfa6 Fetch doc in same transaction as _all_doc row
add 45e0c30 Fix typo in error message
add 8554329 Merge pull request #2796 from cloudant/fix-typo
add 3895223 Add after_interactive_write plugin to couch_views_updater
add f522b88 Refactor fetching rev code in fabric2_fdb
add 5efcbfc Add fold_docs for DocId list
add 232e1d5 Report the chttpd_auth authentication db in session info
add a5fded8 Add native encryption support
add 43da896 Merge pull request #2826 from apache/aegis
add 21bb444 Add a couch_views test for multiple design documents with the same map
add 0a74954 Update erlfdb to v1.1.0
add b07a629 Allow specifying FDB transaction options
add e889cf0 Fix mango test suite
add 19d8582 Remove etag from changes and _list_dbs
add c6d3a7b Temporary disable fabric2_tx_options_tests
add cf0b032 Re-enable the tx options tests
add 97227c4 Improve robustness of couch expiring cache test
add e48da92 Fix a flaky fdbcore index test
add 45a899a Fix list_dbs_info_tx_too_old flaky test
add bd44fc6 return correct not implemented for reduce
add 7e7a3f6 add test to make sure type <<"text">> design docs are ignored (#2866)
add 577be65 Re-enable ExUnit tests
add 51b8cc1 Update erlfdb
add 25bf5cb Merge pull request #2874 from cloudant/enable-exunit
new 8e91e63 Convert aegis key cach to LRU with hard expiration time
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (d529bc5)
\
N -- N -- N refs/heads/aegis_expiring_cache (8e91e63)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.gitignore | 2 +
Makefile | 19 +-
Makefile.win | 7 +-
bin/warnings_in_scope | 125 ++++++
build-aux/Jenkinsfile.pr | 2 +-
configure | 13 +
configure.ps1 | 14 +
emilio.config | 20 +
rebar.config.script | 11 +-
rel/overlay/etc/default.ini | 68 ++-
src/aegis/src/aegis.erl | 42 +-
src/aegis/src/aegis_key_manager.erl | 99 +----
src/aegis/src/aegis_noop_key_manager.erl | 15 +-
src/aegis/src/aegis_server.erl | 463 ++++++++++-----------
src/aegis/src/aegis_sup.erl | 5 -
src/aegis/test/aegis_key_manager_test.erl | 84 ----
src/aegis/test/aegis_server_test.erl | 305 +++++++-------
src/chttpd/src/chttpd.erl | 15 +-
src/chttpd/src/chttpd_db.erl | 90 ++--
src/chttpd/src/chttpd_httpd_handlers.erl | 40 +-
src/chttpd/src/chttpd_misc.erl | 136 ++++--
src/chttpd/src/chttpd_stats.erl | 96 ++++-
src/chttpd/src/chttpd_test_util.erl | 2 +-
src/chttpd/src/chttpd_view.erl | 20 +-
.../eunit/chttpd_db_bulk_get_multipart_test.erl | 31 +-
src/chttpd/test/eunit/chttpd_db_bulk_get_test.erl | 30 +-
src/chttpd/test/eunit/chttpd_db_test.erl | 38 +-
src/chttpd/test/eunit/chttpd_dbs_info_test.erl | 13 +-
src/chttpd/test/eunit/chttpd_deleted_dbs_test.erl | 234 +++++++++++
.../test/eunit/chttpd_open_revs_error_test.erl | 112 -----
src/chttpd/test/eunit/chttpd_purge_tests.erl | 6 +-
src/chttpd/test/eunit/chttpd_security_tests.erl | 57 +--
src/chttpd/test/eunit/chttpd_session_tests.erl | 74 ++++
src/chttpd/test/eunit/chttpd_stats_tests.erl | 77 ++++
.../test/eunit/chttpd_test.hrl} | 2 +
src/chttpd/test/eunit/chttpd_view_test.erl | 4 +-
src/couch/.gitignore | 2 +
src/couch/rebar.config.script | 7 +-
src/couch/src/couch_db.erl | 2 +
src/couch/src/couch_httpd_auth.erl | 3 +-
.../src/couch_expiring_cache_fdb.erl | 87 ++--
.../src/couch_expiring_cache_server.erl | 12 +-
.../test/couch_expiring_cache_tests.erl | 106 +++--
src/couch_jobs/src/couch_jobs_fdb.erl | 4 +
src/couch_jobs/test/couch_jobs_tests.erl | 28 ++
src/couch_rate/src/couch_rate_limiter.erl | 2 +
src/couch_views/src/couch_views.erl | 21 +-
src/couch_views/src/couch_views_epi.erl | 4 +-
src/couch_views/src/couch_views_fdb.erl | 40 +-
src/couch_views/src/couch_views_indexer.erl | 2 +-
src/couch_views/src/couch_views_jobs.erl | 35 +-
src/couch_views/src/couch_views_plugin.erl | 40 ++
src/couch_views/src/couch_views_updater.erl | 5 +-
src/couch_views/test/couch_views_cleanup_test.erl | 411 ++++++++++++++++++
src/couch_views/test/couch_views_indexer_test.erl | 90 +++-
src/couch_views/test/couch_views_map_test.erl | 2 +-
src/fabric/include/fabric2.hrl | 9 +-
src/fabric/src/fabric2_db.erl | 332 ++++++++++++++-
src/fabric/src/fabric2_fdb.erl | 237 +++++++++--
src/fabric/src/fabric2_index.erl | 73 ++--
src/fabric/src/fabric2_server.erl | 109 ++++-
src/fabric/src/fabric2_util.erl | 24 ++
src/fabric/test/fabric2_db_crud_tests.erl | 235 ++++++++++-
src/fabric/test/fabric2_db_misc_tests.erl | 34 +-
src/fabric/test/fabric2_doc_att_tests.erl | 52 ++-
src/fabric/test/fabric2_index_tests.erl | 10 +-
src/fabric/test/fabric2_tx_options_tests.erl | 103 +++++
src/mango/src/mango_epi.erl | 4 +-
src/mango/src/mango_httpd.erl | 20 +-
src/mango/src/mango_idx.erl | 3 +-
.../src/mango_plugin.erl} | 40 +-
src/mango/test/16-index-selectors-test.py | 8 +
test/elixir/test/all_docs_test.exs | 30 +-
73 files changed, 3394 insertions(+), 1203 deletions(-)
create mode 100755 bin/warnings_in_scope
create mode 100644 emilio.config
delete mode 100644 src/aegis/test/aegis_key_manager_test.erl
create mode 100644 src/chttpd/test/eunit/chttpd_deleted_dbs_test.erl
delete mode 100644 src/chttpd/test/eunit/chttpd_open_revs_error_test.erl
create mode 100644 src/chttpd/test/eunit/chttpd_session_tests.erl
create mode 100644 src/chttpd/test/eunit/chttpd_stats_tests.erl
copy src/{fabric/test/fabric2_test.hrl => chttpd/test/eunit/chttpd_test.hrl} (97%)
create mode 100644 src/couch_views/src/couch_views_plugin.erl
create mode 100644 src/couch_views/test/couch_views_cleanup_test.erl
create mode 100644 src/fabric/test/fabric2_tx_options_tests.erl
copy src/{global_changes/src/global_changes_plugin.erl => mango/src/mango_plugin.erl} (59%)
[couchdb] 01/01: Convert aegis key cach to LRU with hard expiration
time
Posted by ei...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
eiri pushed a commit to branch aegis_expiring_cache
in repository https://gitbox.apache.org/repos/asf/couchdb.git
commit 8e91e639b4a73b4f48828f1616d978307695dc9d
Author: Eric Avdey <ei...@eiri.ca>
AuthorDate: Tue May 5 16:38:57 2020 -0300
Convert aegis key cach to LRU with hard expiration time
---
src/aegis/src/aegis_server.erl | 178 ++++++++++++++++++++++++++++++-----
src/aegis/test/aegis_server_test.erl | 149 +++++++++++++++++++++++++++++
src/fabric/src/fabric2_util.erl | 8 ++
3 files changed, 309 insertions(+), 26 deletions(-)
diff --git a/src/aegis/src/aegis_server.erl b/src/aegis/src/aegis_server.erl
index be8202c..ed576c5 100644
--- a/src/aegis/src/aegis_server.erl
+++ b/src/aegis/src/aegis_server.erl
@@ -40,13 +40,16 @@
]).
-
-define(KEY_CHECK, aegis_key_check).
-define(INIT_TIMEOUT, 60000).
-define(TIMEOUT, 10000).
+-define(CACHE_LIMIT, 100000).
+-define(CACHE_MAX_AGE_SEC, 1800).
+-define(CACHE_EXPIRATION_CHECK_SEC, 10).
+-define(LAST_ACCESSED_INACTIVITY_SEC, 10).
--record(entry, {uuid, encryption_key}).
+-record(entry, {uuid, encryption_key, counter, last_accessed, expires_at}).
start_link() ->
@@ -84,8 +87,10 @@ encrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
uuid := UUID
} = Db,
- case ets:member(?KEY_CHECK, UUID) of
- true ->
+ Now = fabric2_util:now(sec),
+
+ case ets:lookup(?KEY_CHECK, UUID) of
+ [{UUID, ExpiresAt}] when ExpiresAt >= Now ->
case gen_server:call(?MODULE, {encrypt, Db, Key, Value}) of
CipherText when is_binary(CipherText) ->
CipherText;
@@ -95,7 +100,7 @@ encrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
{error, Reason} ->
erlang:error(Reason)
end;
- false ->
+ _ ->
process_flag(sensitive, true),
{ok, DbKey} = do_open_db(Db),
@@ -109,8 +114,10 @@ decrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
uuid := UUID
} = Db,
- case ets:member(?KEY_CHECK, UUID) of
- true ->
+ Now = fabric2_util:now(sec),
+
+ case ets:lookup(?KEY_CHECK, UUID) of
+ [{UUID, ExpiresAt}] when ExpiresAt >= Now ->
case gen_server:call(?MODULE, {decrypt, Db, Key, Value}) of
PlainText when is_binary(PlainText) ->
PlainText;
@@ -120,7 +127,7 @@ decrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
{error, Reason} ->
erlang:error(Reason)
end;
- false ->
+ _ ->
process_flag(sensitive, true),
{ok, DbKey} = do_open_db(Db),
@@ -133,10 +140,16 @@ decrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
init([]) ->
process_flag(sensitive, true),
Cache = ets:new(?MODULE, [set, private, {keypos, #entry.uuid}]),
+ ByAccess = ets:new(?MODULE,
+ [ordered_set, private, {keypos, #entry.counter}]),
ets:new(?KEY_CHECK, [named_table, protected, {read_concurrency, true}]),
+ erlang:send_after(0, self(), maybe_remove_expired),
+
St = #{
- cache => Cache
+ cache => Cache,
+ by_access => ByAccess,
+ counter => 0
},
{ok, St, ?INIT_TIMEOUT}.
@@ -145,16 +158,13 @@ terminate(_Reason, _St) ->
ok.
-handle_call({insert_key, UUID, DbKey}, _From, #{cache := Cache} = St) ->
- ok = insert(Cache, UUID, DbKey),
- {reply, ok, St, ?TIMEOUT};
+handle_call({insert_key, UUID, DbKey}, _From, St) ->
+ NewSt = insert(St, UUID, DbKey),
+ {reply, ok, NewSt, ?TIMEOUT};
handle_call({encrypt, #{uuid := UUID} = Db, Key, Value}, From, St) ->
- #{
- cache := Cache
- } = St,
- {ok, DbKey} = lookup(Cache, UUID),
+ {ok, DbKey} = lookup(St, UUID),
erlang:spawn(fun() ->
process_flag(sensitive, true),
@@ -172,11 +182,8 @@ handle_call({encrypt, #{uuid := UUID} = Db, Key, Value}, From, St) ->
{noreply, St, ?TIMEOUT};
handle_call({decrypt, #{uuid := UUID} = Db, Key, Value}, From, St) ->
- #{
- cache := Cache
- } = St,
- {ok, DbKey} = lookup(Cache, UUID),
+ {ok, DbKey} = lookup(St, UUID),
erlang:spawn(fun() ->
process_flag(sensitive, true),
@@ -197,10 +204,21 @@ handle_call(_Msg, _From, St) ->
{noreply, St}.
+handle_cast({accessed, UUID}, St) ->
+ NewSt = bump_last_accessed(St, UUID),
+ {noreply, NewSt};
+
+
handle_cast(_Msg, St) ->
{noreply, St}.
+handle_info(maybe_remove_expired, St) ->
+ remove_expired_entries(St),
+ CheckInterval = timer:seconds(expiration_check_interval()),
+ erlang:send_after(CheckInterval, self(), maybe_remove_expired),
+ {noreply, St};
+
handle_info(_Msg, St) ->
{noreply, St}.
@@ -259,17 +277,125 @@ do_decrypt(DbKey, #{uuid := UUID}, Key, Value) ->
%% cache functions
-insert(Cache, UUID, DbKey) ->
- Entry = #entry{uuid = UUID, encryption_key = DbKey},
+insert(St, UUID, DbKey) ->
+ #{
+ cache := Cache,
+ by_access := ByAccess,
+ counter := Counter
+ } = St,
+
+ Now = fabric2_util:now(sec),
+ ExpiresAt = Now + max_age(),
+
+ Entry = #entry{
+ uuid = UUID,
+ encryption_key = DbKey,
+ counter = Counter,
+ last_accessed = Now,
+ expires_at = ExpiresAt
+ },
+
true = ets:insert(Cache, Entry),
- true = ets:insert(?KEY_CHECK, {UUID, true}),
- ok.
+ true = ets:insert_new(ByAccess, Entry),
+ true = ets:insert(?KEY_CHECK, {UUID, ExpiresAt}),
+
+ maybe_evict_old_entries(St),
+ St#{counter := Counter + 1}.
-lookup(Cache, UUID) ->
+
+lookup(#{cache := Cache}, UUID) ->
case ets:lookup(Cache, UUID) of
- [#entry{uuid = UUID, encryption_key = DbKey}] ->
+ [#entry{uuid = UUID, encryption_key = DbKey} = Entry] ->
+ maybe_bump_last_accessed(Entry),
{ok, DbKey};
[] ->
{error, not_found}
end.
+
+
+maybe_bump_last_accessed(#entry{last_accessed = LastAccessed} = Entry) ->
+ case fabric2_util:now(sec) > LastAccessed + ?LAST_ACCESSED_INACTIVITY_SEC of
+ true ->
+ gen_server:cast(?MODULE, {accessed, Entry#entry.uuid});
+ false ->
+ ok
+ end.
+
+
+bump_last_accessed(St, UUID) ->
+ #{
+ cache := Cache,
+ by_access := ByAccess,
+ counter := Counter
+ } = St,
+
+
+ [#entry{counter = OldCounter} = Entry0] = ets:lookup(Cache, UUID),
+
+ Entry = Entry0#entry{
+ last_accessed = fabric2_util:now(sec),
+ counter = Counter
+ },
+
+ true = ets:insert(Cache, Entry),
+ true = ets:insert_new(ByAccess, Entry),
+
+ ets:delete(ByAccess, OldCounter),
+
+ St#{counter := Counter + 1}.
+
+
+maybe_evict_old_entries(#{cache := Cache} = St) ->
+ CacheLimit = cache_limit(),
+ CacheSize = ets:info(Cache, size),
+ evict_old_entries(St, CacheSize - CacheLimit).
+
+
+evict_old_entries(St, N) when N > 0 ->
+ #{
+ cache := Cache,
+ by_access := ByAccess
+ } = St,
+
+ OldestKey = ets:first(ByAccess),
+ [#entry{uuid = UUID}] = ets:lookup(ByAccess, OldestKey),
+ true = ets:delete(?KEY_CHECK, UUID),
+ true = ets:delete(Cache, UUID),
+ true = ets:delete(ByAccess, OldestKey),
+ evict_old_entries(St, N - 1);
+
+evict_old_entries(_St, _) ->
+ ok.
+
+
+remove_expired_entries(St) ->
+ #{
+ cache := Cache,
+ by_access := ByAccess
+ } = St,
+
+ MatchConditions = [{'=<', '$1', fabric2_util:now(sec)}],
+
+ KeyCheckMatchHead = {'_', '$1'},
+ KeyCheckExpired = [{KeyCheckMatchHead, MatchConditions, [true]}],
+ Count = ets:select_delete(?KEY_CHECK, KeyCheckExpired),
+
+ CacheMatchHead = #entry{expires_at = '$1', _ = '_'},
+ CacheExpired = [{CacheMatchHead, MatchConditions, [true]}],
+ Count = ets:select_delete(Cache, CacheExpired),
+ Count = ets:select_delete(ByAccess, CacheExpired).
+
+
+
+max_age() ->
+ config:get_integer("aegis", "cache_max_age_sec", ?CACHE_MAX_AGE_SEC).
+
+
+expiration_check_interval() ->
+ config:get_integer(
+ "aegis", "cache_expiration_check_sec", ?CACHE_EXPIRATION_CHECK_SEC).
+
+
+cache_limit() ->
+ config:get_integer("aegis", "cache_limit", ?CACHE_LIMIT).
diff --git a/src/aegis/test/aegis_server_test.erl b/src/aegis/test/aegis_server_test.erl
index 0f23a3f..0f96798 100644
--- a/src/aegis/test/aegis_server_test.erl
+++ b/src/aegis/test/aegis_server_test.erl
@@ -163,3 +163,152 @@ test_disabled_decrypt() ->
Db = ?DB#{is_encrypted => aegis_server:open_db(?DB)},
Decrypted = aegis:decrypt(Db, <<1:64>>, ?ENCRYPTED),
?assertEqual(?ENCRYPTED, Decrypted).
+
+
+
+lru_cache_with_expiration_test_() ->
+ {
+ foreach,
+ fun() ->
+ %% this has to be be set before start of aegis server
+ %% for config param "cache_expiration_check_sec" to be picked up
+ meck:new([config, aegis_server, fabric2_util], [passthrough]),
+ ok = meck:expect(config, get_integer, fun
+ ("aegis", "cache_limit", _) -> 5;
+ ("aegis", "cache_max_age_sec", _) -> 130;
+ ("aegis", "cache_expiration_check_sec", _) -> 1;
+ (_, _, Default) -> Default
+ end),
+ Ctx = setup(),
+ ok = meck:expect(fabric2_util, now, fun(sec) ->
+ get(time) == undefined andalso put(time, 10),
+ Now = get(time),
+ put(time, Now + 10),
+ Now
+ end),
+ Ctx
+ end,
+ fun teardown/1,
+ [
+ {"counter moves forward on access bump",
+ {timeout, ?TIMEOUT, fun test_advance_counter/0}},
+ {"oldest entries evicted",
+ {timeout, ?TIMEOUT, fun test_evict_old_entries/0}},
+ {"access bump preserves entries",
+ {timeout, ?TIMEOUT, fun test_bump_accessed/0}},
+ {"expired entries removed",
+ {timeout, ?TIMEOUT, fun test_remove_expired/0}}
+ ]
+ }.
+
+
+test_advance_counter() ->
+ ?assertEqual(0, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)),
+
+ ok = meck:expect(aegis_server, handle_cast, fun({accessed, _} = Msg, St) ->
+ #{counter := Counter} = St,
+ get(counter) == undefined andalso put(counter, 0),
+ OldCounter = get(counter),
+ put(counter, Counter),
+ ?assert(Counter > OldCounter),
+ meck:passthrough([Msg, St])
+ end),
+
+ lists:foreach(fun(I) ->
+ Db = ?DB#{uuid => <<I:64>>},
+ aegis_server:encrypt(Db, <<I:64>>, ?VALUE),
+ aegis_server:encrypt(Db, <<(I+1):64>>, ?VALUE)
+ end, lists:seq(1, 10)),
+
+ ?assertEqual(10, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)).
+
+
+test_evict_old_entries() ->
+ ?assertEqual(0, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)),
+
+ %% overflow cache
+ lists:foreach(fun(I) ->
+ Db = ?DB#{uuid => <<I:64>>},
+ aegis_server:encrypt(Db, <<I:64>>, ?VALUE)
+ end, lists:seq(1, 10)),
+
+ ?assertEqual(10, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)),
+
+ %% confirm that newest keys are still in cache
+ lists:foreach(fun(I) ->
+ Db = ?DB#{uuid => <<I:64>>},
+ aegis_server:encrypt(Db, <<(I+1):64>>, ?VALUE)
+ end, lists:seq(6, 10)),
+
+ ?assertEqual(10, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)),
+
+ %% confirm that oldest keys been eviced and needed re-fetch
+ lists:foreach(fun(I) ->
+ Db = ?DB#{uuid => <<I:64>>},
+ aegis_server:encrypt(Db, <<(I+1):64>>, ?VALUE)
+ end, lists:seq(1, 5)),
+
+ ?assertEqual(15, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)).
+
+
+test_bump_accessed() ->
+ ?assertEqual(0, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)),
+
+ %% fill the cache
+ lists:foreach(fun(I) ->
+ Db = ?DB#{uuid => <<I:64>>},
+ aegis_server:encrypt(Db, <<I:64>>, ?VALUE)
+ end, lists:seq(1, 5)),
+
+ ?assertEqual(5, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)),
+
+ %% bump oldest key and then insert a new key to trigger eviction
+ aegis_server:encrypt(?DB#{uuid => <<1:64>>}, <<1:64>>, ?VALUE),
+ aegis_server:encrypt(?DB#{uuid => <<6:64>>}, <<6:64>>, ?VALUE),
+ ?assertEqual(6, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)),
+
+ %% confirm that former oldest key is still in cache
+ aegis_server:encrypt(?DB#{uuid => <<1:64>>}, <<2:64>>, ?VALUE),
+ ?assertEqual(6, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)),
+
+ %% confirm that the second oldest key been evicted by the new insert
+ aegis_server:encrypt(?DB#{uuid => <<2:64>>}, <<3:64>>, ?VALUE),
+ ?assertEqual(7, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)).
+
+
+test_remove_expired() ->
+ ?assertEqual(0, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)),
+
+ %% to detect when maybe_remove_expired called
+ ok = meck:expect(aegis_server, handle_info,fun
+ (maybe_remove_expired, St) ->
+ meck:passthrough([maybe_remove_expired, St])
+ end),
+
+ %% fill the cache. first key expires a 140, last at 180 of "our" time
+ lists:foreach(fun(I) ->
+ Db = ?DB#{uuid => <<I:64>>},
+ aegis_server:encrypt(Db, <<I:64>>, ?VALUE)
+ end, lists:seq(1, 5)),
+
+ ?assertEqual(5, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)),
+
+ %% confirm enties are still in cache and wind up our "clock" to 160
+ lists:foreach(fun(I) ->
+ Db = ?DB#{uuid => <<I:64>>},
+ aegis_server:encrypt(Db, <<I:64>>, ?VALUE)
+ end, lists:seq(1, 5)),
+
+ ?assertEqual(5, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)),
+
+ %% wait for remove_expired_entries to be triggered
+ meck:reset(aegis_server),
+ meck:wait(aegis_server, handle_info, [maybe_remove_expired, '_'], 2500),
+
+ %% 3 "oldest" entries should be removed, 2 yet to expire still in cache
+ lists:foreach(fun(I) ->
+ Db = ?DB#{uuid => <<I:64>>},
+ aegis_server:encrypt(Db, <<I:64>>, ?VALUE)
+ end, lists:seq(1, 5)),
+
+ ?assertEqual(8, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)).
diff --git a/src/fabric/src/fabric2_util.erl b/src/fabric/src/fabric2_util.erl
index 9b6d18c..136762b 100644
--- a/src/fabric/src/fabric2_util.erl
+++ b/src/fabric/src/fabric2_util.erl
@@ -41,6 +41,7 @@
all_docs_view_opts/1,
iso8601_timestamp/0,
+ now/1,
do_recovery/0,
pmap/2,
@@ -348,6 +349,13 @@ iso8601_timestamp() ->
io_lib:format(Format, [Year, Month, Date, Hour, Minute, Second]).
+now(ms) ->
+ {Mega, Sec, Micro} = os:timestamp(),
+ (Mega * 1000000 + Sec) * 1000 + round(Micro / 1000);
+now(sec) ->
+ now(ms) div 1000.
+
+
do_recovery() ->
config:get_boolean("couchdb",
"enable_database_recovery", false).