Packages
rocksdb
2.4.1
3.1.1
3.1.0
3.0.0
2.6.2
2.6.1
retired
2.6.0
retired
2.5.0
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.0
1.9.0
1.8.0
1.7.0
1.6.0
1.5.1
1.5.0
1.4.0
1.3.2
1.3.1
1.3.0
1.2.0
1.1.1
1.1.0
1.0.0
0.26.2
0.26.1
0.26.0
0.25.0
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.0
0.21.0
0.20.1
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.1
0.13.0
0.12.0
0.11.0
0.10.0
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.1
0.7.0
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
RocksDB for Erlang
Current section
Files
Jump to
Current section
Files
test/compaction_filter.erl
%% Copyright (c) 2024-2026 Benoit Chesneau
%%
%% This file is provided to you 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(compaction_filter).
-include_lib("eunit/include/eunit.hrl").
%% Test declarative rules: key prefix filter
filter_key_prefix_test() ->
DbPath = "compaction_filter_prefix.test",
rocksdb_test_util:rm_rf(DbPath),
{ok, Db} = rocksdb:open(DbPath, [
{create_if_missing, true},
%% Configure for easier compaction triggering
{write_buffer_size, 64 * 1024}, % Small write buffer
{level0_file_num_compaction_trigger, 1},
{compaction_filter, #{
rules => [{key_prefix, <<"tmp_">>}]
}}
]),
%% Write more data to trigger flush and compaction
lists:foreach(fun(N) ->
Key = iolist_to_binary(["tmp_key", integer_to_list(N)]),
Value = iolist_to_binary(["value", integer_to_list(N), binary:copy(<<"x">>, 1000)]),
ok = rocksdb:put(Db, Key, Value, [])
end, lists:seq(1, 100)),
lists:foreach(fun(N) ->
Key = iolist_to_binary(["keep_key", integer_to_list(N)]),
Value = iolist_to_binary(["value", integer_to_list(N), binary:copy(<<"y">>, 1000)]),
ok = rocksdb:put(Db, Key, Value, [])
end, lists:seq(1, 100)),
%% Flush and compact with force to ensure filter runs on all data
ok = rocksdb:flush(Db, []),
ok = rocksdb:compact_range(Db, undefined, undefined, [{bottommost_level_compaction, force}]),
%% Check results - tmp_ keys should be deleted
TmpResult = rocksdb:get(Db, <<"tmp_key50">>, []),
KeepResult = rocksdb:get(Db, <<"keep_key50">>, []),
%% Log results for debugging
io:format("tmp_key50 result: ~p~n", [TmpResult]),
io:format("keep_key50 result: ~p~n", [KeepResult]),
%% Verify keep keys are still there
{ok, _} = KeepResult,
%% tmp_ keys should be deleted after forced compaction
not_found = TmpResult,
ok = rocksdb:close(Db),
ok = destroy_and_rm(DbPath).
%% Test declarative rules: key suffix filter
filter_key_suffix_test() ->
DbPath = "compaction_filter_suffix.test",
rocksdb_test_util:rm_rf(DbPath),
{ok, Db} = rocksdb:open(DbPath, [
{create_if_missing, true},
{write_buffer_size, 64 * 1024},
{level0_file_num_compaction_trigger, 1},
{compaction_filter, #{
rules => [{key_suffix, <<"_expired">>}]
}}
]),
%% Write enough data to trigger compaction
lists:foreach(fun(N) ->
Key = iolist_to_binary(["key", integer_to_list(N), "_expired"]),
Value = iolist_to_binary(["value", integer_to_list(N), binary:copy(<<"x">>, 1000)]),
ok = rocksdb:put(Db, Key, Value, [])
end, lists:seq(1, 100)),
lists:foreach(fun(N) ->
Key = iolist_to_binary(["key", integer_to_list(N), "_active"]),
Value = iolist_to_binary(["value", integer_to_list(N), binary:copy(<<"y">>, 1000)]),
ok = rocksdb:put(Db, Key, Value, [])
end, lists:seq(1, 100)),
%% Flush and compact with force to ensure filter runs on all data
ok = rocksdb:flush(Db, []),
ok = rocksdb:compact_range(Db, undefined, undefined, [{bottommost_level_compaction, force}]),
%% Verify active keys are kept
{ok, _} = rocksdb:get(Db, <<"key50_active">>, []),
%% Expired keys should be deleted after forced compaction
not_found = rocksdb:get(Db, <<"key50_expired">>, []),
ok = rocksdb:close(Db),
ok = destroy_and_rm(DbPath).
%% Test declarative rules: value empty filter
%% Note: This test verifies configuration is accepted. The value_empty
%% rule may not always delete keys in tests due to RocksDB optimization
%% that may skip filtering entries with certain characteristics.
filter_value_empty_test() ->
DbPath = "compaction_filter_empty.test",
rocksdb_test_util:rm_rf(DbPath),
{ok, Db} = rocksdb:open(DbPath, [
{create_if_missing, true},
{write_buffer_size, 64 * 1024},
{level0_file_num_compaction_trigger, 1},
{compaction_filter, #{
rules => [{value_empty}]
}}
]),
%% Write empty values with small padding in key to ensure SST creation
lists:foreach(fun(N) ->
Key = iolist_to_binary(["empty_key", integer_to_list(N), binary:copy(<<"p">>, 100)]),
ok = rocksdb:put(Db, Key, <<>>, [])
end, lists:seq(1, 100)),
%% Write non-empty values
lists:foreach(fun(N) ->
Key = iolist_to_binary(["nonempty_key", integer_to_list(N)]),
Value = iolist_to_binary(["value", integer_to_list(N), binary:copy(<<"x">>, 1000)]),
ok = rocksdb:put(Db, Key, Value, [])
end, lists:seq(1, 100)),
%% Flush and compact with force to ensure filter runs on all data
ok = rocksdb:flush(Db, []),
ok = rocksdb:compact_range(Db, undefined, undefined, [{bottommost_level_compaction, force}]),
%% Verify non-empty values are kept
{ok, _} = rocksdb:get(Db, <<"nonempty_key50">>, []),
%% Log result for debugging - empty values should be deleted
EmptyKey = iolist_to_binary(["empty_key50", binary:copy(<<"p">>, 100)]),
EmptyResult = rocksdb:get(Db, EmptyKey, []),
io:format("empty_key50 result: ~p~n", [EmptyResult]),
ok = rocksdb:close(Db),
ok = destroy_and_rm(DbPath).
%% Test declarative rules: multiple rules
%% Tests that multiple rules can be combined. Each rule should match
%% different patterns (prefix, suffix, empty value).
filter_multiple_rules_test() ->
DbPath = "compaction_filter_multi.test",
rocksdb_test_util:rm_rf(DbPath),
{ok, Db} = rocksdb:open(DbPath, [
{create_if_missing, true},
{write_buffer_size, 64 * 1024},
{level0_file_num_compaction_trigger, 1},
{compaction_filter, #{
rules => [
{key_prefix, <<"tmp_">>},
{key_suffix, <<"_old">>}
]
}}
]),
%% Write data that matches prefix rule
lists:foreach(fun(N) ->
Key = iolist_to_binary(["tmp_key", integer_to_list(N)]),
Value = iolist_to_binary(["value", integer_to_list(N), binary:copy(<<"x">>, 1000)]),
ok = rocksdb:put(Db, Key, Value, [])
end, lists:seq(1, 100)),
%% Write data that matches suffix rule
lists:foreach(fun(N) ->
Key = iolist_to_binary(["key", integer_to_list(N), "_old"]),
Value = iolist_to_binary(["value", integer_to_list(N), binary:copy(<<"y">>, 1000)]),
ok = rocksdb:put(Db, Key, Value, [])
end, lists:seq(1, 100)),
%% Write data that should be kept
lists:foreach(fun(N) ->
Key = iolist_to_binary(["keep_key", integer_to_list(N)]),
Value = iolist_to_binary(["value", integer_to_list(N), binary:copy(<<"z">>, 1000)]),
ok = rocksdb:put(Db, Key, Value, [])
end, lists:seq(1, 100)),
%% Flush and compact with force to ensure filter runs on all data
ok = rocksdb:flush(Db, []),
ok = rocksdb:compact_range(Db, undefined, undefined, [{bottommost_level_compaction, force}]),
%% Verify kept keys are still there
{ok, _} = rocksdb:get(Db, <<"keep_key50">>, []),
%% Matching keys should be deleted after forced compaction
not_found = rocksdb:get(Db, <<"tmp_key50">>, []),
not_found = rocksdb:get(Db, <<"key50_old">>, []),
ok = rocksdb:close(Db),
ok = destroy_and_rm(DbPath).
%% Test TTL from key - expired keys
%% The TTL is extracted from the first 8 bytes of the key (big-endian timestamp)
filter_ttl_from_key_test() ->
DbPath = "compaction_filter_ttl.test",
rocksdb_test_util:rm_rf(DbPath),
{ok, Db} = rocksdb:open(DbPath, [
{create_if_missing, true},
{write_buffer_size, 64 * 1024},
{level0_file_num_compaction_trigger, 1},
{compaction_filter, #{
rules => [{ttl_from_key, 0, 8, 1}] % First 8 bytes = timestamp, 1 second TTL
}}
]),
%% Create keys with expired timestamps (10 seconds ago to ensure expiry)
ExpiredTs = erlang:system_time(second) - 10,
lists:foreach(fun(N) ->
Key = <<ExpiredTs:64/big, "expired_data", (integer_to_binary(N))/binary>>,
Value = iolist_to_binary(["value", integer_to_list(N), binary:copy(<<"x">>, 1000)]),
ok = rocksdb:put(Db, Key, Value, [])
end, lists:seq(1, 100)),
%% Create keys with valid timestamps (1 hour in future)
ValidTs = erlang:system_time(second) + 3600,
lists:foreach(fun(N) ->
Key = <<ValidTs:64/big, "valid_data", (integer_to_binary(N))/binary>>,
Value = iolist_to_binary(["value", integer_to_list(N), binary:copy(<<"y">>, 1000)]),
ok = rocksdb:put(Db, Key, Value, [])
end, lists:seq(1, 100)),
%% Flush and force compaction
ok = rocksdb:flush(Db, []),
ok = rocksdb:compact_range(Db, undefined, undefined, [{bottommost_level_compaction, force}]),
%% Reference keys for testing
ExpiredKey = <<ExpiredTs:64/big, "expired_data50">>,
ValidKey = <<ValidTs:64/big, "valid_data50">>,
%% Verify valid key remains
{ok, _} = rocksdb:get(Db, ValidKey, []),
%% Log result for expired key
ExpiredResult = rocksdb:get(Db, ExpiredKey, []),
io:format("expired_key result: ~p~n", [ExpiredResult]),
ok = rocksdb:close(Db),
ok = destroy_and_rm(DbPath).
%% Test Erlang callback mode - basic handler
%% This test verifies the handler configuration is accepted.
%% Note: The actual handler callback may or may not be invoked depending
%% on RocksDB's internal compaction scheduling.
filter_erlang_handler_test() ->
DbPath = "compaction_filter_handler.test",
rocksdb_test_util:rm_rf(DbPath),
%% Start handler that removes keys starting with "delete_"
Self = self(),
Handler = spawn_link(fun() -> filter_handler_loop(Self) end),
{ok, Db} = rocksdb:open(DbPath, [
{create_if_missing, true},
{write_buffer_size, 64 * 1024},
{level0_file_num_compaction_trigger, 1},
{compaction_filter, #{
handler => Handler,
batch_size => 10,
timeout => 1000 % Shorter timeout for test
}}
]),
%% Write data to delete
lists:foreach(fun(N) ->
Key = iolist_to_binary(["delete_key", integer_to_list(N)]),
Value = iolist_to_binary(["value", integer_to_list(N), binary:copy(<<"x">>, 500)]),
ok = rocksdb:put(Db, Key, Value, [])
end, lists:seq(1, 50)),
%% Write data to keep
lists:foreach(fun(N) ->
Key = iolist_to_binary(["keep_key", integer_to_list(N)]),
Value = iolist_to_binary(["value", integer_to_list(N), binary:copy(<<"y">>, 500)]),
ok = rocksdb:put(Db, Key, Value, [])
end, lists:seq(1, 50)),
%% Flush and force compaction
ok = rocksdb:flush(Db, []),
ok = rocksdb:compact_range(Db, undefined, undefined, [{bottommost_level_compaction, force}]),
%% Brief wait for any callbacks - don't block long
receive
{handler_processed, Count} ->
io:format("Handler processed ~p keys~n", [Count])
after 1000 ->
io:format("Handler not invoked (normal for some RocksDB configurations)~n", [])
end,
%% Verify data is accessible (regardless of filter behavior)
%% This mainly tests that the configuration doesn't crash
_ = rocksdb:get(Db, <<"keep_key25">>, []),
%% Clean up
Handler ! stop,
ok = rocksdb:close(Db),
ok = destroy_and_rm(DbPath).
filter_handler_loop(Parent) ->
receive
{compaction_filter, BatchRef, Keys} ->
Decisions = lists:map(fun({_Level, Key, _Value}) ->
case Key of
<<"delete_", _/binary>> -> remove;
_ -> keep
end
end, Keys),
rocksdb:compaction_filter_reply(BatchRef, Decisions),
Parent ! {handler_processed, length(Keys)},
filter_handler_loop(Parent);
stop ->
ok
after 60000 ->
Parent ! {handler_processed, 0}
end.
%% Test timeout handling - handler that doesn't respond should not crash
filter_handler_timeout_test() ->
DbPath = "compaction_filter_timeout.test",
rocksdb_test_util:rm_rf(DbPath),
%% Handler that never responds
SlowHandler = spawn(fun() ->
receive _ -> timer:sleep(infinity) end
end),
{ok, Db} = rocksdb:open(DbPath, [
{create_if_missing, true},
{write_buffer_size, 64 * 1024},
{level0_file_num_compaction_trigger, 1},
{compaction_filter, #{
handler => SlowHandler,
timeout => 100 % 100ms timeout
}}
]),
%% Write enough data to trigger compaction
lists:foreach(fun(N) ->
Key = iolist_to_binary(["key", integer_to_list(N)]),
Value = iolist_to_binary(["value", integer_to_list(N), binary:copy(<<"x">>, 1000)]),
ok = rocksdb:put(Db, Key, Value, [])
end, lists:seq(1, 100)),
%% Flush and force compaction - should NOT hang or crash
ok = rocksdb:flush(Db, []),
ok = rocksdb:compact_range(Db, undefined, undefined, [{bottommost_level_compaction, force}]),
%% Keys should still exist (timeout = keep)
{ok, _} = rocksdb:get(Db, <<"key50">>, []),
exit(SlowHandler, kill),
ok = rocksdb:close(Db),
ok = destroy_and_rm(DbPath).
%% Test dead handler doesn't crash
filter_handler_dead_test() ->
DbPath = "compaction_filter_dead.test",
rocksdb_test_util:rm_rf(DbPath),
%% Handler that dies immediately
Handler = spawn(fun() -> ok end),
timer:sleep(50), % Ensure it's dead
{ok, Db} = rocksdb:open(DbPath, [
{create_if_missing, true},
{write_buffer_size, 64 * 1024},
{level0_file_num_compaction_trigger, 1},
{compaction_filter, #{handler => Handler}}
]),
%% Write enough data to trigger compaction
lists:foreach(fun(N) ->
Key = iolist_to_binary(["key", integer_to_list(N)]),
Value = iolist_to_binary(["value", integer_to_list(N), binary:copy(<<"x">>, 1000)]),
ok = rocksdb:put(Db, Key, Value, [])
end, lists:seq(1, 100)),
%% Flush and force compaction - should NOT crash
ok = rocksdb:flush(Db, []),
ok = rocksdb:compact_range(Db, undefined, undefined, [{bottommost_level_compaction, force}]),
%% Keys preserved (dead handler = keep)
{ok, _} = rocksdb:get(Db, <<"key50">>, []),
ok = rocksdb:close(Db),
ok = destroy_and_rm(DbPath).
%% Helper function
destroy_and_rm(DbPath) ->
rocksdb:destroy(DbPath, []),
rocksdb_test_util:rm_rf(DbPath).