This is an automated email from the ASF dual-hosted git repository. davisp pushed a commit to branch optimize-ddoc-cache in repository https://gitbox.apache.org/repos/asf/couchdb.git
commit a10cff2cd8ddb3380fa78adf2f50e0c67c7ce919 Author: Paul J. Davis <[email protected]> AuthorDate: Thu Jul 6 13:48:15 2017 -0500 FIXUP: Reuse fabric lookup results On suggestion from @chewbranca I've gone ahead and made the optimization to insert the revid or non-revid specific version of the request design document. --- src/ddoc_cache/src/ddoc_cache_entry.erl | 33 +++++++++++++++--- src/ddoc_cache/src/ddoc_cache_entry_custom.erl | 7 +++- src/ddoc_cache/src/ddoc_cache_entry_ddocid.erl | 13 +++++++- src/ddoc_cache/src/ddoc_cache_entry_ddocid_rev.erl | 12 ++++++- .../src/ddoc_cache_entry_validation_funs.erl | 7 +++- src/ddoc_cache/src/ddoc_cache_lru.erl | 30 ++++++++++++----- src/ddoc_cache/test/ddoc_cache_basic_test.erl | 39 +++++++++++++++++----- src/ddoc_cache/test/ddoc_cache_entry_test.erl | 6 ++-- src/ddoc_cache/test/ddoc_cache_eviction_test.erl | 6 ++-- src/ddoc_cache/test/ddoc_cache_no_cache_test.erl | 1 + src/ddoc_cache/test/ddoc_cache_open_error_test.erl | 2 +- src/ddoc_cache/test/ddoc_cache_refresh_test.erl | 17 +++++++--- src/ddoc_cache/test/ddoc_cache_remove_test.erl | 30 +++++++++++++---- 13 files changed, 161 insertions(+), 42 deletions(-) diff --git a/src/ddoc_cache/src/ddoc_cache_entry.erl b/src/ddoc_cache/src/ddoc_cache_entry.erl index 914e32e..79c3dcf 100644 --- a/src/ddoc_cache/src/ddoc_cache_entry.erl +++ b/src/ddoc_cache/src/ddoc_cache_entry.erl @@ -18,8 +18,9 @@ dbname/1, ddocid/1, recover/1, + insert/2, - start_link/1, + start_link/2, shutdown/1, open/2, accessed/1, @@ -65,8 +66,12 @@ recover({Mod, Arg}) -> Mod:recover(Arg). -start_link(Key) -> - Pid = proc_lib:spawn_link(?MODULE, init, [Key]), +insert({Mod, Arg}, Value) -> + Mod:insert(Arg, Value). + + +start_link(Key, Default) -> + Pid = proc_lib:spawn_link(?MODULE, init, [{Key, Default}]), {ok, Pid}. @@ -99,7 +104,7 @@ refresh(Pid) -> gen_server:cast(Pid, force_refresh). -init(Key) -> +init({Key, undefined}) -> true = ets:update_element(?CACHE, Key, {#entry.pid, self()}), St = #st{ key = Key, @@ -108,6 +113,26 @@ init(Key) -> accessed = 1 }, ?EVENT(started, Key), + gen_server:enter_loop(?MODULE, [], St); + +init({Key, Default}) -> + Updates = [ + {#entry.val, Default}, + {#entry.pid, self()} + ], + NewTs = os:timestamp(), + true = ets:update_element(?CACHE, Key, Updates), + true = ets:insert(?LRU, {{NewTs, Key, self()}}), + Msg = {'$gen_cast', refresh}, + St = #st{ + key = Key, + val = {open_ok, {ok, Default}}, + opener = erlang:send_after(?REFRESH_TIMEOUT, self(), Msg), + waiters = undefined, + ts = NewTs, + accessed = 1 + }, + ?EVENT(default_started, Key), gen_server:enter_loop(?MODULE, [], St). diff --git a/src/ddoc_cache/src/ddoc_cache_entry_custom.erl b/src/ddoc_cache/src/ddoc_cache_entry_custom.erl index d858ad6..9eaf16f 100644 --- a/src/ddoc_cache/src/ddoc_cache_entry_custom.erl +++ b/src/ddoc_cache/src/ddoc_cache_entry_custom.erl @@ -16,7 +16,8 @@ -export([ dbname/1, ddocid/1, - recover/1 + recover/1, + insert/2 ]). @@ -30,3 +31,7 @@ ddocid(_) -> recover({DbName, Mod}) -> Mod:recover(DbName). + + +insert(_, _) -> + ok. diff --git a/src/ddoc_cache/src/ddoc_cache_entry_ddocid.erl b/src/ddoc_cache/src/ddoc_cache_entry_ddocid.erl index cac9abc..5248469 100644 --- a/src/ddoc_cache/src/ddoc_cache_entry_ddocid.erl +++ b/src/ddoc_cache/src/ddoc_cache_entry_ddocid.erl @@ -16,7 +16,8 @@ -export([ dbname/1, ddocid/1, - recover/1 + recover/1, + insert/2 ]). @@ -33,3 +34,13 @@ ddocid({_, DDocId}) -> recover({DbName, DDocId}) -> fabric:open_doc(DbName, DDocId, [ejson_body, ?ADMIN_CTX]). + + +insert({DbName, DDocId}, {ok, #doc{revs = Revs} = DDoc}) -> + {Depth, [RevId | _]} = Revs, + Rev = {Depth, RevId}, + Key = {ddoc_cache_entry_ddocid_rev, {DbName, DDocId, Rev}}, + spawn(fun() -> ddoc_cache_lru:insert(Key, DDoc) end); + +insert(_, _) -> + ok. diff --git a/src/ddoc_cache/src/ddoc_cache_entry_ddocid_rev.erl b/src/ddoc_cache/src/ddoc_cache_entry_ddocid_rev.erl index 012abab..868fa77 100644 --- a/src/ddoc_cache/src/ddoc_cache_entry_ddocid_rev.erl +++ b/src/ddoc_cache/src/ddoc_cache_entry_ddocid_rev.erl @@ -16,7 +16,8 @@ -export([ dbname/1, ddocid/1, - recover/1 + recover/1, + insert/2 ]). @@ -35,3 +36,12 @@ recover({DbName, DDocId, Rev}) -> Opts = [ejson_body, ?ADMIN_CTX], {ok, [Resp]} = fabric:open_revs(DbName, DDocId, [Rev], Opts), Resp. + + +insert({DbName, DDocId, _Rev}, {ok, #doc{} = DDoc}) -> + Key = {ddoc_cache_entry_ddocid, {DbName, DDocId}}, + spawn(fun() -> ddoc_cache_lru:insert(Key, DDoc) end); + +insert(_, _) -> + ok. + diff --git a/src/ddoc_cache/src/ddoc_cache_entry_validation_funs.erl b/src/ddoc_cache/src/ddoc_cache_entry_validation_funs.erl index 3d43f7a..2182dea 100644 --- a/src/ddoc_cache/src/ddoc_cache_entry_validation_funs.erl +++ b/src/ddoc_cache/src/ddoc_cache_entry_validation_funs.erl @@ -16,7 +16,8 @@ -export([ dbname/1, ddocid/1, - recover/1 + recover/1, + insert/2 ]). @@ -37,3 +38,7 @@ recover(DbName) -> end end, DDocs), {ok, Funs}. + + +insert(_, _) -> + ok. diff --git a/src/ddoc_cache/src/ddoc_cache_lru.erl b/src/ddoc_cache/src/ddoc_cache_lru.erl index cbe481e..6ae4de4 100644 --- a/src/ddoc_cache/src/ddoc_cache_lru.erl +++ b/src/ddoc_cache/src/ddoc_cache_lru.erl @@ -18,6 +18,7 @@ -export([ start_link/0, open/1, + insert/2, refresh/2 ]). @@ -53,9 +54,9 @@ start_link() -> open(Key) -> try ets:lookup(?CACHE, Key) of [] -> - lru_start(Key); + lru_start(Key, true); [#entry{pid = undefined}] -> - lru_start(Key); + lru_start(Key, false); [#entry{val = undefined, pid = Pid}] -> couch_stats:increment_counter([ddoc_cache, miss]), ddoc_cache_entry:open(Pid, Key); @@ -69,6 +70,15 @@ open(Key) -> end. +insert(Key, Value) -> + case ets:lookup(?CACHE, Key) of + [] -> + gen_server:call(?MODULE, {start, Key, Value}, infinity); + [#entry{}] -> + ok + end. + + refresh(DbName, DDocIds) -> gen_server:cast(?MODULE, {refresh, DbName, DDocIds}). @@ -96,7 +106,7 @@ terminate(_Reason, St) -> ok. -handle_call({start, Key}, _From, St) -> +handle_call({start, Key, Default}, _From, St) -> #st{ pids = Pids, dbs = Dbs, @@ -108,7 +118,7 @@ handle_call({start, Key}, _From, St) -> case trim(St, CurSize, max(0, MaxSize)) of {ok, N} -> true = ets:insert_new(?CACHE, #entry{key = Key}), - {ok, Pid} = ddoc_cache_entry:start_link(Key), + {ok, Pid} = ddoc_cache_entry:start_link(Key, Default), true = ets:update_element(?CACHE, Key, {#entry.pid, Pid}), ok = khash:put(Pids, Pid, Key), store_key(Dbs, Key, Pid), @@ -167,7 +177,7 @@ handle_cast({do_refresh, DbName, DDocIdList}, St) -> lists:foreach(fun(DDocId) -> case khash:lookup(DDocIds, DDocId) of {value, Keys} -> - khash:fold(Keys, fun(_, Pid, _) -> + khash:fold(Keys, fun(Key, Pid, _) -> ddoc_cache_entry:refresh(Pid) end, nil); not_found -> @@ -222,11 +232,15 @@ handle_db_event(_DbName, _Event, St) -> {ok, St}. -lru_start(Key) -> - case gen_server:call(?MODULE, {start, Key}, infinity) of +lru_start(Key, DoInsert) -> + case gen_server:call(?MODULE, {start, Key, undefined}, infinity) of {ok, Pid} -> couch_stats:increment_counter([ddoc_cache, miss]), - ddoc_cache_entry:open(Pid, Key); + Resp = ddoc_cache_entry:open(Pid, Key), + if not DoInsert -> ok; true -> + ddoc_cache_entry:insert(Key, Resp) + end, + Resp; full -> couch_stats:increment_counter([ddoc_cache, recovery]), ddoc_cache_entry:recover(Key) diff --git a/src/ddoc_cache/test/ddoc_cache_basic_test.erl b/src/ddoc_cache/test/ddoc_cache_basic_test.erl index f908c78..7f6dbc9 100644 --- a/src/ddoc_cache/test/ddoc_cache_basic_test.erl +++ b/src/ddoc_cache/test/ddoc_cache_basic_test.erl @@ -27,11 +27,22 @@ recover(DbName) -> {ok, {DbName, totes_custom}}. +start_couch() -> + Ctx = ddoc_cache_tutil:start_couch(), + meck:new(ddoc_cache_ev, [passthrough]), + Ctx. + + +stop_couch(Ctx) -> + meck:unload(), + ddoc_cache_tutil:stop_couch(Ctx). + + check_basic_test_() -> { setup, - fun ddoc_cache_tutil:start_couch/0, - fun ddoc_cache_tutil:stop_couch/1, + fun start_couch/0, + fun stop_couch/1, {with, [ fun cache_ddoc/1, fun cache_ddoc_rev/1, @@ -58,25 +69,31 @@ check_no_vdu_test_() -> cache_ddoc({DbName, _}) -> ddoc_cache_tutil:clear(), + meck:reset(ddoc_cache_ev), ?assertEqual(0, ets:info(?CACHE, size)), Resp1 = ddoc_cache:open_doc(DbName, ?FOOBAR), ?assertMatch({ok, #doc{id = ?FOOBAR}}, Resp1), - ?assertEqual(1, ets:info(?CACHE, size)), + meck:wait(ddoc_cache_ev, event, [started, '_'], 1000), + meck:wait(ddoc_cache_ev, event, [default_started, '_'], 1000), + ?assertEqual(2, ets:info(?CACHE, size)), Resp2 = ddoc_cache:open_doc(DbName, ?FOOBAR), ?assertEqual(Resp1, Resp2), - ?assertEqual(1, ets:info(?CACHE, size)). + ?assertEqual(2, ets:info(?CACHE, size)). cache_ddoc_rev({DbName, _}) -> ddoc_cache_tutil:clear(), + meck:reset(ddoc_cache_ev), Rev = ddoc_cache_tutil:get_rev(DbName, ?FOOBAR), ?assertEqual(0, ets:info(?CACHE, size)), Resp1 = ddoc_cache:open_doc(DbName, ?FOOBAR, Rev), ?assertMatch({ok, #doc{id = ?FOOBAR}}, Resp1), - ?assertEqual(1, ets:info(?CACHE, size)), + meck:wait(ddoc_cache_ev, event, [started, '_'], 1000), + meck:wait(ddoc_cache_ev, event, [default_started, '_'], 1000), + ?assertEqual(2, ets:info(?CACHE, size)), Resp2 = ddoc_cache:open_doc(DbName, ?FOOBAR, Rev), ?assertEqual(Resp1, Resp2), - ?assertEqual(1, ets:info(?CACHE, size)), + ?assertEqual(2, ets:info(?CACHE, size)), % Assert that the non-rev cache entry is separate Resp3 = ddoc_cache:open_doc(DbName, ?FOOBAR), @@ -108,12 +125,16 @@ cache_custom({DbName, _}) -> cache_ddoc_refresher_unchanged({DbName, _}) -> ddoc_cache_tutil:clear(), + meck:reset(ddoc_cache_ev), ?assertEqual(0, ets:info(?CACHE, size)), ddoc_cache:open_doc(DbName, ?FOOBAR), - [Entry1] = ets:lookup(?CACHE, ets:first(?CACHE)), + meck:wait(ddoc_cache_ev, event, [started, '_'], 1000), + meck:wait(ddoc_cache_ev, event, [default_started, '_'], 1000), + Tab1 = [_, _] = lists:sort(ets:tab2list(?CACHE)), ddoc_cache:open_doc(DbName, ?FOOBAR), - [Entry2] = ets:lookup(?CACHE, ets:first(?CACHE)), - ?assertEqual(Entry1, Entry2). + meck:wait(ddoc_cache_ev, event, [accessed, '_'], 1000), + Tab2 = lists:sort(ets:tab2list(?CACHE)), + ?assertEqual(Tab2, Tab1). dont_cache_not_found({DbName, _}) -> diff --git a/src/ddoc_cache/test/ddoc_cache_entry_test.erl b/src/ddoc_cache/test/ddoc_cache_entry_test.erl index e593bf7..381185c 100644 --- a/src/ddoc_cache/test/ddoc_cache_entry_test.erl +++ b/src/ddoc_cache/test/ddoc_cache_entry_test.erl @@ -61,7 +61,7 @@ check_entry_test_() -> cancel_and_replace_opener(_) -> Key = {ddoc_cache_entry_custom, {<<"foo">>, ?MODULE}}, true = ets:insert_new(?CACHE, #entry{key = Key}), - {ok, Entry} = ddoc_cache_entry:start_link(Key), + {ok, Entry} = ddoc_cache_entry:start_link(Key, undefined), Opener1 = element(4, sys:get_state(Entry)), Ref1 = erlang:monitor(process, Opener1), gen_server:cast(Entry, force_refresh), @@ -78,7 +78,7 @@ condenses_access_messages({DbName, _}) -> meck:reset(ddoc_cache_ev), Key = {ddoc_cache_entry_custom, {DbName, ?MODULE}}, true = ets:insert(?CACHE, #entry{key = Key}), - {ok, Entry} = ddoc_cache_entry:start_link(Key), + {ok, Entry} = ddoc_cache_entry:start_link(Key, undefined), erlang:suspend_process(Entry), lists:foreach(fun(_) -> gen_server:cast(Entry, accessed) @@ -105,7 +105,7 @@ evict_when_not_accessed(_) -> meck:reset(ddoc_cache_ev), Key = {ddoc_cache_entry_custom, {<<"bar">>, ?MODULE}}, true = ets:insert_new(?CACHE, #entry{key = Key}), - {ok, Entry} = ddoc_cache_entry:start_link(Key), + {ok, Entry} = ddoc_cache_entry:start_link(Key, undefined), Ref = erlang:monitor(process, Entry), ?assertEqual(1, element(7, sys:get_state(Entry))), ok = gen_server:cast(Entry, refresh), diff --git a/src/ddoc_cache/test/ddoc_cache_eviction_test.erl b/src/ddoc_cache/test/ddoc_cache_eviction_test.erl index 0b9f57b..30b4fb7 100644 --- a/src/ddoc_cache/test/ddoc_cache_eviction_test.erl +++ b/src/ddoc_cache/test/ddoc_cache_eviction_test.erl @@ -86,8 +86,10 @@ check_upgrade_clause({DbName, _}) -> ddoc_cache_tutil:clear(), meck:reset(ddoc_cache_ev), {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR), - ?assertEqual(1, ets:info(?CACHE, size)), + meck:wait(ddoc_cache_ev, event, [started, '_'], 1000), + meck:wait(ddoc_cache_ev, event, [default_started, '_'], 1000), + ?assertEqual(2, ets:info(?CACHE, size)), gen_server:cast(ddoc_cache_opener, {do_evict, DbName}), meck:wait(ddoc_cache_ev, event, [evicted, DbName], 1000), - meck:wait(ddoc_cache_ev, event, [removed, '_'], 1000), + meck:wait(2, ddoc_cache_ev, event, [removed, '_'], 1000), ?assertEqual(0, ets:info(?CACHE, size)). diff --git a/src/ddoc_cache/test/ddoc_cache_no_cache_test.erl b/src/ddoc_cache/test/ddoc_cache_no_cache_test.erl index a5a5751..637a6e8 100644 --- a/src/ddoc_cache/test/ddoc_cache_no_cache_test.erl +++ b/src/ddoc_cache/test/ddoc_cache_no_cache_test.erl @@ -20,6 +20,7 @@ ddoc(DDocId) -> {ok, #doc{ id = DDocId, + revs = {1, [<<"deadbeefdeadbeef">>]}, body = {[ {<<"ohai">>, null} ]} diff --git a/src/ddoc_cache/test/ddoc_cache_open_error_test.erl b/src/ddoc_cache/test/ddoc_cache_open_error_test.erl index 0ac2390..f3a9b10 100644 --- a/src/ddoc_cache/test/ddoc_cache_open_error_test.erl +++ b/src/ddoc_cache/test/ddoc_cache_open_error_test.erl @@ -31,7 +31,7 @@ stop_couch(Ctx) -> ddoc_cache_tutil:stop_couch(Ctx). -check_basic_test_() -> +check_open_error_test_() -> { setup, fun start_couch/0, diff --git a/src/ddoc_cache/test/ddoc_cache_refresh_test.erl b/src/ddoc_cache/test/ddoc_cache_refresh_test.erl index 7bc1704..f145987 100644 --- a/src/ddoc_cache/test/ddoc_cache_refresh_test.erl +++ b/src/ddoc_cache/test/ddoc_cache_refresh_test.erl @@ -58,8 +58,11 @@ refresh_ddoc({DbName, _}) -> ddoc_cache_tutil:clear(), meck:reset(ddoc_cache_ev), {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR), - ?assertEqual(1, ets:info(?CACHE, size)), - [#entry{key = Key, val = DDoc}] = ets:tab2list(?CACHE), + meck:wait(ddoc_cache_ev, event, [started, '_'], 1000), + meck:wait(ddoc_cache_ev, event, [default_started, '_'], 1000), + + ?assertEqual(2, ets:info(?CACHE, size)), + [#entry{key = Key, val = DDoc}, _] = lists:sort(ets:tab2list(?CACHE)), NewDDoc = DDoc#doc{ body = {[{<<"foo">>, <<"baz">>}]} }, @@ -69,7 +72,7 @@ refresh_ddoc({DbName, _}) -> }, meck:wait(ddoc_cache_ev, event, [updated, {Key, Expect}], 1000), ?assertMatch({ok, Expect}, ddoc_cache:open_doc(DbName, ?FOOBAR)), - ?assertEqual(1, ets:info(?CACHE, size)). + ?assertEqual(2, ets:info(?CACHE, size)). refresh_ddoc_rev({DbName, _}) -> @@ -77,7 +80,11 @@ refresh_ddoc_rev({DbName, _}) -> meck:reset(ddoc_cache_ev), Rev = ddoc_cache_tutil:get_rev(DbName, ?FOOBAR), {ok, RevDDoc} = ddoc_cache:open_doc(DbName, ?FOOBAR, Rev), - [#entry{key = Key, val = DDoc}] = ets:tab2list(?CACHE), + + meck:wait(ddoc_cache_ev, event, [started, '_'], 1000), + meck:wait(ddoc_cache_ev, event, [default_started, '_'], 1000), + + [_, #entry{key = Key, val = DDoc}] = lists:sort(ets:tab2list(?CACHE)), NewDDoc = DDoc#doc{ body = {[{<<"foo">>, <<"kazam">>}]} }, @@ -86,7 +93,7 @@ refresh_ddoc_rev({DbName, _}) -> % getting the same original response from the cache meck:wait(ddoc_cache_ev, event, [update_noop, Key], 1000), ?assertMatch({ok, RevDDoc}, ddoc_cache:open_doc(DbName, ?FOOBAR, Rev)), - ?assertEqual(1, ets:info(?CACHE, size)). + ?assertEqual(2, ets:info(?CACHE, size)). refresh_vdu({DbName, _}) -> diff --git a/src/ddoc_cache/test/ddoc_cache_remove_test.erl b/src/ddoc_cache/test/ddoc_cache_remove_test.erl index 7596b99..8787482 100644 --- a/src/ddoc_cache/test/ddoc_cache_remove_test.erl +++ b/src/ddoc_cache/test/ddoc_cache_remove_test.erl @@ -67,16 +67,26 @@ remove_ddoc({DbName, _}) -> meck:reset(ddoc_cache_ev), ?assertEqual(0, ets:info(?CACHE, size)), {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR), - ?assertEqual(1, ets:info(?CACHE, size)), - [#entry{key = Key, val = DDoc}] = ets:tab2list(?CACHE), + + meck:wait(ddoc_cache_ev, event, [started, '_'], 1000), + meck:wait(ddoc_cache_ev, event, [default_started, '_'], 1000), + + [#entry{val = DDoc}, #entry{val = DDoc}] = ets:tab2list(?CACHE), + {Depth, [RevId | _]} = DDoc#doc.revs, NewDDoc = DDoc#doc{ deleted = true, body = {[]} }, {ok, _} = fabric:update_doc(DbName, NewDDoc, [?ADMIN_CTX]), - meck:wait(ddoc_cache_ev, event, [removed, Key], 1000), + + DDocIdKey = {ddoc_cache_entry_ddocid, {DbName, ?FOOBAR}}, + Rev = {Depth, RevId}, + DDocIdRevKey = {ddoc_cache_entry_ddocid_rev, {DbName, ?FOOBAR, Rev}}, + meck:wait(ddoc_cache_ev, event, [removed, DDocIdKey], 1000), + meck:wait(ddoc_cache_ev, event, [update_noop, DDocIdRevKey], 1000), + ?assertMatch({not_found, deleted}, ddoc_cache:open_doc(DbName, ?FOOBAR)), - ?assertEqual(0, ets:info(?CACHE, size)). + ?assertEqual(1, ets:info(?CACHE, size)). remove_ddoc_rev({DbName, _}) -> @@ -84,7 +94,15 @@ remove_ddoc_rev({DbName, _}) -> meck:reset(ddoc_cache_ev), Rev = ddoc_cache_tutil:get_rev(DbName, ?VDU), {ok, _} = ddoc_cache:open_doc(DbName, ?VDU, Rev), - [#entry{key = Key, val = DDoc, pid = Pid}] = ets:tab2list(?CACHE), + + meck:wait(ddoc_cache_ev, event, [started, '_'], 1000), + meck:wait(ddoc_cache_ev, event, [default_started, '_'], 1000), + + % Notice the sort so that we know we're getting the + % revid version second. + [_, #entry{key = Key, val = DDoc, pid = Pid}] + = lists:sort(ets:tab2list(?CACHE)), + NewDDoc = DDoc#doc{ body = {[{<<"an">>, <<"update">>}]} }, @@ -101,7 +119,7 @@ remove_ddoc_rev({DbName, _}) -> {{not_found, missing}, _}, ddoc_cache:open_doc(DbName, ?VDU, Rev) ), - ?assertEqual(0, ets:info(?CACHE, size)). + ?assertEqual(1, ets:info(?CACHE, size)). remove_ddoc_rev_only({DbName, _}) -> -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
