Current section

Files

Jump to
rocksdb test pessimistic_transaction.erl
Raw

test/pessimistic_transaction.erl

-module(pessimistic_transaction).
-compile([export_all/1]).
-include_lib("eunit/include/eunit.hrl").
-include_lib("stdlib/include/assert.hrl").
destroy_reopen(DbName, Options) ->
_ = rocksdb:destroy(DbName, []),
_ = rocksdb_test_util:rm_rf(DbName),
{ok, Db, _} = rocksdb:open_pessimistic_transaction_db(DbName, Options, [{"default", []}]),
Db.
close_destroy(Db, DbName) ->
rocksdb:close(Db),
rocksdb:destroy(DbName, []),
rocksdb_test_util:rm_rf(DbName).
%% Basic CRUD operations test
basic_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
{ok, Transaction} = rocksdb:pessimistic_transaction(Db, []),
ok = rocksdb:pessimistic_transaction_put(Transaction, <<"a">>, <<"v1">>),
ok = rocksdb:pessimistic_transaction_put(Transaction, <<"b">>, <<"v2">>),
%% Data not visible outside transaction before commit
?assertEqual(not_found, rocksdb:get(Db, <<"a">>, [])),
?assertEqual(not_found, rocksdb:get(Db, <<"b">>, [])),
%% Data visible inside transaction
?assertEqual({ok, <<"v1">>}, rocksdb:pessimistic_transaction_get(Transaction, <<"a">>, [])),
?assertEqual({ok, <<"v2">>}, rocksdb:pessimistic_transaction_get(Transaction, <<"b">>, [])),
ok = rocksdb:pessimistic_transaction_commit(Transaction),
%% Data visible after commit
?assertEqual({ok, <<"v1">>}, rocksdb:get(Db, <<"a">>, [])),
?assertEqual({ok, <<"v2">>}, rocksdb:get(Db, <<"b">>, [])),
ok = rocksdb:release_pessimistic_transaction(Transaction),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% Delete operation test
delete_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
{ok, Transaction} = rocksdb:pessimistic_transaction(Db, []),
ok = rocksdb:pessimistic_transaction_put(Transaction, <<"a">>, <<"v1">>),
ok = rocksdb:pessimistic_transaction_put(Transaction, <<"b">>, <<"v2">>),
ok = rocksdb:pessimistic_transaction_delete(Transaction, <<"b">>),
ok = rocksdb:pessimistic_transaction_delete(Transaction, <<"c">>),
?assertEqual(not_found, rocksdb:get(Db, <<"a">>, [])),
?assertEqual(not_found, rocksdb:get(Db, <<"b">>, [])),
?assertEqual({ok, <<"v1">>}, rocksdb:pessimistic_transaction_get(Transaction, <<"a">>, [])),
?assertEqual(not_found, rocksdb:pessimistic_transaction_get(Transaction, <<"b">>, [])),
ok = rocksdb:pessimistic_transaction_commit(Transaction),
?assertEqual({ok, <<"v1">>}, rocksdb:get(Db, <<"a">>, [])),
?assertEqual(not_found, rocksdb:get(Db, <<"b">>, [])),
ok = rocksdb:release_pessimistic_transaction(Transaction),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% Rollback test
rollback_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
%% First transaction - commit some data
{ok, Transaction} = rocksdb:pessimistic_transaction(Db, []),
ok = rocksdb:pessimistic_transaction_put(Transaction, <<"a">>, <<"v1">>),
ok = rocksdb:pessimistic_transaction_put(Transaction, <<"b">>, <<"v2">>),
ok = rocksdb:pessimistic_transaction_commit(Transaction),
?assertEqual({ok, <<"v1">>}, rocksdb:get(Db, <<"a">>, [])),
?assertEqual({ok, <<"v2">>}, rocksdb:get(Db, <<"b">>, [])),
ok = rocksdb:release_pessimistic_transaction(Transaction),
%% Second transaction - make changes then rollback
{ok, Transaction1} = rocksdb:pessimistic_transaction(Db, []),
ok = rocksdb:pessimistic_transaction_put(Transaction1, <<"a">>, <<"v2">>),
ok = rocksdb:pessimistic_transaction_put(Transaction1, <<"c">>, <<"v3">>),
ok = rocksdb:pessimistic_transaction_delete(Transaction1, <<"b">>),
ok = rocksdb:pessimistic_transaction_rollback(Transaction1),
%% Original values should remain
?assertEqual({ok, <<"v1">>}, rocksdb:get(Db, <<"a">>, [])),
?assertEqual({ok, <<"v2">>}, rocksdb:get(Db, <<"b">>, [])),
?assertEqual(not_found, rocksdb:get(Db, <<"c">>, [])),
ok = rocksdb:release_pessimistic_transaction(Transaction1),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% GetForUpdate test - acquire exclusive lock on read
get_for_update_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
%% Put some initial data
ok = rocksdb:put(Db, <<"a">>, <<"v1">>, []),
{ok, Transaction} = rocksdb:pessimistic_transaction(Db, []),
%% GetForUpdate acquires a lock on the key
?assertEqual({ok, <<"v1">>}, rocksdb:pessimistic_transaction_get_for_update(Transaction, <<"a">>, [])),
%% Can still read with regular get
?assertEqual({ok, <<"v1">>}, rocksdb:pessimistic_transaction_get(Transaction, <<"a">>, [])),
%% Update the value
ok = rocksdb:pessimistic_transaction_put(Transaction, <<"a">>, <<"v2">>),
%% New value visible in transaction
?assertEqual({ok, <<"v2">>}, rocksdb:pessimistic_transaction_get(Transaction, <<"a">>, [])),
ok = rocksdb:pessimistic_transaction_commit(Transaction),
?assertEqual({ok, <<"v2">>}, rocksdb:get(Db, <<"a">>, [])),
ok = rocksdb:release_pessimistic_transaction(Transaction),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% Lock timeout test
lock_timeout_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
%% Put some initial data
ok = rocksdb:put(Db, <<"a">>, <<"v1">>, []),
%% Transaction 1 acquires lock via GetForUpdate
{ok, Txn1} = rocksdb:pessimistic_transaction(Db, []),
?assertEqual({ok, <<"v1">>}, rocksdb:pessimistic_transaction_get_for_update(Txn1, <<"a">>, [])),
%% Transaction 2 tries to lock the same key with short timeout
{ok, Txn2} = rocksdb:pessimistic_transaction(Db, [{lock_timeout, 100}]),
Result = rocksdb:pessimistic_transaction_get_for_update(Txn2, <<"a">>, []),
?assertMatch({error, _}, Result),
ok = rocksdb:pessimistic_transaction_rollback(Txn1),
ok = rocksdb:pessimistic_transaction_rollback(Txn2),
ok = rocksdb:release_pessimistic_transaction(Txn1),
ok = rocksdb:release_pessimistic_transaction(Txn2),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% Column family test
column_family_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
{ok, TestH} = rocksdb:create_column_family(Db, "test", []),
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
%% Put in default and test column families
ok = rocksdb:pessimistic_transaction_put(Txn, <<"a">>, <<"v1">>),
ok = rocksdb:pessimistic_transaction_put(Txn, TestH, <<"a">>, <<"cf_v1">>),
%% Values are different per column family
?assertEqual({ok, <<"v1">>}, rocksdb:pessimistic_transaction_get(Txn, <<"a">>, [])),
?assertEqual({ok, <<"cf_v1">>}, rocksdb:pessimistic_transaction_get(Txn, TestH, <<"a">>, [])),
ok = rocksdb:pessimistic_transaction_commit(Txn),
%% Verify after commit
?assertEqual({ok, <<"v1">>}, rocksdb:get(Db, <<"a">>, [])),
?assertEqual({ok, <<"cf_v1">>}, rocksdb:get(Db, TestH, <<"a">>, [])),
ok = rocksdb:release_pessimistic_transaction(Txn),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% Iterator test
iterator_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
%% Put some initial data
ok = rocksdb:put(Db, <<"a">>, <<"v1">>, []),
ok = rocksdb:put(Db, <<"b">>, <<"v2">>, []),
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
%% Add data in transaction
ok = rocksdb:pessimistic_transaction_put(Txn, <<"c">>, <<"v3">>),
{ok, It} = rocksdb:pessimistic_transaction_iterator(Txn, []),
%% Iterator should see both committed and uncommitted data
?assertEqual({ok, <<"a">>, <<"v1">>}, rocksdb:iterator_move(It, <<>>)),
?assertEqual({ok, <<"b">>, <<"v2">>}, rocksdb:iterator_move(It, next)),
?assertEqual({ok, <<"c">>, <<"v3">>}, rocksdb:iterator_move(It, next)),
?assertEqual({error, invalid_iterator}, rocksdb:iterator_move(It, next)),
ok = rocksdb:iterator_close(It),
ok = rocksdb:pessimistic_transaction_commit(Txn),
ok = rocksdb:release_pessimistic_transaction(Txn),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% Iterator with column family test
cf_iterator_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
{ok, TestH} = rocksdb:create_column_family(Db, "test", []),
ok = rocksdb:put(Db, <<"a">>, <<"v1">>, []),
ok = rocksdb:put(Db, TestH, <<"x">>, <<"cf_v1">>, []),
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
ok = rocksdb:pessimistic_transaction_put(Txn, <<"b">>, <<"v2">>),
ok = rocksdb:pessimistic_transaction_put(Txn, TestH, <<"y">>, <<"cf_v2">>),
{ok, DefaultIt} = rocksdb:pessimistic_transaction_iterator(Txn, []),
{ok, TestIt} = rocksdb:pessimistic_transaction_iterator(Txn, TestH, []),
%% Default CF iterator
?assertEqual({ok, <<"a">>, <<"v1">>}, rocksdb:iterator_move(DefaultIt, <<>>)),
?assertEqual({ok, <<"b">>, <<"v2">>}, rocksdb:iterator_move(DefaultIt, next)),
%% Test CF iterator
?assertEqual({ok, <<"x">>, <<"cf_v1">>}, rocksdb:iterator_move(TestIt, <<>>)),
?assertEqual({ok, <<"y">>, <<"cf_v2">>}, rocksdb:iterator_move(TestIt, next)),
ok = rocksdb:iterator_close(DefaultIt),
ok = rocksdb:iterator_close(TestIt),
ok = rocksdb:pessimistic_transaction_commit(Txn),
ok = rocksdb:release_pessimistic_transaction(Txn),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% Test DB options (lock_timeout, deadlock_detect)
db_options_test() ->
DbName = "pessimistic_tx_testdb",
_ = rocksdb:destroy(DbName, []),
_ = rocksdb_test_util:rm_rf(DbName),
%% Open with custom TransactionDB options
DbOpts = [{create_if_missing, true}],
TxnDbOpts = [{lock_timeout, 5000}, {deadlock_detect, true}, {num_stripes, 8}],
{ok, Db, _} = rocksdb:open_pessimistic_transaction_db(DbName, DbOpts ++ TxnDbOpts, [{"default", []}]),
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
ok = rocksdb:pessimistic_transaction_put(Txn, <<"test">>, <<"value">>),
ok = rocksdb:pessimistic_transaction_commit(Txn),
ok = rocksdb:release_pessimistic_transaction(Txn),
?assertEqual({ok, <<"value">>}, rocksdb:get(Db, <<"test">>, [])),
close_destroy(Db, DbName),
ok.
%% Test transaction options (set_snapshot, deadlock_detect, lock_timeout)
txn_options_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
%% Put some initial data
ok = rocksdb:put(Db, <<"a">>, <<"v1">>, []),
%% Create transaction with options
TxnOpts = [{set_snapshot, true}, {deadlock_detect, true}, {lock_timeout, 1000}],
{ok, Txn} = rocksdb:pessimistic_transaction(Db, TxnOpts),
?assertEqual({ok, <<"v1">>}, rocksdb:pessimistic_transaction_get(Txn, <<"a">>, [])),
ok = rocksdb:pessimistic_transaction_put(Txn, <<"b">>, <<"v2">>),
ok = rocksdb:pessimistic_transaction_commit(Txn),
ok = rocksdb:release_pessimistic_transaction(Txn),
?assertEqual({ok, <<"v2">>}, rocksdb:get(Db, <<"b">>, [])),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% Basic savepoint test - set and rollback to savepoint
savepoint_basic_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
%% Put initial data
ok = rocksdb:pessimistic_transaction_put(Txn, <<"a">>, <<"v1">>),
%% Set savepoint
ok = rocksdb:pessimistic_transaction_set_savepoint(Txn),
%% Put more data after savepoint
ok = rocksdb:pessimistic_transaction_put(Txn, <<"b">>, <<"v2">>),
ok = rocksdb:pessimistic_transaction_put(Txn, <<"a">>, <<"v1_modified">>),
%% Verify both values are visible in transaction
?assertEqual({ok, <<"v1_modified">>}, rocksdb:pessimistic_transaction_get(Txn, <<"a">>, [])),
?assertEqual({ok, <<"v2">>}, rocksdb:pessimistic_transaction_get(Txn, <<"b">>, [])),
%% Rollback to savepoint
ok = rocksdb:pessimistic_transaction_rollback_to_savepoint(Txn),
%% After rollback: 'a' has original value, 'b' is gone
?assertEqual({ok, <<"v1">>}, rocksdb:pessimistic_transaction_get(Txn, <<"a">>, [])),
?assertEqual(not_found, rocksdb:pessimistic_transaction_get(Txn, <<"b">>, [])),
%% Commit and verify
ok = rocksdb:pessimistic_transaction_commit(Txn),
ok = rocksdb:release_pessimistic_transaction(Txn),
?assertEqual({ok, <<"v1">>}, rocksdb:get(Db, <<"a">>, [])),
?assertEqual(not_found, rocksdb:get(Db, <<"b">>, [])),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% Multiple savepoints test
multiple_savepoints_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
%% First operation
ok = rocksdb:pessimistic_transaction_put(Txn, <<"a">>, <<"v1">>),
%% First savepoint
ok = rocksdb:pessimistic_transaction_set_savepoint(Txn),
%% Second operation
ok = rocksdb:pessimistic_transaction_put(Txn, <<"b">>, <<"v2">>),
%% Second savepoint
ok = rocksdb:pessimistic_transaction_set_savepoint(Txn),
%% Third operation
ok = rocksdb:pessimistic_transaction_put(Txn, <<"c">>, <<"v3">>),
%% All three should be visible
?assertEqual({ok, <<"v1">>}, rocksdb:pessimistic_transaction_get(Txn, <<"a">>, [])),
?assertEqual({ok, <<"v2">>}, rocksdb:pessimistic_transaction_get(Txn, <<"b">>, [])),
?assertEqual({ok, <<"v3">>}, rocksdb:pessimistic_transaction_get(Txn, <<"c">>, [])),
%% Rollback to second savepoint - removes 'c'
ok = rocksdb:pessimistic_transaction_rollback_to_savepoint(Txn),
?assertEqual({ok, <<"v1">>}, rocksdb:pessimistic_transaction_get(Txn, <<"a">>, [])),
?assertEqual({ok, <<"v2">>}, rocksdb:pessimistic_transaction_get(Txn, <<"b">>, [])),
?assertEqual(not_found, rocksdb:pessimistic_transaction_get(Txn, <<"c">>, [])),
%% Rollback to first savepoint - removes 'b'
ok = rocksdb:pessimistic_transaction_rollback_to_savepoint(Txn),
?assertEqual({ok, <<"v1">>}, rocksdb:pessimistic_transaction_get(Txn, <<"a">>, [])),
?assertEqual(not_found, rocksdb:pessimistic_transaction_get(Txn, <<"b">>, [])),
?assertEqual(not_found, rocksdb:pessimistic_transaction_get(Txn, <<"c">>, [])),
%% Commit and verify only 'a' is saved
ok = rocksdb:pessimistic_transaction_commit(Txn),
ok = rocksdb:release_pessimistic_transaction(Txn),
?assertEqual({ok, <<"v1">>}, rocksdb:get(Db, <<"a">>, [])),
?assertEqual(not_found, rocksdb:get(Db, <<"b">>, [])),
?assertEqual(not_found, rocksdb:get(Db, <<"c">>, [])),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% Pop savepoint test - discard savepoint without rollback
pop_savepoint_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
ok = rocksdb:pessimistic_transaction_put(Txn, <<"a">>, <<"v1">>),
%% Set savepoint
ok = rocksdb:pessimistic_transaction_set_savepoint(Txn),
ok = rocksdb:pessimistic_transaction_put(Txn, <<"b">>, <<"v2">>),
%% Pop savepoint (discard it without rolling back)
ok = rocksdb:pessimistic_transaction_pop_savepoint(Txn),
%% Both values should still be visible
?assertEqual({ok, <<"v1">>}, rocksdb:pessimistic_transaction_get(Txn, <<"a">>, [])),
?assertEqual({ok, <<"v2">>}, rocksdb:pessimistic_transaction_get(Txn, <<"b">>, [])),
%% Now rollback_to_savepoint should fail (no savepoint exists)
?assertMatch({error, _}, rocksdb:pessimistic_transaction_rollback_to_savepoint(Txn)),
%% Commit - both values should be saved
ok = rocksdb:pessimistic_transaction_commit(Txn),
ok = rocksdb:release_pessimistic_transaction(Txn),
?assertEqual({ok, <<"v1">>}, rocksdb:get(Db, <<"a">>, [])),
?assertEqual({ok, <<"v2">>}, rocksdb:get(Db, <<"b">>, [])),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% Error case: rollback_to_savepoint without setting one
no_savepoint_error_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
ok = rocksdb:pessimistic_transaction_put(Txn, <<"a">>, <<"v1">>),
%% Try to rollback without a savepoint
?assertMatch({error, _}, rocksdb:pessimistic_transaction_rollback_to_savepoint(Txn)),
%% Try to pop without a savepoint
?assertMatch({error, _}, rocksdb:pessimistic_transaction_pop_savepoint(Txn)),
%% Data should still be there
?assertEqual({ok, <<"v1">>}, rocksdb:pessimistic_transaction_get(Txn, <<"a">>, [])),
ok = rocksdb:pessimistic_transaction_rollback(Txn),
ok = rocksdb:release_pessimistic_transaction(Txn),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% Test transaction ID
get_id_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
{ok, Txn1} = rocksdb:pessimistic_transaction(Db, []),
{ok, Txn2} = rocksdb:pessimistic_transaction(Db, []),
%% Each transaction should have a unique ID
{ok, Id1} = rocksdb:pessimistic_transaction_get_id(Txn1),
{ok, Id2} = rocksdb:pessimistic_transaction_get_id(Txn2),
?assert(is_integer(Id1)),
?assert(is_integer(Id2)),
?assertNotEqual(Id1, Id2),
ok = rocksdb:pessimistic_transaction_rollback(Txn1),
ok = rocksdb:pessimistic_transaction_rollback(Txn2),
ok = rocksdb:release_pessimistic_transaction(Txn1),
ok = rocksdb:release_pessimistic_transaction(Txn2),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% Test get_waiting_txns - not waiting case
get_waiting_txns_not_waiting_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
%% When not waiting on anything, should return empty list
{ok, Result} = rocksdb:pessimistic_transaction_get_waiting_txns(Txn),
?assert(is_map(Result)),
?assertMatch(#{waiting_txns := []}, Result),
ok = rocksdb:pessimistic_transaction_rollback(Txn),
ok = rocksdb:release_pessimistic_transaction(Txn),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% Test get_waiting_txns - with lock contention
%% This test spawns a process that will block trying to acquire a lock
get_waiting_txns_contention_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
%% Put initial data
ok = rocksdb:put(Db, <<"key">>, <<"v1">>, []),
%% Transaction 1 acquires lock
{ok, Txn1} = rocksdb:pessimistic_transaction(Db, []),
{ok, Id1} = rocksdb:pessimistic_transaction_get_id(Txn1),
{ok, <<"v1">>} = rocksdb:pessimistic_transaction_get_for_update(Txn1, <<"key">>, []),
%% Spawn a process that will try to get the same lock with a long timeout
Self = self(),
Pid = spawn(fun() ->
{ok, Txn2} = rocksdb:pessimistic_transaction(Db, [{lock_timeout, 10000}]),
Self ! {txn2_started, Txn2},
%% This will block waiting for Txn1's lock
Result = rocksdb:pessimistic_transaction_get_for_update(Txn2, <<"key">>, []),
Self ! {txn2_result, Result, Txn2}
end),
%% Wait for Txn2 to start
Txn2 = receive
{txn2_started, T2} -> T2
after 1000 ->
error(timeout_waiting_for_txn2)
end,
%% Give some time for Txn2 to start waiting
timer:sleep(100),
%% Check waiting txns for Txn2 - should be waiting on Txn1
{ok, WaitInfo} = rocksdb:pessimistic_transaction_get_waiting_txns(Txn2),
?assert(is_map(WaitInfo)),
#{waiting_txns := WaitingTxns, key := WaitingKey} = WaitInfo,
%% Should be waiting on Txn1
?assertEqual([Id1], WaitingTxns),
?assertEqual(<<"key">>, WaitingKey),
%% Release Txn1's lock
ok = rocksdb:pessimistic_transaction_commit(Txn1),
ok = rocksdb:release_pessimistic_transaction(Txn1),
%% Txn2 should now succeed
receive
{txn2_result, {ok, <<"v1">>}, _} -> ok;
{txn2_result, Other, _} -> error({unexpected_result, Other})
after 5000 ->
exit(Pid, kill),
error(timeout_waiting_for_txn2_result)
end,
%% Cleanup Txn2
ok = rocksdb:pessimistic_transaction_rollback(Txn2),
ok = rocksdb:release_pessimistic_transaction(Txn2),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% Test multi_get - batch get without locks
multi_get_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
%% Put some initial data
ok = rocksdb:put(Db, <<"key1">>, <<"value1">>, []),
ok = rocksdb:put(Db, <<"key2">>, <<"value2">>, []),
ok = rocksdb:put(Db, <<"key3">>, <<"value3">>, []),
%% Start a transaction and add some changes
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
%% Add a new key and modify an existing one
ok = rocksdb:pessimistic_transaction_put(Txn, <<"key4">>, <<"value4">>),
ok = rocksdb:pessimistic_transaction_put(Txn, <<"key1">>, <<"modified1">>),
%% Test multi_get - should see uncommitted changes within transaction
Results = rocksdb:pessimistic_transaction_multi_get(Txn, [<<"key1">>, <<"key2">>, <<"key4">>, <<"key5">>], []),
?assertEqual([{ok, <<"modified1">>}, {ok, <<"value2">>}, {ok, <<"value4">>}, not_found], Results),
%% Commit and verify
ok = rocksdb:pessimistic_transaction_commit(Txn),
ok = rocksdb:release_pessimistic_transaction(Txn),
%% Verify via regular multi_get
Results2 = rocksdb:multi_get(Db, [<<"key1">>, <<"key2">>, <<"key4">>], []),
?assertEqual([{ok, <<"modified1">>}, {ok, <<"value2">>}, {ok, <<"value4">>}], Results2),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.
%% Test multi_get_for_update - batch get with exclusive locks
multi_get_for_update_test() ->
Db = destroy_reopen("pessimistic_tx_testdb", [{create_if_missing, true}]),
%% Put some initial data
ok = rocksdb:put(Db, <<"key1">>, <<"value1">>, []),
ok = rocksdb:put(Db, <<"key2">>, <<"value2">>, []),
ok = rocksdb:put(Db, <<"key3">>, <<"value3">>, []),
%% Start a transaction
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
%% Multi-get with exclusive locks
Results = rocksdb:pessimistic_transaction_multi_get_for_update(Txn, [<<"key1">>, <<"key2">>, <<"key4">>], []),
?assertEqual([{ok, <<"value1">>}, {ok, <<"value2">>}, not_found], Results),
%% Modify the locked keys
ok = rocksdb:pessimistic_transaction_put(Txn, <<"key1">>, <<"modified1">>),
ok = rocksdb:pessimistic_transaction_put(Txn, <<"key2">>, <<"modified2">>),
%% Get again should see the modifications
Results2 = rocksdb:pessimistic_transaction_multi_get_for_update(Txn, [<<"key1">>, <<"key2">>], []),
?assertEqual([{ok, <<"modified1">>}, {ok, <<"modified2">>}], Results2),
%% Commit
ok = rocksdb:pessimistic_transaction_commit(Txn),
ok = rocksdb:release_pessimistic_transaction(Txn),
%% Verify committed values
?assertEqual({ok, <<"modified1">>}, rocksdb:get(Db, <<"key1">>, [])),
?assertEqual({ok, <<"modified2">>}, rocksdb:get(Db, <<"key2">>, [])),
close_destroy(Db, "pessimistic_tx_testdb"),
ok.