@@ -2229,7 +2229,7 @@ list_data_files_test_() ->
22292229
22302230list_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+
26462662bitfold_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
30933110setup_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+
31503204trailing_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+
34333544fold_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