Skip to content

Commit b8118c9

Browse files
committed
Staging 1.7 merge to develop
2 parents de15411 + 2be3c41 commit b8118c9

File tree

3 files changed

+195
-35
lines changed

3 files changed

+195
-35
lines changed

rebar.config

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
{cuttlefish, ".*", {git, "git://github.com/basho/cuttlefish.git", {branch, "develop"}}}
77
]}.
88

9-
{port_env,
9+
{port_env,
1010
[
11-
{"DRV_CFLAGS",
11+
{"DRV_CFLAGS",
1212
"-g -Wall -fPIC -errors $ERL_CFLAGS"},
1313

1414
%% Solaris specific flags

src/bitcask.erl

Lines changed: 176 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,17 @@ get(Ref, Key, TryNum) ->
234234
E when is_record(E, bitcask_entry) ->
235235
case E#bitcask_entry.tstamp < expiry_time(State#bc_state.opts) of
236236
true ->
237-
%% Expired entry; remove from keydir and free up memory
238-
ok = bitcask_nifs:keydir_remove(State#bc_state.keydir, Key),
239-
not_found;
237+
%% Expired entry; remove from keydir
238+
case bitcask_nifs:keydir_remove(State#bc_state.keydir, Key,
239+
E#bitcask_entry.tstamp,
240+
E#bitcask_entry.file_id,
241+
E#bitcask_entry.offset) of
242+
ok ->
243+
not_found;
244+
already_exists ->
245+
% Updated since last read, try again.
246+
get(Ref, Key, TryNum-1)
247+
end;
240248
false ->
241249
%% HACK: Use a fully-qualified call to get_filestate/2 so that
242250
%% we can intercept calls w/ Pulse tests.
@@ -1151,7 +1159,7 @@ get_opt(Key, Opts) ->
11511159
put_state(Ref, State) ->
11521160
erlang:put(Ref, State).
11531161

1154-
kt_id(Key) ->
1162+
kt_id(Key) when is_binary(Key) ->
11551163
Key.
11561164

11571165
scan_key_files([], _KeyDir, Acc, _CloseFile, _KT) ->
@@ -1241,6 +1249,7 @@ init_keydir(Dirname, WaitTime, ReadWriteModeP, KT) ->
12411249

12421250
case ScanResult of
12431251
{error, _} ->
1252+
ok = bitcask_nifs:keydir_release(KeyDir),
12441253
ScanResult;
12451254
_ ->
12461255
%% Now that we loaded all the data, mark the keydir as ready
@@ -1326,19 +1335,9 @@ get_filestate(FileId, Dirname, ReadFiles, Mode) ->
13261335

13271336

13281337
list_data_files(Dirname, WritingFile, MergingFile) ->
1329-
%% Get list of {tstamp, filename} for all files in the directory then
1330-
%% reverse sort that list and extract the fully-qualified filename.
13311338
Files1 = bitcask_fileops:data_file_tstamps(Dirname),
1332-
Files2 = bitcask_fileops:data_file_tstamps(Dirname),
1333-
% TODO: Remove crazy
1334-
if Files1 == Files2 ->
1335-
%% No race, Files1 is a stable list.
1336-
[F || {_Tstamp, F} <- lists:sort(Files1),
1337-
F /= WritingFile,
1338-
F /= MergingFile];
1339-
true ->
1340-
list_data_files(Dirname, WritingFile, MergingFile)
1341-
end.
1339+
[F || {_Tstamp, F} <- lists:sort(Files1),
1340+
F /= WritingFile, F /= MergingFile].
13421341

13431342
merge_files(#mstate { input_files = [] } = State) ->
13441343
State;
@@ -1659,7 +1658,16 @@ readable_and_setuid_files(Dirname) ->
16591658
%% Filter out files with setuid bit set: they've been marked for
16601659
%% deletion by an earlier *successful* merge.
16611660
Fs = [F || F <- list_data_files(Dirname, WritingFile, MergingFile)],
1662-
lists:partition(fun(F) -> not has_pending_delete_bit(F) end, Fs).
1661+
1662+
WritingFile2 = bitcask_lockops:read_activefile(write, Dirname),
1663+
MergingFile2 = bitcask_lockops:read_activefile(merge, Dirname),
1664+
case {WritingFile2, MergingFile2} of
1665+
{WritingFile, MergingFile} ->
1666+
lists:partition(fun(F) -> not has_pending_delete_bit(F) end, Fs);
1667+
_ ->
1668+
% Changed while fetching file list, retry
1669+
readable_and_setuid_files(Dirname)
1670+
end.
16631671

16641672
%% Internal put - have validated that the file is opened for write
16651673
%% and looked up the state at this point
@@ -1720,15 +1728,21 @@ do_put(Key, Value, #bc_state{write_file = WriteFile} = State,
17201728
State3 = wrap_write_file(State2),
17211729
do_put(Key, Value, State3, Retries - 1, already_exists);
17221730

1723-
#bitcask_entry{file_id=OldFileId,offset=OldOffset}
1724-
when OldFileId =< WriteFileId ->
1725-
PrevTombstone = <<?TOMBSTONE2_STR, OldFileId:32>>,
1726-
{ok, WriteFile1, _, _} =
1727-
bitcask_fileops:write(WriteFile0, Key, PrevTombstone,
1728-
Tstamp),
1729-
State3 = State2#bc_state{write_file = WriteFile1},
1731+
#bitcask_entry{file_id=OldFileId,offset=OldOffset} ->
1732+
State3 =
1733+
case OldFileId < WriteFileId of
1734+
true ->
1735+
PrevTomb = <<?TOMBSTONE2_STR, OldFileId:32>>,
1736+
{ok, WriteFile1, _, _} =
1737+
bitcask_fileops:write(WriteFile0, Key,
1738+
PrevTomb, Tstamp),
1739+
State2#bc_state{write_file = WriteFile1};
1740+
false ->
1741+
State2
1742+
end,
17301743
write_and_keydir_put(State3, Key, Value, Tstamp, Retries,
17311744
bitcask_time:tstamp(), OldFileId, OldOffset);
1745+
17321746
_ ->
17331747
State3 = State2#bc_state{write_file = WriteFile0},
17341748
write_and_keydir_put(State3, Key, Value, Tstamp, Retries,
@@ -1908,9 +1922,12 @@ expiry_merge([], _LiveKeyDir, _KT, Acc) ->
19081922
Acc;
19091923
expiry_merge([File | Files], LiveKeyDir, KT, Acc0) ->
19101924
FileId = bitcask_fileops:file_tstamp(File),
1911-
Fun = fun(K, Tstamp, {Offset, _TotalSz}, Acc) ->
1912-
bitcask_nifs:keydir_remove(LiveKeyDir, KT(K), Tstamp, FileId, Offset),
1913-
Acc
1925+
Fun = fun({tombstone, _}, _, _, Acc) ->
1926+
Acc;
1927+
(K, Tstamp, {Offset, _TotalSz}, Acc) ->
1928+
bitcask_nifs:keydir_remove(LiveKeyDir, KT(K), Tstamp, FileId,
1929+
Offset),
1930+
Acc
19141931
end,
19151932
case bitcask_fileops:fold_keys(File, Fun, ok, default) of
19161933
{error, Reason} ->
@@ -1965,11 +1982,13 @@ init_dataset(Dirname, Opts, KVs) ->
19651982
os:cmd(?FMT("rm -rf ~s", [Dirname])),
19661983

19671984
B = bitcask:open(Dirname, [read_write] ++ Opts),
1968-
lists:foldl(fun({K, V}, _) ->
1969-
ok = bitcask:put(B, K, V)
1970-
end, undefined, KVs),
1985+
put_kvs(B, KVs),
19711986
B.
19721987

1988+
put_kvs(B, KVs) ->
1989+
lists:foldl(fun({K, V}, _) ->
1990+
ok = bitcask:put(B, K, V)
1991+
end, undefined, KVs).
19731992

19741993
default_dataset() ->
19751994
[{<<"k">>, <<"v">>},
@@ -2029,6 +2048,44 @@ list_data_files_test2() ->
20292048
%% Now use the list_data_files to scan the dir
20302049
ExpFiles = list_data_files("/tmp/bc.test.list", undefined, undefined).
20312050

2051+
% Test that readable_files will not return the currently active
2052+
% write or merge file by mistake if they change in between fetching them
2053+
% and listing the files in the directory.
2054+
list_data_files_race_test() ->
2055+
Dir = "/tmp/bc.test.list.race",
2056+
Fname = fun(N) ->
2057+
filename:join(Dir, integer_to_list(N) ++ ".bitcask.data")
2058+
end,
2059+
WriteFile = fun(N) ->
2060+
ok = file:write_file(Fname(N), <<>>)
2061+
end,
2062+
WriteFiles = fun(S,E) ->
2063+
[WriteFile(N) || N <- lists:seq(S, E)]
2064+
end,
2065+
os:cmd("rm -rf " ++ Dir ++ "; mkdir -p " ++ Dir),
2066+
WriteFiles(1,5),
2067+
% Faking 4 as merge file, 5 as write file,
2068+
% then switching to 6 as merge, 7 as write
2069+
KindN = fun(merge) -> 4; (write) -> 5 end,
2070+
meck:new(bitcask_lockops, [passthrough]),
2071+
meck:expect(bitcask_lockops, read_activefile,
2072+
fun(Kind, _) ->
2073+
case get({fake_activefile, Kind}) of
2074+
undefined ->
2075+
N = KindN(Kind),
2076+
% Next time return file + 2
2077+
WriteFile(N+2),
2078+
put({fake_activefile, Kind}, Fname(N+2)),
2079+
Fname(N);
2080+
File ->
2081+
File
2082+
end
2083+
end),
2084+
ReadFiles = lists:usort(bitcask:readable_files(Dir)),
2085+
meck:unload(),
2086+
?assertEqual([Fname(N)||N<-lists:seq(1,5)],
2087+
ReadFiles).
2088+
20322089
fold_test_() ->
20332090
{timeout, 60, fun fold_test2/0}.
20342091

@@ -2865,6 +2922,25 @@ corrupt_file(Path, Offset, Data) ->
28652922
ok = file:write(FH, Data),
28662923
file:close(FH).
28672924

2925+
% Verify that if the cached efile port goes away, we can recover
2926+
% and not get stuck opening casks
2927+
efile_error_test() ->
2928+
Dir = "/tmp/bc.efile.error",
2929+
B = bitcask:open(Dir, [read_write]),
2930+
ok = bitcask:put(B, <<"k">>, <<"v">>),
2931+
ok = bitcask:close(B),
2932+
Port = get(bitcask_efile_port),
2933+
% If this fails, we stopped using the efile port trick to list
2934+
% dir contents, so remove this test
2935+
?assert(is_port(Port)),
2936+
true = erlang:port_close(Port),
2937+
case bitcask:open(Dir) of
2938+
{error, _} = Err ->
2939+
?assertEqual(ok, Err);
2940+
B2 when is_reference(B2) ->
2941+
ok = bitcask:close(B2)
2942+
end.
2943+
28682944
%% About leak_t0():
28692945
%%
28702946
%% If bitcask leaks file descriptors for the 'touch'ed files, output is:
@@ -3210,6 +3286,26 @@ update_tstamp_stats_test2() ->
32103286
bitcask_time:test__clear_fudge()
32113287
end.
32123288

3289+
scan_err_test_() ->
3290+
{setup,
3291+
fun() ->
3292+
meck:new(bitcask_fileops, [passthrough]),
3293+
ok
3294+
end,
3295+
fun(_) ->
3296+
meck:unload()
3297+
end,
3298+
[fun() ->
3299+
Dir = "/tmp/bc.scan.err",
3300+
meck:expect(bitcask_fileops, data_file_tstamps,
3301+
fun(_) -> {error, because} end),
3302+
?assertMatch({error, _}, bitcask:open(Dir)),
3303+
meck:unload(bitcask_fileops),
3304+
B = bitcask:open(Dir),
3305+
?assertMatch({true, B}, {is_reference(B), B}),
3306+
ok = bitcask:close(B)
3307+
end]}.
3308+
32133309
total_byte_stats_test_() ->
32143310
{timeout, 60, fun total_byte_stats_test2/0}.
32153311

@@ -3281,6 +3377,31 @@ merge_batch_test2() ->
32813377
bitcask:close(B)
32823378
end.
32833379

3380+
merge_expired_test_() ->
3381+
{timeout, 120, fun merge_expired_test2/0}.
3382+
3383+
merge_expired_test2() ->
3384+
Dir = "/tmp/bc.merge.expired.files",
3385+
NKeys = 10,
3386+
KF = fun(N) -> <<N:8/integer>> end,
3387+
KVGen = fun(S, E) ->
3388+
[{KF(N), <<"v">>} || N <- lists:seq(S, E)]
3389+
end,
3390+
DataSet = KVGen(1, 3),
3391+
B = init_dataset(Dir, [{max_file_size, 1}], DataSet),
3392+
ok = bitcask:delete(B, KF(1)),
3393+
put_kvs(B, KVGen(4, NKeys)),
3394+
% Merge away the first 4 files as if they were completely expired,
3395+
FirstFiles = [Dir ++ "/" ++ integer_to_list(N) ++ ".bitcask.data" ||
3396+
N <- lists:seq(1, 4)],
3397+
?assertEqual(ok, bitcask:merge(Dir, [], {FirstFiles, FirstFiles})),
3398+
ExpectedKeys = [KF(N) || N <- lists:seq(4, NKeys)],
3399+
ActualKeys1 = lists:sort(bitcask:list_keys(B)),
3400+
ActualKeys2 = lists:sort(bitcask:fold(B, fun(K,_V,A)->[K|A] end, [])),
3401+
bitcask:close(B),
3402+
?assertEqual(ExpectedKeys, ActualKeys1),
3403+
?assertEqual(ExpectedKeys, ActualKeys2).
3404+
32843405
max_merge_size_test_() ->
32853406
{timeout, 120, fun max_merge_size_test2/0}.
32863407

@@ -3361,6 +3482,31 @@ legacy_tombstones_test2() ->
33613482
bitcask:close(B),
33623483
?assertEqual([Last], AllFiles3).
33633484

3485+
update_tombstones_test() ->
3486+
Dir = "/tmp/bc.update.tombstones",
3487+
Key = <<"k">>,
3488+
Data = [{Key, integer_to_binary(N)} || N <- lists:seq(1, 10)],
3489+
B = init_dataset(Dir, [read_write, {max_file_size, 50000000}], Data),
3490+
ok = bitcask:close(B),
3491+
% Re-open to guarantee opening a second file.
3492+
% An update on the new file requires a tombstone.
3493+
B2 = bitcask:open(Dir, [read_write, {max_file_size, 50000000}]),
3494+
ok = bitcask:put(B2, Key, <<"last_val">>),
3495+
ok = bitcask:close(B2),
3496+
Files = bitcask:readable_files(Dir),
3497+
Fds = [begin
3498+
{ok, Fd} = bitcask_fileops:open_file(File),
3499+
Fd
3500+
end || File <- Files],
3501+
CountF = fun(_K, V, _Tstamp, _, Acc) ->
3502+
case bitcask:is_tombstone(V) of
3503+
true -> Acc + 1;
3504+
false -> Acc
3505+
end
3506+
end,
3507+
TombCount = bitcask:subfold(CountF, Fds, 0),
3508+
?assertEqual(1, TombCount).
3509+
33643510
make_merge_file(Dir, Seed, Probability) ->
33653511
random:seed(Seed),
33663512
case filelib:is_dir(Dir) of

src/bitcask_fileops.erl

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@
6060
-include_lib("eqc/include/eqc_fsm.hrl").
6161
-endif.
6262
-compile(export_all).
63-
-endif.
6463
-include_lib("eunit/include/eunit.hrl").
64+
-endif.
6565

6666
%% @doc Open a new file for writing.
6767
%% Called on a Dirname, will open a fresh file in that directory.
@@ -854,9 +854,20 @@ ensure_dir(F) ->
854854
end
855855
end.
856856

857-
list_dir(Directory) ->
857+
list_dir(Dir) ->
858+
list_dir(Dir, 1).
859+
860+
list_dir(_, 0) ->
861+
{error, efile_driver_unavailable};
862+
list_dir(Directory, Retries) when is_integer(Retries), Retries > 0 ->
858863
Port = get_efile_port(),
859-
prim_file:list_dir(Port, Directory).
864+
case prim_file:list_dir(Port, Directory) of
865+
{error, einval} ->
866+
clear_efile_port(),
867+
list_dir(Directory, Retries-1);
868+
Result ->
869+
Result
870+
end.
860871

861872
get_efile_port() ->
862873
Key = bitcask_efile_port,
@@ -875,6 +886,9 @@ get_efile_port() ->
875886
Port
876887
end.
877888

889+
clear_efile_port() ->
890+
erase(bitcask_efile_port).
891+
878892
prim_file_drv_open(Driver, Portopts) ->
879893
try erlang:open_port({spawn, Driver}, Portopts) of
880894
Port ->

0 commit comments

Comments
 (0)