Skip to content

Commit d241d3b

Browse files
committed
Fix checksum validation for hintfiles
- Also adds some additional unit tests wday-contrib 2338 2346
1 parent 0e0bb1e commit d241d3b

4 files changed

Lines changed: 220 additions & 61 deletions

File tree

include/bitcask.hrl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
%% -------------------------------------------------------------------
22
%%
33
%% Copyright (c) 2010-2017 Basho Technologies, Inc.
4+
%% Copyright (c) 2025 Workday, Inc.
45
%%
56
%% This file is provided to you under the Apache License,
67
%% Version 2.0 (the "License"); you may not use this file
@@ -82,6 +83,13 @@
8283
-define(MAXOFFSET_V2, 16#7fffffffffffffff). % max 63-bit unsigned
8384

8485
%% for hintfile validation
86+
-define(HINT_RECORD_BITS, (?TSTAMPFIELD
87+
+ ?KEYSIZEFIELD + ?TOTALSIZEFIELD + ?TOMBSTONEFIELD_V2 + ?OFFSETFIELD_V2)).
88+
-if(((?HINT_RECORD_BITS) rem 8) =/= 0).
89+
-error("Bad HINT_RECORD_BITS calculation").
90+
-endif.
91+
-define(HINT_RECORD_SZ, (?HINT_RECORD_BITS div 8)).
92+
8593
-define(CHUNK_SIZE, 65535).
8694
-define(MIN_CHUNK_SIZE, 1024).
8795
-define(MAX_CHUNK_SIZE, 134217728).

rebar.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
nowarn_unused_import, % EQC
152152
warnings_as_errors
153153
]},
154+
{eunit_opts, [verbose]},
154155
{deps, [
155156
{meck, "0.9.2"},
156157
{cuttlefish,

src/bitcask.erl

Lines changed: 179 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2229,7 +2229,7 @@ list_data_files_test_() ->
22292229

22302230
list_data_files_test2() ->
22312231
DN = setup_testfolder("bc.test.list"),
2232-
os:cmd("mkdir -p " ++ DN),
2232+
os:cmd("mkdir -p '" ++ DN ++ "'"),
22332233

22342234
%% Generate a list of files from 8->12
22352235
ExpFiles = [?FMT(DN ++ "/~w.bitcask.data", [I]) ||
@@ -2255,7 +2255,7 @@ list_data_files_race_test() ->
22552255
WriteFiles = fun(S,E) ->
22562256
[WriteFile(N) || N <- lists:seq(S, E)]
22572257
end,
2258-
os:cmd("rm -rf " ++ Dir ++ "; mkdir -p " ++ Dir),
2258+
os:cmd("rm -rf '" ++ Dir ++ "'; mkdir -p '" ++ Dir ++ "'"),
22592259
WriteFiles(1,5),
22602260
% Faking 4 as merge file, 5 as write file,
22612261
% then switching to 6 as merge, 7 as write
@@ -2643,6 +2643,22 @@ merge_test2() ->
26432643
end, undefined, default_dataset()),
26442644
ok = bitcask:close(B).
26452645

2646+
merge_wrap_test_() ->
2647+
[{timeout, 120, fun test_merge_wrap/0}].
2648+
2649+
test_merge_wrap() ->
2650+
Dir = setup_testfolder("bc.test.merge_wrap"),
2651+
MaxFileSize = 100,
2652+
NumKeys = 50,
2653+
2654+
B1 = bitcask:open(Dir, [read_write, {max_file_size, MaxFileSize * 10}]),
2655+
[ok = bitcask:put(B1, <<N:32>>, crypto:strong_rand_bytes(MaxFileSize div 2))
2656+
|| N <- lists:seq(1, NumKeys)],
2657+
ok = bitcask:merge(Dir, [{max_file_size, MaxFileSize}]), %% This will trigger a wrap
2658+
Keys = bitcask:fold(B1, fun(K, _V, Acc0) -> [K|Acc0] end, [], -1, -1, true),
2659+
?assertEqual(length(Keys), NumKeys),
2660+
bitcask:close(B1).
2661+
26462662
bitfold_test_() ->
26472663
{timeout, 60, fun bitfold_test2/0}.
26482664

@@ -3084,10 +3100,11 @@ truncated_datafile_test2() ->
30843100
ok = bitcask:close(B2),
30853101
ok.
30863102

3087-
hintfile_tests_() ->
3088-
[{timeout, 60, fun truncated_hintfile_test/0},
3089-
{timeout, 60, fun missing_hintfile_test/0},
3090-
{timeout, 60, fun corrupt_hintfile_test/0}
3103+
hintfile_test_() ->
3104+
[{timeout, 60, fun test_truncated_hintfile/0},
3105+
{timeout, 60, fun test_missing_hintfile/0},
3106+
{timeout, 60, fun test_corrupt_hintfile/0},
3107+
{timeout, 60, fun test_ensure_valid_through_chunks/0}
30913108
].
30923109

30933110
setup_hintfile() ->
@@ -3110,7 +3127,7 @@ setup_hintfile() ->
31103127
{FS1, HintFile}.
31113128

31123129

3113-
missing_hintfile_test() ->
3130+
test_missing_hintfile() ->
31143131
{FS, HintFile} = setup_hintfile(),
31153132
ok = file:delete(HintFile),
31163133
false = bitcask_fileops:is_file(HintFile),
@@ -3119,34 +3136,71 @@ missing_hintfile_test() ->
31193136
%% correct result from data file.
31203137
#filestate{hintfd = undefined, hintcrc = 0} = FS2 = bitcask:validate_or_delete_hintfile(FS),
31213138
false = bitcask_fileops:is_file(HintFile),
3122-
100 = bitcask_fileops:fold_keys(FS2, fun(_, _, _, Acc) -> Acc + 1 end,
3123-
0),
3139+
?assertEqual(100,
3140+
bitcask_fileops:fold_keys(FS2, fun(_, _, _, Acc) -> Acc + 1 end, 0)),
31243141
ok.
31253142

3126-
corrupt_hintfile_test() ->
3143+
test_corrupt_hintfile() ->
31273144
{FS, HintFile} = setup_hintfile(),
31283145
truncate_file(HintFile, {bof, 1}),
31293146

31303147
%% Attempts to validate hintfile and deletes it, still returns
31313148
%% correct result from data file.
31323149
#filestate{hintfd = undefined, hintcrc = 0} = FS2 = bitcask:validate_or_delete_hintfile(FS),
31333150
false = bitcask_fileops:is_file(HintFile),
3134-
100 = bitcask_fileops:fold_keys(FS2, fun(_, _, _, Acc) -> Acc + 1 end,
3135-
0),
3151+
?assertEqual(100,
3152+
bitcask_fileops:fold_keys(FS2, fun(_, _, _, Acc) -> Acc + 1 end, 0)),
31363153
ok.
31373154

3138-
truncated_hintfile_test() ->
3155+
test_truncated_hintfile() ->
31393156
{FS, HintFile} = setup_hintfile(),
31403157
truncate_file(HintFile, {eof, -(?CRCSIZEFIELD + 8)}),
31413158

31423159
%% Attempts to validate hintfile and deletes it, still returns
31433160
%% correct result from data file.
31443161
#filestate{hintfd = undefined, hintcrc = 0} = FS2 = bitcask:validate_or_delete_hintfile(FS),
31453162
false = bitcask_fileops:is_file(HintFile),
3146-
100 = bitcask_fileops:fold_keys(FS2, fun(_, _, _, Acc) -> Acc + 1 end,
3147-
0),
3163+
?assertEqual(100,
3164+
bitcask_fileops:fold_keys(FS2, fun(_, _, _, Acc) -> Acc + 1 end, 0)),
31483165
ok.
31493166

3167+
%% Ensures valid hintfiles are not marked as invalid due to improper chunking
3168+
test_ensure_valid_through_chunks() ->
3169+
Dir = setup_testfolder("bc.test.valid_through_chunks"),
3170+
B1 = bitcask:open(Dir, [read_write]),
3171+
{BytesWritten, NumWritten} = write_until_hint_bytes(B1, ?CHUNK_SIZE * 10),
3172+
?assert(BytesWritten > ?CHUNK_SIZE - ?HINT_RECORD_SZ),
3173+
State = get_state(B1),
3174+
FS = State#bc_state.write_file,
3175+
ok = bitcask:close(B1),
3176+
[HintFile|_] = filelib:wildcard(Dir ++ "/*.hint"),
3177+
3178+
%% Reads and validates hintfile
3179+
#filestate{hintfd = HintFD, hintcrc = HintCRC} = FS1 = bitcask:validate_or_delete_hintfile(FS),
3180+
?assertNot(HintFD == undefined),
3181+
?assert(HintCRC > 0),
3182+
?assertEqual(NumWritten, bitcask_fileops:fold_keys(FS, fun(_, _, _, Acc) -> Acc + 1 end, 0)),
3183+
?assert(bitcask_fileops:is_file(HintFile)),
3184+
{FS1, HintFile}.
3185+
3186+
write_until_hint_bytes(B1, MaxBytes) ->
3187+
write_until_hint_bytes_loop(B1, MaxBytes, {0, 0}).
3188+
3189+
write_until_hint_bytes_loop(B1, MaxBytes, {BytesWritten, NumWritten} = Acc) ->
3190+
Key = iolist_to_binary(["k", integer_to_binary(NumWritten)]),
3191+
NewBytes = BytesWritten + byte_size(Key) + ?HINT_RECORD_SZ,
3192+
case NewBytes > MaxBytes of
3193+
true ->
3194+
Acc;
3195+
false ->
3196+
case bitcask:put(B1, Key, <<NumWritten:32>>) of
3197+
ok ->
3198+
write_until_hint_bytes_loop(B1, MaxBytes, {NewBytes, NumWritten + 1});
3199+
Error ->
3200+
Error
3201+
end
3202+
end.
3203+
31503204
trailing_junk_big_datafile_test_() ->
31513205
{timeout, 60, fun trailing_junk_big_datafile_test2/0}.
31523206

@@ -3430,6 +3484,63 @@ freeze_close_reopen() ->
34303484
os:cmd("rm -rf " ++ Cask)
34313485
end.
34323486

3487+
fold_file_failure_test_() ->
3488+
[{timeout, 60, fun test_fold_file_failure/0},
3489+
{timeout, 60, fun test_fold_file_missing/0}].
3490+
3491+
test_fold_file_failure() ->
3492+
Dir = setup_testfolder("bc.fold_file_failure"),
3493+
B1 = bitcask:open(Dir, [read_write, {max_file_size, 100}]),
3494+
[ok = bitcask:put(B1, <<"k">>, <<X:32>>) || X <- lists:seq(1, 100)],
3495+
ok = bitcask:close(B1),
3496+
3497+
[_, TargetFile |_] = filelib:wildcard(Dir ++ "/*.data"),
3498+
3499+
B2 = bitcask:open(Dir, [read_write, {max_file_size, 100}]),
3500+
3501+
meck:new(bitcask_io, [passthrough]),
3502+
meck:expect(bitcask_io, file_open,
3503+
fun(Filename, Opts) ->
3504+
case Filename =:= TargetFile of
3505+
true ->
3506+
{error, eacces};
3507+
false ->
3508+
meck:passthrough([Filename, Opts])
3509+
end
3510+
end),
3511+
{error,max_retries_exceeded_for_fold} = bitcask:fold(B2, fun(K, _V, Acc0) -> [K|Acc0] end, [], -1, -1, true),
3512+
meck:unload(bitcask_io).
3513+
3514+
%% In cases where a data file is missing, Bitcask will skip it during fold.
3515+
test_fold_file_missing() ->
3516+
Dir = setup_testfolder("bc.fold_file_missinig"),
3517+
B1 = bitcask:open(Dir, [read_write, {max_file_size, 100}]),
3518+
[ok = bitcask:put(B1, <<"k", X:32>>, <<X:32>>) || X <- lists:seq(1, 100)],
3519+
ok = bitcask:close(B1),
3520+
3521+
[_, TargetFile |_] = filelib:wildcard(Dir ++ "/*.data"),
3522+
3523+
B2 = bitcask:open(Dir, [read_write, {max_file_size, 100}]),
3524+
3525+
PreError = bitcask:fold(B2,
3526+
fun(K, _V, Acc0) -> [K|Acc0] end, [], -1, -1, true),
3527+
?assertEqual(length(PreError), 100),
3528+
meck:new(bitcask_io, [passthrough]),
3529+
meck:expect(bitcask_io, file_open,
3530+
fun(Filename, Opts) ->
3531+
case Filename =:= TargetFile of
3532+
true ->
3533+
{error, enoent};
3534+
false ->
3535+
meck:passthrough([Filename, Opts])
3536+
end
3537+
end),
3538+
PostError = bitcask:fold(B2,
3539+
fun(K, _V, Acc0) -> [K|Acc0] end, [], -1, -1, true),
3540+
?assertEqual(length(PostError), 96),
3541+
meck:unload(bitcask_io),
3542+
bitcask:close(B2).
3543+
34333544
fold_itercount_test_() ->
34343545
{timeout, 60, fun fold_itercount_test2/0}.
34353546

@@ -3912,20 +4023,58 @@ update_tombstones_test() ->
39124023
TombCount = bitcask:subfold(CountF, Fds, 0),
39134024
?assertEqual(1, TombCount).
39144025

3915-
make_merge_file(Dir, Probability) ->
3916-
case filelib:is_dir(Dir) of
3917-
true ->
3918-
DataFiles = filelib:wildcard("*.data", Dir),
3919-
{ok, FH} = file:open(Dir ++ "/merge.txt", [write,raw]),
3920-
[case rand:uniform(100) < Probability of
3921-
true ->
3922-
file:write(FH, io_lib:format("~s\n", [DF]));
3923-
false ->
3924-
ok
3925-
end || DF <- DataFiles],
3926-
ok = file:close(FH);
3927-
false ->
3928-
ok
3929-
end.
4026+
un_write_test_() ->
4027+
[{timeout, 120, fun test_un_write_merge_race/0}].
4028+
4029+
%% When an object is "put" it's first written to the data file then updated
4030+
%% in the keydir. In between these two actions, another process may update
4031+
%% the key in keydir with a later file. This is typically due to a merge.
4032+
%%
4033+
%% When this occurs, we unwrite the "put" from the file because we no longer
4034+
%% need it.
4035+
test_un_write_merge_race() ->
4036+
meck:new(bitcask_fileops, [passthrough]),
4037+
4038+
UnWriteCalled = counters:new(1, []),
4039+
meck:expect(bitcask_fileops, un_write,
4040+
fun(FS) ->
4041+
counters:add(UnWriteCalled, 1, 1),
4042+
meck:passthrough([FS])
4043+
end),
4044+
4045+
Dir = setup_testfolder("bc.test.un_write_race"),
4046+
TestPid = self(),
4047+
NumKeys = 10,
4048+
4049+
B1 = bitcask:open(Dir, [read_write, {max_file_size, 500}]),
4050+
[ok = bitcask:put(B1, <<N:32>>, <<"initial">>) || N <- lists:seq(1, NumKeys)],
4051+
bitcask:close(B1),
4052+
4053+
B2 = bitcask:open(Dir, [read_write, {max_file_size, 1000}]),
4054+
4055+
spawn(fun() ->
4056+
ok = bitcask:merge(Dir),
4057+
TestPid ! merge_done
4058+
end),
4059+
4060+
[ok = bitcask:put(B2, <<N:32>>, <<"race_update">>)
4061+
|| N <- lists:seq(1, NumKeys)],
4062+
4063+
[?assertEqual({ok, <<"race_update">>}, bitcask:get(B2, <<N:32>>))
4064+
|| N <- lists:seq(1, NumKeys)],
4065+
4066+
receive
4067+
merge_done -> ok
4068+
after 10000 ->
4069+
throw(merge_timeout)
4070+
end,
4071+
4072+
?assert(counters:get(UnWriteCalled, 1) > 0),
4073+
4074+
[begin
4075+
?assertEqual({ok, <<"race_update">>}, bitcask:get(B2, <<N:32>>))
4076+
end || N <- lists:seq(1, NumKeys)],
4077+
bitcask:close(B2),
4078+
meck:unload(bitcask_fileops).
39304079

39314080
-endif. % TEST

0 commit comments

Comments
 (0)