You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by va...@apache.org on 2020/03/13 22:43:04 UTC

[couchdb] 01/01: Refactor some of the "tx_too_old" tests

This is an automated email from the ASF dual-hosted git repository.

vatamane pushed a commit to branch refactor-tx-too-old-tests
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit b6b5586a625dd5dc2df7d36dcece59612a380d41
Author: Nick Vatamaniuc <va...@apache.org>
AuthorDate: Fri Mar 13 18:28:51 2020 -0400

    Refactor some of the "tx_too_old" tests
    
    * There was a good amount of duplication between `_db_crud_tests` and
     `_changes_fold_tests`, so make a common test utility module so both suites can
     use.
    
    * Clean up test names. Previously some were named `tx_too_long` but since the
      official FDB error is `transaction_too_old` rename them to match a bit
      better.
    
    * `list_dbs_info` implementation queue of 100 futures to parallelize fetching.
      So its test was update to create more than 100 dbs. Creating 100 dbs took
      about 3 seconds so add a small parallel map (pmap) utility function to help
      with that.
---
 src/fabric/test/fabric2_changes_fold_tests.erl | 103 +++++++--------------
 src/fabric/test/fabric2_db_crud_tests.erl      | 118 +++++++++++--------------
 src/fabric/test/fabric2_tutil.erl              |  99 +++++++++++++++++++++
 3 files changed, 186 insertions(+), 134 deletions(-)

diff --git a/src/fabric/test/fabric2_changes_fold_tests.erl b/src/fabric/test/fabric2_changes_fold_tests.erl
index fddf180..8fedae1 100644
--- a/src/fabric/test/fabric2_changes_fold_tests.erl
+++ b/src/fabric/test/fabric2_changes_fold_tests.erl
@@ -21,9 +21,6 @@
 
 -define(DOC_COUNT, 25).
 
--define(PDICT_ERROR_IN_FOLD_RANGE, '$fabric2_error_in_fold_range').
--define(PDICT_ERROR_IN_USER_FUN, '$fabric2_error_throw_in_user_fun').
-
 
 changes_fold_test_() ->
     {
@@ -43,10 +40,10 @@ changes_fold_test_() ->
                     ?TDEF_FE(fold_changes_basic_rev),
                     ?TDEF_FE(fold_changes_since_now_rev),
                     ?TDEF_FE(fold_changes_since_seq_rev),
-                    ?TDEF_FE(fold_changes_basic_tx_too_long),
-                    ?TDEF_FE(fold_changes_reverse_tx_too_long),
-                    ?TDEF_FE(fold_changes_tx_too_long_with_single_row_emits),
-                    ?TDEF_FE(fold_changes_since_seq_tx_too_long),
+                    ?TDEF_FE(fold_changes_basic_tx_too_old),
+                    ?TDEF_FE(fold_changes_reverse_tx_too_old),
+                    ?TDEF_FE(fold_changes_tx_too_old_with_single_row_emits),
+                    ?TDEF_FE(fold_changes_since_seq_tx_too_old),
                     ?TDEF_FE(fold_changes_not_progressing)
                 ]
             }
@@ -66,10 +63,7 @@ teardown_all(Ctx) ->
 
 
 setup() ->
