blob: ef8fe592c31ede031094305fa34a8ac3e920acbc [file] [log] [blame]
% 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(fabric2_db_crud_tests).
-include_lib("fabric/include/fabric2.hrl").
-include_lib("couch/include/couch_eunit.hrl").
-include_lib("eunit/include/eunit.hrl").
-include("fabric2_test.hrl").
-define(PDICT_RAISE_IN_ERLFDB_WAIT, '$pdict_raise_in_erlfdb_wait').
crud_test_() ->
{
"Test database CRUD operations",
{
setup,
fun setup_all/0,
fun teardown_all/1,
{
foreach,
fun setup/0,
fun cleanup/1,
[
?TDEF_FE(create_db),
?TDEF_FE(open_db),
?TDEF_FE(delete_db),
?TDEF_FE(recreate_db),
?TDEF_FE(recreate_db_interactive),
?TDEF_FE(recreate_db_non_interactive),
?TDEF_FE(undelete_db),
?TDEF_FE(remove_deleted_db),
?TDEF_FE(scheduled_remove_deleted_db, 15),
?TDEF_FE(scheduled_remove_deleted_dbs, 15),
?TDEF_FE(old_db_handle),
?TDEF_FE(list_dbs),
?TDEF_FE(list_dbs_user_fun),
?TDEF_FE(list_dbs_user_fun_partial),
?TDEF_FE(list_dbs_info),
?TDEF_FE(list_dbs_info_partial),
?TDEF_FE(list_dbs_tx_too_old),
?TDEF_FE(list_dbs_info_tx_too_old, 15),
?TDEF_FE(list_deleted_dbs_info),
?TDEF_FE(list_deleted_dbs_info_user_fun),
?TDEF_FE(list_deleted_dbs_info_user_fun_partial),
?TDEF_FE(list_deleted_dbs_info_with_timestamps),
?TDEF_FE(get_info_wait_retry_on_tx_too_old),
?TDEF_FE(get_info_wait_retry_on_tx_abort)
]
}
}
}.
scheduled_db_remove_error_test_() ->
{
"Test scheduled database remove operations",
{
setup,
fun setup_all/0,
fun teardown_all/1,
{
foreach,
fun setup/0,
fun cleanup/1,
[
?TDEF_FE(scheduled_remove_deleted_dbs_with_error)
]
}
}
}.
setup_all() ->
meck:new(config, [passthrough]),
meck:expect(config, get_integer, fun
("couchdb", "db_expiration_schedule_sec", _) -> 2;
("couchdb", "db_expiration_retention_sec", _) -> 0;
(_, _, Default) -> Default
end),
Ctx = test_util:start_couch([fabric, couch_jobs]),
meck:new(erlfdb, [passthrough]),
meck:new(fabric2_db_expiration, [passthrough]),
Ctx.
teardown_all(Ctx) ->
meck:unload(),
test_util:stop_couch(Ctx).
setup() ->
fabric2_test_util:tx_too_old_mock_erlfdb().
cleanup(_) ->
ok = config:set("couchdb", "db_expiration_enabled", "false", false),
ok = config:set("couchdb", "enable_database_recovery", "false", false),
fabric2_test_util:tx_too_old_reset_errors(),
reset_fail_erfdb_wait(),
meck:reset([fabric2_db_expiration]),
meck:reset([config]),
meck:reset([erlfdb]).
create_db(_) ->
DbName = ?tempdb(),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
?assertEqual(true, ets:member(fabric2_server, DbName)),
?assertEqual({error, file_exists}, fabric2_db:create(DbName, [])).
open_db(_) ->
DbName = ?tempdb(),
?assertError(database_does_not_exist, fabric2_db:open(DbName, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
?assertEqual(true, ets:member(fabric2_server, DbName)),
% Opening the cached version
?assertMatch({ok, _}, fabric2_db:open(DbName, [])),
% Remove from cache and re-open
true = ets:delete(fabric2_server, DbName),
?assertMatch({ok, _}, fabric2_db:open(DbName, [])).
delete_db(_) ->
DbName = ?tempdb(),
?assertError(database_does_not_exist, fabric2_db:delete(DbName, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
?assertEqual(true, ets:member(fabric2_server, DbName)),
?assertEqual(ok, fabric2_db:delete(DbName, [])),
?assertEqual(false, ets:member(fabric2_server, DbName)),
?assertError(database_does_not_exist, fabric2_db:open(DbName, [])).
recreate_db(_) ->
DbName = ?tempdb(),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
{ok, Db1} = fabric2_db:open(DbName, []),
?assertEqual(ok, fabric2_db:delete(DbName, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
?assertError(database_does_not_exist, fabric2_db:get_db_info(Db1)),
?assertEqual(ok, fabric2_db:delete(DbName, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
{ok, Db2} = fabric2_db:open(DbName, []),
CurOpts = [{uuid, fabric2_db:get_uuid(Db2)}],
?assertMatch({ok, #{}}, fabric2_db:open(DbName, CurOpts)),
% Remove from cache to force it to open through fabric2_fdb:open
fabric2_server:remove(DbName),
?assertMatch({ok, #{}}, fabric2_db:open(DbName, CurOpts)),
BadOpts = [{uuid, fabric2_util:uuid()}],
?assertError(database_does_not_exist, fabric2_db:open(DbName, BadOpts)),
% Remove from cache to force it to open through fabric2_fdb:open
fabric2_server:remove(DbName),
?assertError(database_does_not_exist, fabric2_db:open(DbName, BadOpts)).
recreate_db_interactive(_) ->
DbName = ?tempdb(),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
{ok, Db1} = fabric2_db:open(DbName, [{interactive, true}]),
?assertEqual(ok, fabric2_db:delete(DbName, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
?assertMatch({ok, _}, fabric2_db:get_db_info(Db1)).
recreate_db_non_interactive(_) ->
% This is also the default case, but we check that parsing the `false` open
% value works correctly.
DbName = ?tempdb(),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
{ok, Db1} = fabric2_db:open(DbName, [{interactive, false}]),
?assertEqual(ok, fabric2_db:delete(DbName, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
?assertError(database_does_not_exist, fabric2_db:get_db_info(Db1)).
undelete_db(_) ->
DbName = ?tempdb(),
?assertError(database_does_not_exist, fabric2_db:delete(DbName, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
?assertEqual(true, ets:member(fabric2_server, DbName)),
ok = config:set("couchdb", "enable_database_recovery", "true", false),
?assertEqual(ok, fabric2_db:delete(DbName, [])),
?assertEqual(false, ets:member(fabric2_server, DbName)),
{ok, Infos} = fabric2_db:list_deleted_dbs_info(),
[DeletedDbInfo] = [
Info
|| Info <- Infos,
DbName == proplists:get_value(db_name, Info)
],
Timestamp = proplists:get_value(timestamp, DeletedDbInfo),
OldTS = <<"2020-01-01T12:00:00Z">>,
?assertEqual(not_found, fabric2_db:undelete(DbName, DbName, OldTS, [])),
BadDbName = <<"bad_dbname">>,
?assertEqual(
not_found,
fabric2_db:undelete(BadDbName, BadDbName, Timestamp, [])
),
ok = fabric2_db:undelete(DbName, DbName, Timestamp, []),
{ok, AllDbInfos} = fabric2_db:list_dbs_info(),
?assert(is_db_info_member(DbName, AllDbInfos)).
remove_deleted_db(_) ->
DbName = ?tempdb(),
?assertError(database_does_not_exist, fabric2_db:delete(DbName, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
?assertEqual(true, ets:member(fabric2_server, DbName)),
ok = config:set("couchdb", "enable_database_recovery", "true", false),
?assertEqual(ok, fabric2_db:delete(DbName, [])),
?assertEqual(false, ets:member(fabric2_server, DbName)),
{ok, Infos} = fabric2_db:list_deleted_dbs_info(),
[DeletedDbInfo] = [
Info
|| Info <- Infos,
DbName == proplists:get_value(db_name, Info)
],
Timestamp = proplists:get_value(timestamp, DeletedDbInfo),
OldTS = <<"2020-01-01T12:00:00Z">>,
?assertEqual(
not_found,
fabric2_db:delete(DbName, [{deleted_at, OldTS}])
),
BadDbName = <<"bad_dbname">>,
?assertEqual(
not_found,
fabric2_db:delete(BadDbName, [{deleted_at, Timestamp}])
),
ok = fabric2_db:delete(DbName, [{deleted_at, Timestamp}]),
{ok, Infos2} = fabric2_db:list_deleted_dbs_info(),
DeletedDbs = [proplists:get_value(db_name, Info) || Info <- Infos2],
?assert(not lists:member(DbName, DeletedDbs)).
scheduled_remove_deleted_db(_) ->
ok = config:set("couchdb", "db_expiration_enabled", "true", false),
ok = config:set("couchdb", "enable_database_recovery", "true", false),
DbName = ?tempdb(),
?assertError(database_does_not_exist, fabric2_db:delete(DbName, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
?assertEqual(true, ets:member(fabric2_server, DbName)),
?assertEqual(ok, fabric2_db:delete(DbName, [])),
?assertEqual(false, ets:member(fabric2_server, DbName)),
meck:reset(fabric2_db_expiration),
meck:wait(fabric2_db_expiration, process_expirations, '_', 7000),
?assertEqual(
ok,
test_util:wait(fun() ->
{ok, Infos} = fabric2_db:list_deleted_dbs_info(),
DeletedDbs = [proplists:get_value(db_name, Info) || Info <- Infos],
case lists:member(DbName, DeletedDbs) of
true -> wait;
false -> ok
end
end)
).
scheduled_remove_deleted_dbs(_) ->
ok = config:set("couchdb", "db_expiration_enabled", "true", false),
ok = config:set("couchdb", "db_expiration_batch", "2", false),
ok = config:set("couchdb", "enable_database_recovery", "true", false),
DbNameList = [create_and_delete_db() || _I <- lists:seq(1, 5)],
meck:reset(fabric2_db_expiration),
meck:wait(fabric2_db_expiration, process_expirations, '_', 7000),
{ok, Infos} = fabric2_db:list_deleted_dbs_info(),
DeletedDbs = [proplists:get_value(db_name, Info) || Info <- Infos],
lists:map(
fun(DbName) ->
?assert(not lists:member(DbName, DeletedDbs))
end,
DbNameList
).
scheduled_remove_deleted_dbs_with_error(_) ->
meck:expect(fabric2_db_expiration, process_expirations, fun(_, _) ->
throw(process_expirations_error)
end),
{Pid, Ref} = spawn_monitor(fun() ->
fabric2_db_expiration:cleanup(true)
end),
receive
{'DOWN', Ref, process, Pid, Error} ->
?assertMatch({job_error, process_expirations_error, _}, Error)
end,
JobType = <<"db_expiration">>,
JobId = <<"db_expiration_job">>,
FQJobId = <<JobId/binary, "-", 1:16/integer>>,
?assertMatch({ok, _}, couch_jobs:get_job_data(undefined, JobType, FQJobId)),
{ok, JobState} = couch_jobs:get_job_state(undefined, JobType, FQJobId),
?assert(lists:member(JobState, [pending, running])).
old_db_handle(_) ->
% db hard deleted
DbName1 = ?tempdb(),
?assertError(database_does_not_exist, fabric2_db:delete(DbName1, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName1, [])),
{ok, Db1} = fabric2_db:open(DbName1, []),
?assertMatch({ok, _}, fabric2_db:get_db_info(Db1)),
?assertEqual(ok, fabric2_db:delete(DbName1, [])),
?assertError(database_does_not_exist, fabric2_db:get_db_info(Db1)),
% db soft deleted
DbName2 = ?tempdb(),
?assertError(database_does_not_exist, fabric2_db:delete(DbName2, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName2, [])),
{ok, Db2} = fabric2_db:open(DbName2, []),
?assertMatch({ok, _}, fabric2_db:get_db_info(Db2)),
ok = config:set("couchdb", "enable_database_recovery", "true", false),
?assertEqual(ok, fabric2_db:delete(DbName2, [])),
?assertError(database_does_not_exist, fabric2_db:get_db_info(Db2)),
% db soft deleted and re-created
DbName3 = ?tempdb(),
?assertError(database_does_not_exist, fabric2_db:delete(DbName3, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName3, [])),
{ok, Db3} = fabric2_db:open(DbName3, []),
?assertMatch({ok, _}, fabric2_db:get_db_info(Db3)),
ok = config:set("couchdb", "enable_database_recovery", "true", false),
?assertEqual(ok, fabric2_db:delete(DbName3, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName3, [])),
?assertError(database_does_not_exist, fabric2_db:get_db_info(Db3)),
% db soft deleted and undeleted
DbName4 = ?tempdb(),
?assertError(database_does_not_exist, fabric2_db:delete(DbName4, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName4, [])),
{ok, Db4} = fabric2_db:open(DbName4, []),
?assertMatch({ok, _}, fabric2_db:get_db_info(Db4)),
ok = config:set("couchdb", "enable_database_recovery", "true", false),
?assertEqual(ok, fabric2_db:delete(DbName4, [])),
{ok, Infos} = fabric2_db:list_deleted_dbs_info(),
[DeletedDbInfo] = [
Info
|| Info <- Infos,
DbName4 == proplists:get_value(db_name, Info)
],
Timestamp = proplists:get_value(timestamp, DeletedDbInfo),
ok = fabric2_db:undelete(DbName4, DbName4, Timestamp, []),
?assertMatch({ok, _}, fabric2_db:get_db_info(Db4)),
% db hard deleted and re-created
DbName5 = ?tempdb(),
?assertError(database_does_not_exist, fabric2_db:delete(DbName5, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName5, [])),
{ok, Db5} = fabric2_db:open(DbName5, []),
?assertMatch({ok, _}, fabric2_db:get_db_info(Db5)),
ok = config:set("couchdb", "enable_database_recovery", "false", false),
?assertEqual(ok, fabric2_db:delete(DbName5, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName5, [])),
?assertError(database_does_not_exist, fabric2_db:get_db_info(Db5)).
list_dbs(_) ->
DbName = ?tempdb(),
AllDbs1 = fabric2_db:list_dbs(),
?assert(is_list(AllDbs1)),
?assert(not lists:member(DbName, AllDbs1)),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
AllDbs2 = fabric2_db:list_dbs(),
?assert(lists:member(DbName, AllDbs2)),
?assertEqual(ok, fabric2_db:delete(DbName, [])),
AllDbs3 = fabric2_db:list_dbs(),
?assert(not lists:member(DbName, AllDbs3)).
list_dbs_user_fun(_) ->
?assertMatch({ok, _}, fabric2_db:create(?tempdb(), [])),
UserFun = fun(Row, Acc) -> {ok, [Row | Acc]} end,
{ok, UserAcc} = fabric2_db:list_dbs(UserFun, [], []),
Base = lists:foldl(
fun(DbName, Acc) ->
[{row, [{id, DbName}]} | Acc]
end,
[{meta, []}],
fabric2_db:list_dbs()
),
Expect = lists:reverse(Base, [complete]),
?assertEqual(Expect, lists:reverse(UserAcc)).
list_dbs_user_fun_partial(_) ->
UserFun = fun(Row, Acc) -> {stop, [Row | Acc]} end,
{ok, UserAcc} = fabric2_db:list_dbs(UserFun, [], []),
?assertEqual([{meta, []}], UserAcc).
list_dbs_info(_) ->
DbName = ?tempdb(),
{ok, AllDbInfos1} = fabric2_db:list_dbs_info(),
?assert(is_list(AllDbInfos1)),
?assert(not is_db_info_member(DbName, AllDbInfos1)),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
{ok, AllDbInfos2} = fabric2_db:list_dbs_info(),
?assert(is_db_info_member(DbName, AllDbInfos2)),
?assertEqual(ok, fabric2_db:delete(DbName, [])),
{ok, AllDbInfos3} = fabric2_db:list_dbs_info(),
?assert(not is_db_info_member(DbName, AllDbInfos3)).
list_dbs_info_partial(_) ->
UserFun = fun(Row, Acc) -> {stop, [Row | Acc]} end,
{ok, UserAcc} = fabric2_db:list_dbs_info(UserFun, [], []),
?assertEqual([{meta, []}], UserAcc).
list_dbs_tx_too_old(_) ->
DbName1 = ?tempdb(),
DbName2 = ?tempdb(),
?assertMatch({ok, _}, fabric2_db:create(DbName1, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName2, [])),
UserFun = fun
({row, _} = Row, Acc) ->
fabric2_test_util:tx_too_old_raise_in_user_fun(),
{ok, [Row | Acc]};
(Row, Acc) ->
{ok, [Row | Acc]}
end,
% Get get expected output without any transactions timing out
Dbs = fabric2_db:list_dbs(UserFun, [], []),
% Blow up in fold range
fabric2_test_util:tx_too_old_setup_errors(0, 1),
?assertEqual(Dbs, fabric2_db:list_dbs(UserFun, [], [])),
% Blow up in fold_range after emitting one row
fabric2_test_util:tx_too_old_setup_errors(0, {1, 1}),
?assertEqual(Dbs, fabric2_db:list_dbs(UserFun, [], [])),
% Blow up in user fun
fabric2_test_util:tx_too_old_setup_errors(1, 0),
?assertEqual(Dbs, fabric2_db:list_dbs(UserFun, [], [])),
% Blow up in user fun after emitting one row
fabric2_test_util:tx_too_old_setup_errors({1, 1}, 0),
?assertEqual(Dbs, fabric2_db:list_dbs(UserFun, [], [])),
% Blow up in in user fun and fold range
fabric2_test_util:tx_too_old_setup_errors(1, {1, 1}),
?assertEqual(Dbs, fabric2_db:list_dbs(UserFun, [], [])),
ok = fabric2_db:delete(DbName1, []),
ok = fabric2_db:delete(DbName2, []).
list_dbs_info_tx_too_old(_) ->
% list_dbs_info uses a queue of 100 futures to fetch db infos in parallel
% so create more than 100 dbs so make sure we have 100+ dbs in our test
DbCount = 101,
DbNames = fabric2_util:pmap(
fun(_) ->
DbName = ?tempdb(),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
DbName
end,
lists:seq(1, DbCount)
),
UserFun = fun
({row, _} = Row, Acc) ->
fabric2_test_util:tx_too_old_raise_in_user_fun(),
{ok, [Row | Acc]};
(Row, Acc) ->
{ok, [Row | Acc]}
end,
% This is the expected return with no tx timeouts
{ok, DbInfos} = fabric2_db:list_dbs_info(UserFun, [], []),
% Blow up in fold range on the first call
fabric2_test_util:tx_too_old_setup_errors(0, 1),
?assertEqual({ok, DbInfos}, fabric2_db:list_dbs_info(UserFun, [], [])),
% Blow up in fold_range after emitting one row
fabric2_test_util:tx_too_old_setup_errors(0, {1, 1}),
?assertEqual({ok, DbInfos}, fabric2_db:list_dbs_info(UserFun, [], [])),
% Blow up in fold_range after emitting 99 rows
fabric2_test_util:tx_too_old_setup_errors(0, {DbCount - 2, 1}),
?assertEqual({ok, DbInfos}, fabric2_db:list_dbs_info(UserFun, [], [])),
% Blow up in fold_range after emitting 100 rows
fabric2_test_util:tx_too_old_setup_errors(0, {DbCount - 1, 1}),
?assertEqual({ok, DbInfos}, fabric2_db:list_dbs_info(UserFun, [], [])),
% Blow up in user fun
fabric2_test_util:tx_too_old_setup_errors(1, 0),
?assertEqual({ok, DbInfos}, fabric2_db:list_dbs_info(UserFun, [], [])),
% Blow up in user fun after emitting one row
fabric2_test_util:tx_too_old_setup_errors({1, 1}, 0),
?assertEqual({ok, DbInfos}, fabric2_db:list_dbs_info(UserFun, [], [])),
% Blow up in user fun after emitting 99 rows
fabric2_test_util:tx_too_old_setup_errors({DbCount - 2, 1}, 0),
?assertEqual({ok, DbInfos}, fabric2_db:list_dbs_info(UserFun, [], [])),
% Blow up in user fun after emitting 100 rows
fabric2_test_util:tx_too_old_setup_errors({DbCount - 1, 1}, 0),
?assertEqual({ok, DbInfos}, fabric2_db:list_dbs_info(UserFun, [], [])),
% Blow up in in user fun and fold range
fabric2_test_util:tx_too_old_setup_errors(1, {1, 1}),
?assertEqual({ok, DbInfos}, fabric2_db:list_dbs_info(UserFun, [], [])),
fabric2_util:pmap(
fun(DbName) ->
?assertEqual(ok, fabric2_db:delete(DbName, []))
end,
DbNames
).
list_deleted_dbs_info(_) ->
DbName = ?tempdb(),
AllDbs1 = fabric2_db:list_dbs(),
?assert(is_list(AllDbs1)),
?assert(not lists:member(DbName, AllDbs1)),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
AllDbs2 = fabric2_db:list_dbs(),
?assert(lists:member(DbName, AllDbs2)),
ok = config:set("couchdb", "enable_database_recovery", "true", false),
?assertEqual(ok, fabric2_db:delete(DbName, [])),
AllDbs3 = fabric2_db:list_dbs(),
?assert(not lists:member(DbName, AllDbs3)),
{ok, DeletedDbsInfo} = fabric2_db:list_deleted_dbs_info(),
DeletedDbs4 = get_deleted_dbs(DeletedDbsInfo),
?assert(lists:member(DbName, DeletedDbs4)).
list_deleted_dbs_info_user_fun(_) ->
DbName = ?tempdb(),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
?assertEqual(ok, fabric2_db:delete(DbName, [])),
UserFun = fun(Row, Acc) -> {ok, [Row | Acc]} end,
{ok, UserAcc} = fabric2_db:list_deleted_dbs_info(UserFun, [], []),
{ok, DeletedDbsInfo} = fabric2_db:list_deleted_dbs_info(),
Base = lists:foldl(
fun(DbInfo, Acc) ->
[{row, DbInfo} | Acc]
end,
[{meta, []}],
DeletedDbsInfo
),
Expect = lists:reverse(Base, [complete]),
?assertEqual(Expect, lists:reverse(UserAcc)).
list_deleted_dbs_info_user_fun_partial(_) ->
UserFun = fun(Row, Acc) -> {stop, [Row | Acc]} end,
{ok, UserAcc} = fabric2_db:list_deleted_dbs_info(UserFun, [], []),
?assertEqual([{meta, []}], UserAcc).
list_deleted_dbs_info_with_timestamps(_) ->
ok = config:set("couchdb", "enable_database_recovery", "true", false),
% Cycle our database three times to get multiple entries
DbName = ?tempdb(),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
?assertEqual(ok, fabric2_db:delete(DbName, [])),
timer:sleep(1100),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
?assertEqual(ok, fabric2_db:delete(DbName, [])),
timer:sleep(1100),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
?assertEqual(ok, fabric2_db:delete(DbName, [])),
UserFun = fun(Row, Acc) ->
case Row of
{row, Info} -> {ok, [Info | Acc]};
_ -> {ok, Acc}
end
end,
Options1 = [{start_key, DbName}, {end_key, <<DbName/binary, 255>>}],
{ok, Infos1} = fabric2_db:list_deleted_dbs_info(UserFun, [], Options1),
TimeStamps1 = [fabric2_util:get_value(timestamp, Info) || Info <- Infos1],
?assertEqual(3, length(TimeStamps1)),
[FirstTS, MiddleTS, LastTS] = lists:sort(TimeStamps1),
% Check we can skip over the FirstTS
Options2 = [{start_key, [DbName, MiddleTS]}, {end_key, [DbName, LastTS]}],
{ok, Infos2} = fabric2_db:list_deleted_dbs_info(UserFun, [], Options2),
TimeStamps2 = [fabric2_util:get_value(timestamp, Info) || Info <- Infos2],
?assertEqual(2, length(TimeStamps2)),
% because foldl reverses
?assertEqual([LastTS, MiddleTS], TimeStamps2),
% Check we an end before LastTS
Options3 = [{start_key, DbName}, {end_key, [DbName, MiddleTS]}],
{ok, Infos3} = fabric2_db:list_deleted_dbs_info(UserFun, [], Options3),
TimeStamps3 = [fabric2_util:get_value(timestamp, Info) || Info <- Infos3],
?assertEqual([MiddleTS, FirstTS], TimeStamps3),
% Check that {dir, rev} works without timestamps
Options4 = [{start_key, DbName}, {end_key, DbName}, {dir, rev}],
{ok, Infos4} = fabric2_db:list_deleted_dbs_info(UserFun, [], Options4),
TimeStamps4 = [fabric2_util:get_value(timestamp, Info) || Info <- Infos4],
?assertEqual([FirstTS, MiddleTS, LastTS], TimeStamps4),
% Check that reverse with keys returns correctly
Options5 = [
{start_key, [DbName, MiddleTS]},
{end_key, [DbName, FirstTS]},
{dir, rev}
],
{ok, Infos5} = fabric2_db:list_deleted_dbs_info(UserFun, [], Options5),
TimeStamps5 = [fabric2_util:get_value(timestamp, Info) || Info <- Infos5],
?assertEqual([FirstTS, MiddleTS], TimeStamps5).
get_info_wait_retry_on_tx_too_old(_) ->
DbName = ?tempdb(),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
{ok, Db} = fabric2_db:open(DbName, []),
fabric2_fdb:transactional(Db, fun(TxDb) ->
#{
tx := Tx,
db_prefix := DbPrefix
} = TxDb,
% Simulate being in a list_dbs_info callback
ok = erlfdb:set_option(Tx, disallow_writes),
InfoF = fabric2_fdb:get_info_future(Tx, DbPrefix),
{info_future, _, _, ChangesF, _, _, _} = InfoF,
raise_in_erlfdb_wait(ChangesF, {erlfdb_error, 1007}, 3),
?assertError({erlfdb_error, 1007}, fabric2_fdb:get_info_wait(InfoF)),
raise_in_erlfdb_wait(ChangesF, {erlfdb_error, 1007}, 2),
?assertMatch([{_, _} | _], fabric2_fdb:get_info_wait(InfoF)),
?assertEqual(ok, fabric2_db:delete(DbName, []))
end).
get_info_wait_retry_on_tx_abort(_) ->
DbName = ?tempdb(),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
{ok, Db} = fabric2_db:open(DbName, []),
fabric2_fdb:transactional(Db, fun(TxDb) ->
#{
tx := Tx,
db_prefix := DbPrefix
} = TxDb,
% Simulate being in a list_dbs_info callback
ok = erlfdb:set_option(Tx, disallow_writes),
InfoF = fabric2_fdb:get_info_future(Tx, DbPrefix),
{info_future, _, _, ChangesF, _, _, _} = InfoF,
raise_in_erlfdb_wait(ChangesF, {erlfdb_error, 1025}, 3),
?assertError({erlfdb_error, 1025}, fabric2_fdb:get_info_wait(InfoF)),
raise_in_erlfdb_wait(ChangesF, {erlfdb_error, 1025}, 2),
?assertMatch([{_, _} | _], fabric2_fdb:get_info_wait(InfoF)),
?assertEqual(ok, fabric2_db:delete(DbName, []))
end).
reset_fail_erfdb_wait() ->
erase(?PDICT_RAISE_IN_ERLFDB_WAIT),
meck:expect(erlfdb, wait, fun(F) -> meck:passthrough([F]) end).
raise_in_erlfdb_wait(Future, Error, Count) ->
put(?PDICT_RAISE_IN_ERLFDB_WAIT, Count),
meck:expect(erlfdb, wait, fun
(F) when F =:= Future ->
case get(?PDICT_RAISE_IN_ERLFDB_WAIT) of
N when is_integer(N), N > 0 ->
put(?PDICT_RAISE_IN_ERLFDB_WAIT, N - 1),
error(Error);
_ ->
meck:passthrough([F])
end;
(F) ->
meck:passthrough([F])
end).
is_db_info_member(_, []) ->
false;
is_db_info_member(DbName, [DbInfo | RestInfos]) ->
case lists:keyfind(db_name, 1, DbInfo) of
{db_name, DbName} ->
true;
_E ->
is_db_info_member(DbName, RestInfos)
end.
get_deleted_dbs(DeletedDbInfos) ->
lists:foldl(
fun(DbInfo, Acc) ->
DbName = fabric2_util:get_value(db_name, DbInfo),
[DbName | Acc]
end,
[],
DeletedDbInfos
).
create_and_delete_db() ->
DbName = ?tempdb(),
?assertError(database_does_not_exist, fabric2_db:delete(DbName, [])),
?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
?assertEqual(true, ets:member(fabric2_server, DbName)),
?assertEqual(ok, fabric2_db:delete(DbName, [])),
?assertEqual(false, ets:member(fabric2_server, DbName)),
DbName.