-    meck:expect(erlfdb, fold_range, fun(Tx, Start, End, Callback, Acc, Opts) ->
-        maybe_tx_too_long(?PDICT_ERROR_IN_FOLD_RANGE),
-        meck:passthrough([Tx, Start, End, Callback, Acc, Opts])
-    end),
+    fabric2_tutil:tx_too_old_mock_erlfdb(),
     {ok, Db} = fabric2_db:create(?tempdb(), [{user_ctx, ?ADMIN_USER}]),
     Rows = lists:map(fun(Val) ->
         DocId = fabric2_util:uuid(),
@@ -90,7 +84,7 @@ setup() ->
 
 
 cleanup({Db, _DocIdRevs}) ->
-    reset_error_counts(),
+    fabric2_tutil:tx_too_old_reset_errors(),
     ok = fabric2_db:delete(fabric2_db:name(Db), []).
 
 
@@ -130,131 +124,114 @@ fold_changes_since_seq_rev({Db, DocRows}) ->
     fold_changes_since_seq_rev({Db, RestRows}).
 
 
-fold_changes_basic_tx_too_long({Db, DocRows0}) ->
+fold_changes_basic_tx_too_old({Db, DocRows0}) ->
     DocRows = lists:reverse(DocRows0),
 
-    tx_too_long_errors(0, 1),
+    fabric2_tutil:tx_too_old_setup_errors(0, 1),
     ?assertEqual(DocRows, changes(Db)),
 
-    tx_too_long_errors(1, 0),
+    fabric2_tutil:tx_too_old_setup_errors(1, 0),
     ?assertEqual(DocRows, changes(Db)),
 
     % Blow up in user fun but after emitting one row successfully.
-    tx_too_long_errors({1, 1}, 0),
+    fabric2_tutil:tx_too_old_setup_errors({1, 1}, 0),
     ?assertEqual(DocRows, changes(Db)),
 
     % Blow up before last document
-    tx_too_long_errors({?DOC_COUNT - 1, 1}, 0),
+    fabric2_tutil:tx_too_old_setup_errors({?DOC_COUNT - 1, 1}, 0),
     ?assertEqual(DocRows, changes(Db)),
 
     % Emit one value, then blow up in user function and then blow up twice in
     % fold_range. But it is not enough to stop the iteration.
-    tx_too_long_errors({1, 1}, {1, 2}),
+    fabric2_tutil:tx_too_old_setup_errors({1, 1}, {1, 2}),
     ?assertEqual(DocRows, changes(Db)).
 
 
-fold_changes_reverse_tx_too_long({Db, DocRows}) ->
+fold_changes_reverse_tx_too_old({Db, DocRows}) ->
     Opts = [{dir, rev}],
 
-    tx_too_long_errors(0, 1),
+    fabric2_tutil:tx_too_old_setup_errors(0, 1),
     ?assertEqual([], changes(Db, 0, Opts)),
 
-    tx_too_long_errors(1, 0),
+    fabric2_tutil:tx_too_old_setup_errors(1, 0),
     ?assertEqual([], changes(Db, 0, Opts)),
 
-    tx_too_long_errors(1, 0),
+    fabric2_tutil:tx_too_old_setup_errors(1, 0),
     ?assertEqual(DocRows, changes(Db, now, Opts)),
 
-    tx_too_long_errors(1, 0),
+    fabric2_tutil:tx_too_old_setup_errors(1, 0),
     ?assertEqual(DocRows, changes(Db, now, Opts)),
 
     % Blow up in user fun but after emitting one row successfully.
-    tx_too_long_errors({1, 1}, 0),
+    fabric2_tutil:tx_too_old_setup_errors({1, 1}, 0),
     ?assertEqual(DocRows, changes(Db, now, Opts)),
 
     % Blow up before last document
-    tx_too_long_errors({?DOC_COUNT - 1, 1}, 0),
+    fabric2_tutil:tx_too_old_setup_errors({?DOC_COUNT - 1, 1}, 0),
     ?assertEqual(DocRows, changes(Db, now, Opts)),
 
     % Emit value, blow up in user function, and twice in fold_range
-    tx_too_long_errors({1, 1}, {1, 2}),
+    fabric2_tutil:tx_too_old_setup_errors({1, 1}, {1, 2}),
     ?assertEqual(DocRows, changes(Db, now, Opts)).
 
 
-fold_changes_tx_too_long_with_single_row_emits({Db, DocRows0}) ->
+fold_changes_tx_too_old_with_single_row_emits({Db, DocRows0}) ->
     % This test does a few basic operations while forcing erlfdb range fold to
     % emit a single row at a time, thus forcing it to use continuations while
     % also inducing tx errors
     Opts = [{target_bytes, 1}],
     DocRows = lists:reverse(DocRows0),
 
-    tx_too_long_errors(0, 1),
+    fabric2_tutil:tx_too_old_setup_errors(0, 1),
     ?assertEqual(DocRows, changes(Db, 0, Opts)),
 
-    tx_too_long_errors(1, 0),
+    fabric2_tutil:tx_too_old_setup_errors(1, 0),
     ?assertEqual(DocRows, changes(Db, 0, Opts)),
 
     % Blow up in user fun but after emitting one row successfully.
-    tx_too_long_errors({1, 1}, 0),
+    fabric2_tutil:tx_too_old_setup_errors({1, 1}, 0),
     ?assertEqual(DocRows, changes(Db, 0, Opts)),
 
     % Blow up before last document
-    tx_too_long_errors({?DOC_COUNT - 1, 1}, 0),
+    fabric2_tutil:tx_too_old_setup_errors({?DOC_COUNT - 1, 1}, 0),
     ?assertEqual(DocRows, changes(Db, 0, Opts)).
 
 
-fold_changes_since_seq_tx_too_long({Db, Rows}) ->
+fold_changes_since_seq_tx_too_old({Db, Rows}) ->
     % Blow up after after a successful emit, then twice
     % in range fold call. Also re-use already existing basic
     % fold_changes_since_seq test function.
-    tx_too_long_errors({1, 1}, {1, 2}),
+    fabric2_tutil:tx_too_old_setup_errors({1, 1}, {1, 2}),
     fold_changes_since_seq({Db, Rows}).
 
 
 fold_changes_not_progressing({Db, _}) ->
     % Fail in first fold range call.
-    tx_too_long_errors(5, 0),
+    fabric2_tutil:tx_too_old_setup_errors(5, 0),
     ?assertError(fold_range_not_progressing, changes(Db)),
 
     % Fail in first user fun call.
-    tx_too_long_errors(0, 5),
+    fabric2_tutil:tx_too_old_setup_errors(0, 5),
     ?assertError(fold_range_not_progressing, changes(Db)),
 
     % Blow up in last user fun call
-    tx_too_long_errors({?DOC_COUNT - 1, 5}, 0),
+    fabric2_tutil:tx_too_old_setup_errors({?DOC_COUNT - 1, 5}, 0),
     ?assertError(fold_range_not_progressing, changes(Db)),
 
     % Blow up in user function after one success.
-    tx_too_long_errors({1, 5}, 0),
+    fabric2_tutil:tx_too_old_setup_errors({1, 5}, 0),
     ?assertError(fold_range_not_progressing, changes(Db)),
 
     % Emit value, blow up in user function, then keep blowing up in fold_range.
-    tx_too_long_errors({1, 1}, {1, 4}),
+    fabric2_tutil:tx_too_old_setup_errors({1, 1}, {1, 4}),
     ?assertError(fold_range_not_progressing, changes(Db)).
 
 
 fold_fun(#{} = Change, Acc) ->
-    maybe_tx_too_long(?PDICT_ERROR_IN_USER_FUN),
+    fabric2_tutil:tx_too_old_raise_in_user_fun(),
     {ok, [Change | Acc]}.
 
 
-tx_too_long_errors(UserFunCount, FoldErrors) when is_integer(UserFunCount) ->
-    tx_too_long_errors({0, UserFunCount}, FoldErrors);
-
-tx_too_long_errors(UserFunErrors, FoldCount) when is_integer(FoldCount) ->
-    tx_too_long_errors(UserFunErrors, {0, FoldCount});
-
-tx_too_long_errors({UserFunSkip, UserFunCount}, {FoldSkip, FoldCount}) ->
-    reset_error_counts(),
-    put(?PDICT_ERROR_IN_USER_FUN, {UserFunSkip, UserFunCount}),
-    put(?PDICT_ERROR_IN_FOLD_RANGE, {FoldSkip, FoldCount}).
-
-
-reset_error_counts() ->
-    erase(?PDICT_ERROR_IN_FOLD_RANGE),
-    erase(?PDICT_ERROR_IN_USER_FUN).
-
-
 changes(Db) ->
     changes(Db, 0, []).
 
@@ -262,17 +239,3 @@ changes(Db) ->
 changes(Db, Since, Opts) ->
     {ok, Rows} = fabric2_db:fold_changes(Db, Since, fun fold_fun/2, [], Opts),
     Rows.
-
-
-maybe_tx_too_long(Key) ->
-    case get(Key) of
-        {Skip, Count} when is_integer(Skip), Skip > 0 ->
-            put(Key, {Skip - 1, Count});
-        {0, Count} when is_integer(Count), Count > 0 ->
-            put(Key, {0, Count - 1}),
-            error({erlfdb_error, 1007});
-        {0, 0} ->
-            ok;
-        undefined ->
-            ok
-    end.
diff --git a/src/fabric/test/fabric2_db_crud_tests.erl b/src/fabric/test/fabric2_db_crud_tests.erl
index 6323a22..d6ede3a 100644
--- a/src/fabric/test/fabric2_db_crud_tests.erl
+++ b/src/fabric/test/fabric2_db_crud_tests.erl
@@ -18,10 +18,6 @@
 -include("fabric2_test.hrl").
 
 
--define(PDICT_ERROR_IN_FOLD_RANGE, '$fabric2_error_in_fold_range').
--define(PDICT_ERROR_IN_USER_FUN, '$fabric2_error_throw_in_user_fun').
-
-
 crud_test_() ->
     {
         "Test database CRUD operations",
@@ -42,8 +38,8 @@ crud_test_() ->
                     ?TDEF_FE(list_dbs_user_fun_partial),
                     ?TDEF_FE(list_dbs_info),
                     ?TDEF_FE(list_dbs_info_partial),
-                    ?TDEF_FE(list_dbs_tx_too_long),
-                    ?TDEF_FE(list_dbs_info_tx_too_long)
+                    ?TDEF_FE(list_dbs_tx_too_old),
+                    ?TDEF_FE(list_dbs_info_tx_too_old)
                 ]
             }
         }
@@ -62,15 +58,11 @@ teardown_all(Ctx) ->
 
 
 setup() ->
-    meck:expect(erlfdb, fold_range, fun(Tx, Start, End, Callback, Acc, Opts) ->
-        maybe_tx_too_long(?PDICT_ERROR_IN_FOLD_RANGE),
-        meck:passthrough([Tx, Start, End, Callback, Acc, Opts])
-    end),
-    ok.
+    fabric2_tutil:tx_too_old_mock_erlfdb().
 
 
 cleanup(_) ->
-    reset_error_counts().
+    fabric2_tutil:tx_too_old_reset_errors().
 
 
 create_db(_) ->
@@ -166,14 +158,14 @@ list_dbs_info_partial(_) ->
     ?assertEqual([{meta, []}], UserAcc).
 
 
-list_dbs_tx_too_long(_) ->
+list_dbs_tx_too_old(_) ->
     DbName1 = ?tempdb(),
     DbName2 = ?tempdb(),
     ?assertMatch({ok, _}, fabric2_db:create(DbName1, [])),
     ?assertMatch({ok, _}, fabric2_db:create(DbName2, [])),
 
     UserFun = fun(Row, Acc) ->
-        maybe_tx_too_long(?PDICT_ERROR_IN_USER_FUN),
+        fabric2_tutil:tx_too_old_raise_in_user_fun(),
         {ok, [Row, Acc]}
     end,
 
@@ -181,64 +173,93 @@ list_dbs_tx_too_long(_) ->
     Dbs = fabric2_db:list_dbs(UserFun, [], []),
 
     % Blow up in fold range
-    tx_too_long_errors(0, 1),
+    fabric2_tutil:tx_too_old_setup_errors(0, 1),
     ?assertEqual(Dbs, fabric2_db:list_dbs(UserFun, [], [])),
 
     % Blow up in fold_range after emitting one row
-    tx_too_long_errors(0, {1, 1}),
+    fabric2_tutil:tx_too_old_setup_errors(0, {1, 1}),
     ?assertEqual(Dbs, fabric2_db:list_dbs(UserFun, [], [])),
 
     % Blow up in user fun
-    tx_too_long_errors(1, 0),
+    fabric2_tutil:tx_too_old_setup_errors(2, 2),
     ?assertEqual(Dbs, fabric2_db:list_dbs(UserFun, [], [])),
 
     % Blow up in user fun after emitting one row
-    tx_too_long_errors({1, 1}, 0),
+    fabric2_tutil:tx_too_old_setup_errors({1, 1}, 0),
     ?assertEqual(Dbs, fabric2_db:list_dbs(UserFun, [], [])),
 
     % Blow up in in user fun and fold range
-    tx_too_long_errors(1, {1, 1}),
+    fabric2_tutil: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_long(_) ->
-    DbName1 = ?tempdb(),
-    DbName2 = ?tempdb(),
-    ?assertMatch({ok, _}, fabric2_db:create(DbName1, [])),
-    ?assertMatch({ok, _}, fabric2_db:create(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_tutil:pmap(fun(_) ->
+        DbName = ?tempdb(),
+        {ok, _} = fabric2_db:create(DbName, []),
+        DbName
+    end, lists:seq(1, DbCount)),
+
+    % Sanity check the pmap results
+    ?assertEqual(DbCount, length(DbNames)),
+    ?assert(lists:all(fun(DbName) -> is_binary(DbName) end, DbNames)),
 
     UserFun = fun(Row, Acc) ->
-        maybe_tx_too_long(?PDICT_ERROR_IN_USER_FUN),
+        fabric2_tutil:tx_too_old_raise_in_user_fun(),
         {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
-    tx_too_long_errors(0, 1),
+    % Blow up in fold range on the first call
+    fabric2_tutil: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
-    tx_too_long_errors(0, {1, 1}),
+    fabric2_tutil: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_tutil: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_tutil:tx_too_old_setup_errors(0, {DbCount - 1, 1}),
     ?assertEqual({ok, DbInfos}, fabric2_db:list_dbs_info(UserFun, [], [])),
 
     % Blow up in user fun
-    tx_too_long_errors(1, 0),
+    fabric2_tutil: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
-    tx_too_long_errors({1, 1}, 0),
+    fabric2_tutil: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_tutil: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_tutil: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
-    tx_too_long_errors(1, {1, 1}),
+    fabric2_tutil:tx_too_old_setup_errors(1, {1, 1}),
     ?assertEqual({ok, DbInfos}, fabric2_db:list_dbs_info(UserFun, [], [])),
 
-    ok = fabric2_db:delete(DbName1, []),
-    ok = fabric2_db:delete(DbName2, []).
+    DeleteResults = fabric2_tutil:pmap(fun(DbName) ->
+        fabric2_db:delete(DbName, [])
+    end, DbNames),
+
+    ?assert(lists:all(fun(Res) -> Res =:= ok end, DeleteResults)).
 
 
 is_db_info_member(_, []) ->
@@ -251,34 +272,3 @@ is_db_info_member(DbName, [DbInfo | RestInfos]) ->
         _E ->
             is_db_info_member(DbName, RestInfos)
     end.
-
-
-tx_too_long_errors(UserFunCount, FoldErrors) when is_integer(UserFunCount) ->
-    tx_too_long_errors({0, UserFunCount}, FoldErrors);
-
-tx_too_long_errors(UserFunErrors, FoldCount) when is_integer(FoldCount) ->
-    tx_too_long_errors(UserFunErrors, {0, FoldCount});
-
-tx_too_long_errors({UserFunSkip, UserFunCount}, {FoldSkip, FoldCount}) ->
-    reset_error_counts(),
-    put(?PDICT_ERROR_IN_USER_FUN, {UserFunSkip, UserFunCount}),
-    put(?PDICT_ERROR_IN_FOLD_RANGE, {FoldSkip, FoldCount}).
-
-
-reset_error_counts() ->
-    erase(?PDICT_ERROR_IN_FOLD_RANGE),
-    erase(?PDICT_ERROR_IN_USER_FUN).
-
-
-maybe_tx_too_long(Key) ->
-    case get(Key) of
-        {Skip, Count} when is_integer(Skip), Skip > 0 ->
-            put(Key, {Skip - 1, Count});
-        {0, Count} when is_integer(Count), Count > 0 ->
-            put(Key, {0, Count - 1}),
-            error({erlfdb_error, 1007});
-        {0, 0} ->
-            ok;
-        undefined ->
-            ok
-    end.
diff --git a/src/fabric/test/fabric2_tutil.erl b/src/fabric/test/fabric2_tutil.erl
new file mode 100644
index 0000000..5b817ba
--- /dev/null
+++ b/src/fabric/test/fabric2_tutil.erl
@@ -0,0 +1,99 @@
+% 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_tutil).
+
+
+-export([
+    pmap/2,
+    pmap/3,
+
+    tx_too_old_mock_erlfdb/0,
+    tx_too_old_setup_errors/2,
+    tx_too_old_reset_errors/0,
+    tx_too_old_raise_in_user_fun/0
+]).
+
+
+-define(PDICT_ERROR_IN_FOLD_RANGE, '$fabric2_error_in_fold_range').
+-define(PDICT_ERROR_IN_USER_FUN, '$fabric2_error_throw_in_user_fun').
+
+
+pmap(Fun, Args) ->
+    pmap(Fun, Args, []).
+
+
+pmap(Fun, Args, Opts) ->
+    Refs = lists:map(fun(Arg) ->
+        {_, Ref} = spawn_monitor(fun() -> exit({'$pmap_res', Fun(Arg)}) end),
+        Ref
+    end, Args),
+    Timeout = fabric2_util:get_value(timeout, Opts, 5000),
+    lists:map(fun(Ref) ->
+        receive
+            {'DOWN', Ref, _, _, {'$pmap_res', Res}} -> Res;
+            {'DOWN', Ref, _, _, Error} -> {pmap_error, Error}
+        after Timeout ->
+            {pmap_error, timeout}
+        end
+    end, Refs).
+
+
+% Set of function to test scenarios where the FDB throws transaction_too_long
+% (1007) errors. The general pattern is to call tx_too_old_mock_erlfdb() in
+% setup. Then, before tests call tx_too_old_setup_errors(UserErrs, FoldErrs)
+% which will set how and when the error will be thrown.
+
+tx_too_old_mock_erlfdb() ->
+    meck:expect(erlfdb, fold_range, fun(Tx, Start, End, Callback, Acc, Opts) ->
+        MockFun = fun(Row, InnerAcc) ->
+            maybe_tx_too_old(?PDICT_ERROR_IN_FOLD_RANGE),
+            Callback(Row, InnerAcc)
+        end,
+        meck:passthrough([Tx, Start, End, MockFun, Acc, Opts])
+    end).
+
+
+tx_too_old_setup_errors(UserCnt, FoldErrs) when is_integer(UserCnt) ->
+    tx_too_old_setup_errors({0, UserCnt}, FoldErrs);
+
+tx_too_old_setup_errors(UserErrs, FoldCnt) when is_integer(FoldCnt) ->
+    tx_too_old_setup_errors(UserErrs, {0, FoldCnt});
+
+tx_too_old_setup_errors({UserSkip, UserCnt}, {FoldSkip, FoldCnt}) ->
+    put(?PDICT_ERROR_IN_USER_FUN, {UserSkip, UserCnt}),
+    put(?PDICT_ERROR_IN_FOLD_RANGE, {FoldSkip, FoldCnt}).
+
+
+tx_too_old_reset_errors() ->
+    erase(?PDICT_ERROR_IN_FOLD_RANGE),
+    erase(?PDICT_ERROR_IN_USER_FUN).
+
+
+tx_too_old_raise_in_user_fun() ->
+    maybe_tx_too_old(?PDICT_ERROR_IN_USER_FUN).
+
+
+% Private functions
+
+maybe_tx_too_old(Key) ->
+    case get(Key) of
+        {Skip, Count} when is_integer(Skip), Skip > 0 ->
+            put(Key, {Skip - 1, Count});
+        {0, Count} when is_integer(Count), Count > 0 ->
+            put(Key, {0, Count - 1}),
+            error({erlfdb_error, 1007});
+        {0, 0} ->
+            ok;
+        undefined ->
+            ok
+    end.