machi/src/machi_util.erl
2015-04-01 16:14:24 +09:00

1102 lines
41 KiB
Erlang

%% -------------------------------------------------------------------
%%
%% Copyright (c) 2007-2015 Basho Technologies, Inc. All Rights Reserved.
%%
%% 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(machi_util).
-export([
checksum/1,
hexstr_to_bin/1, bin_to_hexstr/1,
hexstr_to_int/1, int_to_hexstr/2, int_to_hexbin/2,
make_binary/1,
make_regname/1,
make_checksum_filename/2, make_data_filename/2,
read_max_filenum/2, increment_max_filenum/2,
info_msg/2, verb/1, verb/2,
%% TCP protocol helpers
connect/2
]).
-compile(export_all).
-include("machi.hrl").
-include("machi_projection.hrl").
-include_lib("kernel/include/file.hrl").
append(Server, Prefix, Chunk) when is_binary(Prefix), is_binary(Chunk) ->
CSum = checksum(Chunk),
Server ! {seq_append, self(), Prefix, Chunk, CSum},
receive
{assignment, Offset, File} ->
{Offset, File}
after 10*1000 ->
bummer
end.
make_regname(Prefix) when is_binary(Prefix) ->
erlang:binary_to_atom(Prefix, latin1);
make_regname(Prefix) when is_list(Prefix) ->
erlang:list_to_atom(Prefix).
make_config_filename(DataDir, Prefix) ->
lists:flatten(io_lib:format("~s/config/~s", [DataDir, Prefix])).
make_checksum_filename(DataDir, Prefix, SequencerName, FileNum) ->
lists:flatten(io_lib:format("~s/config/~s.~s.~w.csum",
[DataDir, Prefix, SequencerName, FileNum])).
make_checksum_filename(DataDir, FileName) ->
lists:flatten(io_lib:format("~s/config/~s.csum", [DataDir, FileName])).
make_data_filename(DataDir, File) ->
FullPath = lists:flatten(io_lib:format("~s/~s", [DataDir, File])),
{File, FullPath}.
make_data_filename(DataDir, Prefix, SequencerName, FileNum) ->
File = erlang:iolist_to_binary(io_lib:format("~s.~s.~w",
[Prefix, SequencerName, FileNum])),
FullPath = lists:flatten(io_lib:format("~s/~s", [DataDir, File])),
{File, FullPath}.
read_max_filenum(DataDir, Prefix) ->
case file:read_file_info(make_config_filename(DataDir, Prefix)) of
{error, enoent} ->
0;
{ok, FI} ->
FI#file_info.size
end.
increment_max_filenum(DataDir, Prefix) ->
try
{ok, FH} = file:open(make_config_filename(DataDir, Prefix), [append]),
ok = file:write(FH, "x"),
%% ok = file:sync(FH),
ok = file:close(FH)
catch
error:{badmatch,_}=Error ->
{error, Error, erlang:get_stacktrace()}
end.
hexstr_to_bin(S) when is_list(S) ->
hexstr_to_bin(S, []);
hexstr_to_bin(B) when is_binary(B) ->
hexstr_to_bin(binary_to_list(B), []).
hexstr_to_bin([], Acc) ->
list_to_binary(lists:reverse(Acc));
hexstr_to_bin([X,Y|T], Acc) ->
{ok, [V], []} = io_lib:fread("~16u", [X,Y]),
hexstr_to_bin(T, [V | Acc]).
bin_to_hexstr(<<>>) ->
[];
bin_to_hexstr(<<X:4, Y:4, Rest/binary>>) ->
[hex_digit(X), hex_digit(Y)|bin_to_hexstr(Rest)].
hex_digit(X) when X < 10 ->
X + $0;
hex_digit(X) ->
X - 10 + $a.
make_binary(X) when is_binary(X) ->
X;
make_binary(X) when is_list(X) ->
iolist_to_binary(X).
hexstr_to_int(X) ->
B = hexstr_to_bin(X),
B_size = byte_size(B) * 8,
<<I:B_size/big>> = B,
I.
int_to_hexstr(I, I_size) ->
bin_to_hexstr(<<I:I_size/big>>).
int_to_hexbin(I, I_size) ->
list_to_binary(int_to_hexstr(I, I_size)).
%%%%%%%%%%%%%%%%%
%%% escript stuff
main2(["1file-write-redundant-client"]) ->
io:format("Use: Write a local file to a series of servers.\n"),
io:format("Args: BlockSize Prefix LocalFilePath [silent] [Host Port [Host Port ...]]\n"),
erlang:halt(1);
main2(["1file-write-redundant-client", BlockSizeStr, PrefixStr, LocalFile|HPs0]) ->
BlockSize = list_to_integer(BlockSizeStr),
Prefix = list_to_binary(PrefixStr),
{Out, HPs} = case HPs0 of
["silent"|Rest] -> {silent, Rest};
_ -> {not_silent, HPs0}
end,
Res = escript_upload_redundant(HPs, BlockSize, Prefix, LocalFile),
if Out /= silent ->
print_upload_details(user, Res);
true ->
ok
end,
Res;
main2(["chunk-read-client"]) ->
io:format("Use: Read a series of chunks for a single server.\n"),
io:format("Args: Host Port LocalChunkDescriptionPath [OutputPath|'console']\n"),
erlang:halt(1);
main2(["chunk-read-client", Host, PortStr, ChunkFileList]) ->
main2(["chunk-read-client", Host, PortStr, ChunkFileList, "console"]);
main2(["chunk-read-client", Host, PortStr, ChunkFileList, OutputPath]) ->
FH = open_output_file(OutputPath),
OutFun = make_outfun(FH),
try
main2(["chunk-read-client2", Host, PortStr, ChunkFileList, OutFun])
after
(catch file:close(FH))
end;
main2(["chunk-read-client2", Host, PortStr, ChunkFileList, ProcFun]) ->
Sock = escript_connect(Host, PortStr),
escript_download_chunks(Sock, ChunkFileList, ProcFun);
main2(["delete-client"]) ->
io:format("Use: Delete a file (NOT FOR GENERAL USE)\n"),
io:format("Args: Host Port File\n"),
erlang:halt(1);
main2(["delete-client", Host, PortStr, File]) ->
Sock = escript_connect(Host, PortStr),
escript_delete(Sock, File);
%%%% cc flavors %%%%
main2(["cc-1file-write-redundant-client"]) ->
io:format("Use: Write a local file to a chain via projection.\n"),
io:format("Args: BlockSize Prefix LocalFilePath ProjectionPath\n"),
erlang:halt(1);
main2(["cc-1file-write-redundant-client", BlockSizeStr, PrefixStr, LocalFile, ProjectionPath]) ->
BlockSize = list_to_integer(BlockSizeStr),
Prefix = list_to_binary(PrefixStr),
{_Chain, RawHPs} = calc_chain(write, ProjectionPath, PrefixStr),
HPs = convert_raw_hps(RawHPs),
Res = escript_upload_redundant(HPs, BlockSize, Prefix, LocalFile),
print_upload_details(user, Res),
Res;
main2(["cc-chunk-read-client"]) ->
io:format("Use: Read a series of chunks from a chain via projection.\n"),
io:format("Args: ProjectionPath ChunkFileList [OutputPath|'console' \\\n\t[ErrorCorrection_ProjectionPath]]\n"),
erlang:halt(1);
main2(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList]) ->
main3(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList,"console",
undefined]);
main2(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList, OutputPath]) ->
main3(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList, OutputPath,
undefined]);
main2(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList, OutputPath,
EC_ProjectionPath]) ->
main3(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList, OutputPath,
EC_ProjectionPath]).
main3(["cc-chunk-read-client",
ProjectionPathOrDir, ChunkFileList, OutputPath, EC_ProjectionPath]) ->
P = read_projection_file(ProjectionPathOrDir),
ChainMap = read_chain_map_file(ProjectionPathOrDir),
FH = open_output_file(OutputPath),
ProcFun = make_outfun(FH),
Res = try
escript_cc_download_chunks(ChunkFileList, P, ChainMap, ProcFun,
EC_ProjectionPath)
after
(catch file:close(FH))
end,
Res.
-spec connect(inet:ip_address() | inet:hostname(), inet:port_number()) ->
port().
connect(Host, Port) ->
escript_connect(Host, Port).
escript_connect(Host, PortStr) when is_list(PortStr) ->
Port = list_to_integer(PortStr),
escript_connect(Host, Port);
escript_connect(Host, Port) when is_integer(Port) ->
{ok, Sock} = gen_tcp:connect(Host, Port, [{active,false}, {mode,binary},
{packet, raw}]),
Sock.
escript_upload_file(Sock, BlockSize, Prefix, File) ->
{ok, FH} = file:open(File, [read, raw, binary]),
try
escript_upload_file2(file:read(FH, BlockSize), FH,
BlockSize, Prefix, Sock, [])
after
file:close(FH)
end.
escript_upload_file2({ok, Chunk}, FH, BlockSize, Prefix, Sock, Acc) ->
{OffsetHex, LenHex, File} = upload_chunk_append(Sock, Prefix, Chunk),
verb("~s ~s ~s\n", [OffsetHex, LenHex, File]),
<<Offset:64/big>> = hexstr_to_bin(OffsetHex),
<<Size:32/big>> = hexstr_to_bin(LenHex),
OSF = {Offset, Size, File},
escript_upload_file2(file:read(FH, BlockSize), FH, BlockSize, Prefix, Sock,
[OSF|Acc]);
escript_upload_file2(eof, _FH, _BlockSize, _Prefix, _Sock, Acc) ->
lists:reverse(Acc).
upload_chunk_append(Sock, Prefix, Chunk) ->
%% _ = crypto:hash(md5, Chunk),
Len = byte_size(Chunk),
LenHex = list_to_binary(bin_to_hexstr(<<Len:32/big>>)),
Cmd = <<"A ", LenHex/binary, " ", Prefix/binary, "\n">>,
ok = gen_tcp:send(Sock, [Cmd, Chunk]),
{ok, Line} = gen_tcp:recv(Sock, 0),
PathLen = byte_size(Line) - 3 - 16 - 1 - 1,
<<"OK ", OffsetHex:16/binary, " ", Path:PathLen/binary, _:1/binary>> = Line,
{OffsetHex, LenHex, Path}.
upload_chunk_write(Sock, Offset, File, Chunk) when is_integer(Offset) ->
OffsetHex = list_to_binary(bin_to_hexstr(<<Offset:64/big>>)),
upload_chunk_write(Sock, OffsetHex, File, Chunk);
upload_chunk_write(Sock, OffsetHex, File, Chunk) when is_binary(OffsetHex) ->
%% _ = crypto:hash(md5, Chunk),
Len = byte_size(Chunk),
LenHex = list_to_binary(bin_to_hexstr(<<Len:32/big>>)),
Cmd = <<"W-repl ", OffsetHex/binary, " ",
LenHex/binary, " ", File/binary, "\n">>,
ok = gen_tcp:send(Sock, [Cmd, Chunk]),
{ok, Line} = gen_tcp:recv(Sock, 0),
<<"OK\n">> = Line,
{OffsetHex, LenHex, File}.
escript_upload_redundant([Host, PortStr|HPs], BlockSize, Prefix, LocalFile) ->
Sock = escript_connect(Host, PortStr),
ok = inet:setopts(Sock, [{packet, line}]),
OSFs = try
escript_upload_file(Sock, BlockSize, Prefix, LocalFile)
after
gen_tcp:close(Sock)
end,
escript_upload_redundant2(HPs, OSFs, LocalFile, OSFs).
escript_upload_redundant2([], _OSFs, _LocalFile, OSFs) ->
OSFs;
escript_upload_redundant2([Host, PortStr|HPs], OSFs, LocalFile, OSFs) ->
Sock = escript_connect(Host, PortStr),
{ok, FH} = file:open(LocalFile, [read, binary, raw]),
try
[begin
{ok, Chunk} = file:read(FH, Size),
_OSF2 = upload_chunk_write(Sock, Offset, File, Chunk)
%% verb("~p: ~p\n", [{Host, PortStr}, OSF2])
end || {Offset, Size, File} <- OSFs]
after
gen_tcp:close(Sock),
file:close(FH)
end,
escript_upload_redundant2(HPs, OSFs, LocalFile, OSFs).
escript_download_chunks(Sock, {{{ChunkLine}}}, ProcFun) ->
escript_download_chunk({ok, ChunkLine}, invalid_fd, Sock, ProcFun);
escript_download_chunks(Sock, ChunkFileList, ProcFun) ->
{ok, FH} = file:open(ChunkFileList, [read, raw, binary]),
escript_download_chunk(file:read_line(FH), FH, Sock, ProcFun).
escript_download_chunk({ok, Line}, FH, Sock, ProcFun) ->
ChunkOrError = escript_cc_download_chunk2(Sock, Line),
ProcFun(ChunkOrError),
[ChunkOrError|
escript_download_chunk((catch file:read_line(FH)), FH, Sock, ProcFun)];
escript_download_chunk(eof, _FH, _Sock, ProcFun) ->
ProcFun(eof),
[];
escript_download_chunk(_Else, _FH, _Sock, ProcFun) ->
ProcFun(eof),
[].
escript_cc_download_chunks({{{ChunkLine}}}, P, ChainMap, ProcFun,
EC_ProjectionPath) ->
escript_cc_download_chunk({ok,ChunkLine}, invalid_fd, P, ChainMap, ProcFun,
EC_ProjectionPath);
escript_cc_download_chunks(ChunkFileList, P, ChainMap, ProcFun,
EC_ProjectionPath) ->
{ok, FH} = file:open(ChunkFileList, [read, raw, binary]),
escript_cc_download_chunk(file:read_line(FH), FH, P, ChainMap, ProcFun,
EC_ProjectionPath).
escript_cc_download_chunk({ok, Line}, FH, P, ChainMap, ProcFun,
EC_ProjectionPath) ->
RestLen = byte_size(Line) - 16 - 1 - 8 - 1 - 1,
<<_Offset:16/binary, " ", _Len:8/binary, " ", Rest:RestLen/binary, "\n">>
= Line,
Prefix = re:replace(Rest, "\\..*", "", [{return, binary}]),
{_Chains, RawHPs} = calc_chain(read, P, ChainMap, Prefix),
Chunk = lists:foldl(
fun(_RawHP, Bin) when is_binary(Bin) -> Bin;
(RawHP, _) ->
[Host, PortStr] = convert_raw_hps([RawHP]),
Sock = get_cached_sock(Host, PortStr),
case escript_cc_download_chunk2(Sock, Line) of
Bin when is_binary(Bin) ->
Bin;
{error, _} = Error ->
Error;
{erasure_encoded, _} = EC_info ->
escript_cc_download_ec_chunk(EC_info,
EC_ProjectionPath)
end
end, undefined, RawHPs),
ProcFun(Chunk),
[Chunk|escript_cc_download_chunk((catch file:read_line(FH)),
FH, P, ChainMap, ProcFun,
EC_ProjectionPath)];
escript_cc_download_chunk(eof, _FH, _P, _ChainMap, ProcFun,
_EC_ProjectionPath) ->
ProcFun(eof),
[];
escript_cc_download_chunk(Else, _FH, _P, _ChainMap, ProcFun,
_EC_ProjectionPath) ->
ProcFun(Else),
[].
escript_cc_download_chunk2(Sock, Line) ->
%% Line includes an LF, so we can be lazy.
CmdLF = [<<"R ">>, Line],
ok = gen_tcp:send(Sock, CmdLF),
case gen_tcp:recv(Sock, 3) of
{ok, <<"OK\n">>} ->
{_Offset, Size, _File} = read_hex_size(Line),
{ok, Chunk} = gen_tcp:recv(Sock, Size),
Chunk;
{ok, Else} ->
{ok, OldOpts} = inet:getopts(Sock, [packet]),
ok = inet:setopts(Sock, [{packet, line}]),
{ok, Else2} = gen_tcp:recv(Sock, 0),
ok = inet:setopts(Sock, OldOpts),
case Else of
<<"ERA">> ->
escript_cc_parse_ec_info(Sock, Line, Else2);
_ ->
{error, {Line, <<Else/binary, Else2/binary>>}}
end
end.
escript_cc_parse_ec_info(Sock, Line, Else2) ->
ChompLine = chomp(Line),
{Offset, Size, File} = read_hex_size(ChompLine),
<<"SURE ", BodyLenHex:4/binary, " ", StripeWidthHex:16/binary, " ",
OrigFileLenHex:16/binary, " rs_10_4_v1", _/binary>> = Else2,
<<BodyLen:16/big>> = hexstr_to_bin(BodyLenHex),
{ok, SummaryBody} = gen_tcp:recv(Sock, BodyLen),
<<StripeWidth:64/big>> = hexstr_to_bin(StripeWidthHex),
<<OrigFileLen:64/big>> = hexstr_to_bin(OrigFileLenHex),
NewFileNum = (Offset div StripeWidth) + 1,
NewOffset = Offset rem StripeWidth,
if Offset + Size > OrigFileLen ->
%% Client's request is larger than original file size, derp
{error, bad_offset_and_size};
NewOffset + Size > StripeWidth ->
%% Client's request straddles a stripe boundary, TODO fix me
{error, todo_TODO_implement_this_with_two_reads_and_then_glue_together};
true ->
NewOffsetHex = bin_to_hexstr(<<NewOffset:64/big>>),
LenHex = bin_to_hexstr(<<Size:32/big>>),
NewSuffix = file_suffix_rs_10_4_v1(NewFileNum),
NewFile = iolist_to_binary([File, NewSuffix]),
NewLine = iolist_to_binary([NewOffsetHex, " ", LenHex, " ",
NewFile, "\n"]),
{erasure_encoded, {Offset, Size, File, NewOffset, NewFile,
NewFileNum, NewLine, SummaryBody}}
end.
%% TODO: The EC method/version/type stuff here is loosey-goosey
escript_cc_download_ec_chunk(EC_info, undefined) ->
EC_info;
escript_cc_download_ec_chunk({erasure_encoded,
{_Offset, _Size, _File, _NewOffset, NewFile,
NewFileNum, NewLine, SummaryBody}},
EC_ProjectionPath) ->
{P, ChainMap} = get_cached_projection(EC_ProjectionPath),
%% Remember: we use the whole file name for hashing, not the prefix
{_Chains, RawHPs} = calc_chain(read, P, ChainMap, NewFile),
RawHP = lists:nth(NewFileNum, RawHPs),
[Host, PortStr] = convert_raw_hps([RawHP]),
Sock = get_cached_sock(Host, PortStr),
case escript_cc_download_chunk2(Sock, NewLine) of
Chunk when is_binary(Chunk) ->
Chunk;
{error, _} = Else ->
io:format("TODO: EC chunk get failed:\n\t~s\n", [NewLine]),
io:format("Use this info to reconstruct:\n\t~p\n\n", [SummaryBody]),
Else
end.
get_cached_projection(EC_ProjectionPath) ->
case get(cached_projection) of
undefined ->
P = read_projection_file(EC_ProjectionPath),
ChainMap = read_chain_map_file(EC_ProjectionPath),
put(cached_projection, {P, ChainMap}),
get_cached_projection(EC_ProjectionPath);
Stuff ->
Stuff
end.
file_suffix_rs_10_4_v1(1) -> <<"_k01">>;
file_suffix_rs_10_4_v1(2) -> <<"_k02">>;
file_suffix_rs_10_4_v1(3) -> <<"_k03">>;
file_suffix_rs_10_4_v1(4) -> <<"_k04">>;
file_suffix_rs_10_4_v1(5) -> <<"_k05">>;
file_suffix_rs_10_4_v1(6) -> <<"_k06">>;
file_suffix_rs_10_4_v1(7) -> <<"_k07">>;
file_suffix_rs_10_4_v1(8) -> <<"_k08">>;
file_suffix_rs_10_4_v1(9) -> <<"_k09">>;
file_suffix_rs_10_4_v1(10) -> <<"_k10">>.
escript_delete(Sock, File) ->
ok = gen_tcp:send(Sock, [<<"DEL-migration ">>, File, <<"\n">>]),
ok = inet:setopts(Sock, [{packet, line}]),
case gen_tcp:recv(Sock, 0) of
{ok, <<"OK\n">>} ->
ok;
{ok, <<"ERROR", _/binary>>} ->
error
end.
escript_compare_servers(Sock1, Sock2, H1, H2, Args) ->
FileFilterFun = fun(_) -> true end,
escript_compare_servers(Sock1, Sock2, H1, H2, FileFilterFun, Args).
escript_compare_servers(Sock1, Sock2, H1, H2, FileFilterFun, Args) ->
All = [H1, H2],
put(mydict, dict:new()),
Fetch1 = make_fetcher(H1),
Fetch2 = make_fetcher(H2),
Fmt = case Args of
[] ->
fun(eof) -> ok; (Str) -> io:format(user, Str, []) end;
[null] ->
fun(_) -> ok end;
[OutFile] ->
{ok, FH} = file:open(OutFile, [write]),
fun(eof) -> file:close(FH);
(Str) -> file:write(FH, Str)
end
end,
%% TODO: Broken! Fetch1 and Fetch2 aren't created when comments are below
Sock1=Sock1,Sock2=Sock2,Fetch1=Fetch1,Fetch2=Fetch2, % shut up compiler
%% _X1 = escript_list2(Sock1, Fetch1),
%% _X2 = escript_list2(Sock2, Fetch2),
FoldRes = lists:sort(dict:to_list(get(mydict))),
Fmt("{legend, {file, list_of_servers_without_file}}.\n"),
Fmt(io_lib:format("{all, ~p}.\n", [All])),
Res = [begin
{GotIt, Sizes} = lists:unzip(GotSizes),
Size = lists:max(Sizes),
Missing = {File, {Size, All -- GotIt}},
verb("~p.\n", [Missing]),
Missing
end || {File, GotSizes} <- FoldRes, FileFilterFun(File)],
(catch Fmt(eof)),
Res.
make_fetcher(Host) ->
fun(eof) ->
ok;
(<<SizeHex:16/binary, " ", Rest/binary>>) ->
<<Size:64/big>> = hexstr_to_bin(SizeHex),
FileLen = byte_size(Rest) - 1,
<<File:FileLen/binary, _/binary>> = Rest,
NewDict = dict:append(File, {Host, Size}, get(mydict)),
put(mydict, NewDict)
end.
checksum(Bin) when is_binary(Bin) ->
crypto:hash(md5, Bin).
verb(Fmt) ->
verb(Fmt, []).
verb(Fmt, Args) ->
case application:get_env(kernel, verbose) of
{ok, true} -> io:format(Fmt, Args);
_ -> ok
end.
info_msg(Fmt, Args) ->
case application:get_env(kernel, verbose) of {ok, false} -> ok;
_ -> error_logger:info_msg(Fmt, Args)
end.
repair(File, Size, [], Mode, V, SrcS, SrcS2, DstS, DstS2, _Src) ->
verb("~s: present on both: ", [File]),
repair_both_present(File, Size, Mode, V, SrcS, SrcS2, DstS, DstS2);
repair(File, Size, MissingList, Mode, V, SrcS, SrcS2, DstS, _DstS2, Src) ->
case lists:member(Src, MissingList) of
true ->
verb("~s -> ~p, skipping: not on source server\n", [File, MissingList]);
false when Mode == check ->
verb("~s -> ~p, copy ~s MB (skipped)\n", [File, MissingList, mbytes(Size)]);
false ->
verb("~s -> ~p, copy ~s MB ", [File, MissingList, mbytes(Size)]),
ok = copy_file(File, SrcS, SrcS2, DstS, V),
verb("done\n", [])
end.
copy_file(File, SrcS, SrcS2, DstS, Verbose) ->
%% Use the *second* source socket to copy each chunk.
ProcChecksum = copy_file_proc_checksum_fun(File, SrcS2, DstS, Verbose),
%% Use the *first source socket to enumerate the chunks & checksums.
exit(todo_broken),
machi_flu1_client:checksum_list(SrcS, File, line_by_line, ProcChecksum).
copy_file_proc_checksum_fun(File, SrcS, DstS, _Verbose) ->
fun(<<OffsetHex:16/binary, " ", LenHex:8/binary, " ",
CSumHex:32/binary, "\n">>) ->
<<Len:32/big>> = hexstr_to_bin(LenHex),
DownloadChunkBin = <<OffsetHex/binary, " ", LenHex/binary, " ",
File/binary, "\n">>,
[Chunk] = escript_download_chunks(SrcS, {{{DownloadChunkBin}}},
fun(_) -> ok end),
CSum = hexstr_to_bin(CSumHex),
CSum2 = checksum(Chunk),
if Len == byte_size(Chunk), CSum == CSum2 ->
{_,_,_} = upload_chunk_write(DstS, OffsetHex, File, Chunk),
ok;
true ->
io:format("ERROR: ~s ~s ~s csum/size error\n",
[File, OffsetHex, LenHex]),
error
end;
(_Else) ->
ok
end.
repair_both_present(File, Size, Mode, V, SrcS, _SrcS2, DstS, _DstS2) ->
Tmp1 = lists:flatten(io_lib:format("/tmp/sort.1.~w.~w.~w", tuple_to_list(now()))),
Tmp2 = lists:flatten(io_lib:format("/tmp/sort.2.~w.~w.~w", tuple_to_list(now()))),
J_Both = lists:flatten(io_lib:format("/tmp/join.3-both.~w.~w.~w", tuple_to_list(now()))),
J_SrcOnly = lists:flatten(io_lib:format("/tmp/join.4-src-only.~w.~w.~w", tuple_to_list(now()))),
J_DstOnly = lists:flatten(io_lib:format("/tmp/join.5-dst-only.~w.~w.~w", tuple_to_list(now()))),
S_Identical = lists:flatten(io_lib:format("/tmp/join.6-sort-identical.~w.~w.~w", tuple_to_list(now()))),
{ok, FH1} = file:open(Tmp1, [write, raw, binary]),
{ok, FH2} = file:open(Tmp2, [write, raw, binary]),
try
K = md5_ctx,
MD5_it = fun(Bin) ->
{FH, MD5ctx1} = get(K),
file:write(FH, Bin),
MD5ctx2 = crypto:hash_update(MD5ctx1, Bin),
put(K, {FH, MD5ctx2})
end,
put(K, {FH1, crypto:hash_init(md5)}),
exit(todo_broken),
ok = machi_flu1_client:checksum_list(SrcS, File, fast, MD5_it),
{_, MD5_1} = get(K),
SrcMD5 = crypto:hash_final(MD5_1),
put(K, {FH2, crypto:hash_init(md5)}),
exit(todo_broken),
ok = machi_flu1_client:checksum_list(DstS, File, fast, MD5_it),
{_, MD5_2} = get(K),
DstMD5 = crypto:hash_final(MD5_2),
if SrcMD5 == DstMD5 ->
verb("identical\n", []);
true ->
ok = file:close(FH1),
ok = file:close(FH2),
_Q1 = os:cmd("./REPAIR-SORT-JOIN.sh " ++ Tmp1 ++ " " ++ Tmp2 ++ " " ++ J_Both ++ " " ++ J_SrcOnly ++ " " ++ J_DstOnly ++ " " ++ S_Identical),
case file:read_file_info(S_Identical) of
{ok, _} ->
verb("identical (secondary sort)\n", []);
{error, enoent} ->
io:format("differences found:"),
repair_both(File, Size, V, Mode,
J_Both, J_SrcOnly, J_DstOnly,
SrcS, DstS)
end
end
after
catch file:close(FH1),
catch file:close(FH2),
[(catch file:delete(FF)) || FF <- [Tmp1,Tmp2,J_Both,J_SrcOnly,J_DstOnly,
S_Identical]]
end.
repair_both(File, _Size, V, Mode, J_Both, J_SrcOnly, J_DstOnly, SrcS, DstS) ->
AccFun = if Mode == check ->
fun(_X, List) -> List end;
Mode == repair ->
fun( X, List) -> [X|List] end
end,
BothFun = fun(<<_OffsetSrcHex:16/binary, " ",
LenSrcHex:8/binary, " ", CSumSrcHex:32/binary, " ",
LenDstHex:8/binary, " ", CSumDstHex:32/binary, "\n">> =Line,
{SameB, SameC, DiffB, DiffC, Ds}) ->
<<Len:32/big>> = hexstr_to_bin(LenSrcHex),
if LenSrcHex == LenDstHex,
CSumSrcHex == CSumDstHex ->
{SameB + Len, SameC + 1, DiffB, DiffC, Ds};
true ->
%% D = {OffsetSrcHex, LenSrcHex, ........
{SameB, SameC, DiffB + Len, DiffC + 1,
AccFun(Line, Ds)}
end;
(_Else, Acc) ->
Acc
end,
OnlyFun = fun(<<_OffsetSrcHex:16/binary, " ", LenSrcHex:8/binary, " ",
_CSumHex:32/binary, "\n">> = Line,
{DiffB, DiffC, Ds}) ->
<<Len:32/big>> = hexstr_to_bin(LenSrcHex),
{DiffB + Len, DiffC + 1, AccFun(Line, Ds)};
(_Else, Acc) ->
Acc
end,
{SameBx, SameCx, DiffBy, DiffCy, BothDiffs} =
file_folder(BothFun, {0,0,0,0,[]}, J_Both),
{DiffB_src, DiffC_src, Ds_src} = file_folder(OnlyFun, {0,0,[]}, J_SrcOnly),
{DiffB_dst, DiffC_dst, Ds_dst} = file_folder(OnlyFun, {0,0,[]}, J_DstOnly),
if Mode == check orelse V == true ->
io:format("\n\t"),
io:format("BothR ~p, ", [{SameBx, SameCx, DiffBy, DiffCy}]),
io:format("SrcR ~p, ", [{DiffB_src, DiffC_src}]),
io:format("DstR ~p", [{DiffB_dst, DiffC_dst}]),
io:format("\n");
true -> ok
end,
if Mode == repair ->
ok = repair_both_both(File, V, BothDiffs, SrcS, DstS),
ok = repair_copy_chunks(File, V, Ds_src, DiffB_src, DiffC_src,
SrcS, DstS),
ok = repair_copy_chunks(File, V, Ds_dst, DiffB_dst, DiffC_dst,
DstS, SrcS);
true ->
ok
end.
repair_both_both(_File, _V, [_|_], _SrcS, _DstS) ->
%% TODO: fetch both, check checksums, hopefully only exactly one
%% is correct, then use that one to repair the other. And if the
%% sizes are different, hrm, there may be an extra corner case(s)
%% hiding there.
io:format("WHOA! We have differing checksums or sizes here, TODO not implemented, but there's trouble in the little village on the river....\n"),
timer:sleep(3*1000),
ok;
repair_both_both(_File, _V, [], _SrcS, _DstS) ->
ok.
repair_copy_chunks(_File, _V, [], _DiffBytes, _DiffCount, _SrcS, _DstS) ->
ok;
repair_copy_chunks(File, V, ToBeCopied, DiffBytes, DiffCount, SrcS, DstS) ->
verb("\n", []),
verb("Starting copy of ~p chunks/~s MBytes to \n ~s: ",
[DiffCount, mbytes(DiffBytes), File]),
InnerCopyFun = copy_file_proc_checksum_fun(File, SrcS, DstS, V),
FoldFun = fun(Line, ok) ->
ok = InnerCopyFun(Line) % Strong sanity check
end,
ok = lists:foldl(FoldFun, ok, ToBeCopied),
verb(" done\n", []),
ok.
file_folder(Fun, Acc, Path) ->
{ok, FH} = file:open(Path, [read, raw, binary]),
try
file_folder2(Fun, Acc, FH)
after
file:close(FH)
end.
file_folder2(Fun, Acc, FH) ->
file_folder2(file:read_line(FH), Fun, Acc, FH).
file_folder2({ok, Line}, Fun, Acc, FH) ->
Acc2 = Fun(Line, Acc),
file_folder2(Fun, Acc2, FH);
file_folder2(eof, _Fun, Acc, _FH) ->
Acc.
make_repair_props(["check"|T]) ->
[{mode, check}|make_repair_props(T)];
make_repair_props(["repair"|T]) ->
[{mode, repair}|make_repair_props(T)];
make_repair_props(["verbose"|T]) ->
application:set_env(kernel, verbose, true),
[{verbose, true}|make_repair_props(T)];
make_repair_props(["noverbose"|T]) ->
[{verbose, false}|make_repair_props(T)];
make_repair_props(["progress"|T]) ->
[{progress, true}|make_repair_props(T)];
make_repair_props(["delete-source"|T]) ->
[{delete_source, true}|make_repair_props(T)];
make_repair_props(["nodelete-source"|T]) ->
[{delete_source, false}|make_repair_props(T)];
make_repair_props(["nodelete-tmp"|T]) ->
[{delete_tmp, false}|make_repair_props(T)];
make_repair_props([X|T]) ->
io:format("Error: skipping unknown option ~p\n", [X]),
make_repair_props(T);
make_repair_props([]) ->
%% Proplist defaults
[{mode, check}, {delete_source, false}].
mbytes(0) ->
"0.0";
mbytes(Size) ->
lists:flatten(io_lib:format("~.1.0f", [max(0.1, Size / (1024*1024))])).
chomp(Line) when is_binary(Line) ->
LineLen = byte_size(Line) - 1,
<<ChompLine:LineLen/binary, _/binary>> = Line,
ChompLine.
make_outfun(FH) ->
fun({error, _} = Error) ->
file:write(FH, io_lib:format("Error: ~p\n", [Error]));
(eof) ->
ok;
({erasure_encoded, Info} = _Erasure) ->
file:write(FH, "TODO/WIP: erasure_coded:\n"),
file:write(FH, io_lib:format("\t~p\n", [Info]));
(Bytes) when is_binary(Bytes) orelse is_list(Bytes) ->
file:write(FH, Bytes)
end.
open_output_file("console") ->
user;
open_output_file(Path) ->
{ok, FH} = file:open(Path, [write]),
FH.
print_upload_details(_, {error, _} = Res) ->
io:format("Error: ~p\n", [Res]),
erlang:halt(1);
print_upload_details(FH, Res) ->
[io:format(FH, "~s ~s ~s\n", [bin_to_hexstr(<<Offset:64/big>>),
bin_to_hexstr(<<Len:32/big>>),
File]) ||
{Offset, Len, File} <- Res].
%%%%%%%%%%%%%%%%%
read_projection_file("new") ->
#projection{epoch=0, last_epoch=0,
float_map=undefined, last_float_map=undefined};
read_projection_file(Path) ->
case filelib:is_dir(Path) of
true ->
read_projection_file_loop(Path ++ "/current.proj");
false ->
case filelib:is_file(Path) of
true ->
read_projection_file2(Path);
false ->
error({bummer, Path})
end
end.
read_projection_file2(Path) ->
{ok, [P]} = file:consult(Path),
true = is_record(P, projection),
FloatMap = P#projection.float_map,
LastFloatMap = if P#projection.last_float_map == undefined ->
FloatMap;
true ->
P#projection.last_float_map
end,
P#projection{migrating=(FloatMap /= LastFloatMap),
tree=machi_chash:make_tree(FloatMap),
last_tree=machi_chash:make_tree(LastFloatMap)}.
read_projection_file_loop(Path) ->
read_projection_file_loop(Path, 100).
read_projection_file_loop(Path, 0) ->
error({bummer, Path});
read_projection_file_loop(Path, N) ->
try
read_projection_file2(Path)
catch
error:{badmatch,{error,enoent}} ->
timer:sleep(100),
read_projection_file_loop(Path, N-1)
end.
write_projection(P, Path) when is_record(P, projection) ->
{error, enoent} = file:read_file_info(Path),
{ok, FH} = file:open(Path, [write]),
WritingP = P#projection{tree=undefined, last_tree=undefined},
io:format(FH, "~p.\n", [WritingP]),
ok = file:close(FH).
read_weight_map_file(Path) ->
{ok, [Map]} = file:consult(Path),
true = is_list(Map),
true = lists:all(fun({Chain, Weight})
when is_binary(Chain),
is_integer(Weight), Weight >= 0 ->
true;
(_) ->
false
end, Map),
Map.
%% Assume the file "chains.map" in whatever dir that stores projections.
read_chain_map_file(DirPath) ->
L = case filelib:is_dir(DirPath) of
true ->
{ok, Map} = file:consult(DirPath ++ "/chains.map"),
Map;
false ->
Dir = filename:dirname(DirPath),
{ok, Map} = file:consult(Dir ++ "/chains.map"),
Map
end,
orddict:from_list(L).
get_float_map(P) when is_record(P, projection) ->
P#projection.float_map.
get_last_float_map(P) when is_record(P, projection) ->
P#projection.last_float_map.
hash_and_query(Key, P) when is_record(P, projection) ->
<<Int:(20*8)/unsigned>> = crypto:hash(sha, Key),
Float = Int / ?SHA_MAX,
{_, Current} = machi_chash:query_tree(Float, P#projection.tree),
if P#projection.migrating ->
{_, Last} = machi_chash:query_tree(Float, P#projection.last_tree),
if Last == Current ->
[Current];
true ->
[Current, Last, Current]
end;
true ->
[Current]
end.
calc_chain(write=Op, ProjectionPathOrDir, PrefixStr) ->
P = read_projection_file(ProjectionPathOrDir),
ChainMap = read_chain_map_file(ProjectionPathOrDir),
calc_chain(Op, P, ChainMap, PrefixStr);
calc_chain(read=Op, ProjectionPathOrDir, PrefixStr) ->
P = read_projection_file(ProjectionPathOrDir),
ChainMap = read_chain_map_file(ProjectionPathOrDir),
calc_chain(Op, P, ChainMap, PrefixStr).
calc_chain(write=_Op, P, ChainMap, PrefixStr) ->
%% Writes are easy: always use the new location.
[Chain|_] = hash_and_query(PrefixStr, P),
{Chain, orddict:fetch(Chain, ChainMap)};
calc_chain(read=_Op, P, ChainMap, PrefixStr) ->
%% Reads are slightly trickier: reverse each chain so tail is tried first.
Chains = hash_and_query(PrefixStr, P),
{Chains, lists:flatten([lists:reverse(orddict:fetch(Chain, ChainMap)) ||
Chain <- Chains])}.
convert_raw_hps([{HostBin, Port}|T]) ->
[binary_to_list(HostBin), integer_to_list(Port)|convert_raw_hps(T)];
convert_raw_hps([]) ->
[].
get_cached_sock(Host, PortStr) ->
K = {socket_cache, Host, PortStr},
case erlang:get(K) of
undefined ->
Sock = escript_connect(Host, PortStr),
Krev = {socket_cache_rev, Sock},
erlang:put(K, Sock),
erlang:put(Krev, {Host, PortStr}),
Sock;
Sock ->
Sock
end.
invalidate_cached_sock(Sock) ->
(catch gen_tcp:close(Sock)),
Krev = {socket_cache_rev, Sock},
case erlang:get(Krev) of
undefined ->
ok;
{Host, PortStr} ->
K = {socket_cache, Host, PortStr},
erlang:erase(Krev),
erlang:erase(K),
ok
end.
%%%%%%%%%%%%%%%%%
%%% basho_bench callbacks
-define(SEQ, ?MODULE).
-define(DEFAULT_HOSTIP_LIST, [{{127,0,0,1}, 7071}]).
-record(bb, {
host,
port_str,
%% sock,
proj_check_ticker_started=false,
proj_path,
proj,
chain_map
}).
new(1 = Id) ->
%% broken: start_append_server(),
case basho_bench_config:get(file0_start_listener, no) of
no ->
ok;
{_Port, _DataDir} ->
exit(todo_broken)
end,
timer:sleep(100),
new_common(Id);
new(Id) ->
new_common(Id).
new_common(Id) ->
random:seed(now()),
ProjectionPathOrDir =
basho_bench_config:get(file0_projection_path, undefined),
Servers = basho_bench_config:get(file0_ip_list, ?DEFAULT_HOSTIP_LIST),
NumServers = length(Servers),
{Host, Port} = lists:nth((Id rem NumServers) + 1, Servers),
State0 = #bb{host=Host, port_str=integer_to_list(Port),
proj_path=ProjectionPathOrDir},
{ok, read_projection_info(State0)}.
run(null, _KeyGen, _ValueGen, State) ->
{ok, State};
run(keygen_valuegen_then_null, KeyGen, ValueGen, State) ->
_Prefix = KeyGen(),
_Value = ValueGen(),
{ok, State};
run(append_local_server, KeyGen, ValueGen, State) ->
Prefix = KeyGen(),
Value = ValueGen(),
{_, _} = ?SEQ:append(?SEQ, Prefix, Value),
{ok, State};
run(append_remote_server, KeyGen, ValueGen, State) ->
Prefix = KeyGen(),
Value = ValueGen(),
bb_do_write_chunk(Prefix, Value, State#bb.host, State#bb.port_str, State);
run(cc_append_remote_server, KeyGen, ValueGen, State0) ->
State = check_projection_check(State0),
Prefix = KeyGen(),
Value = ValueGen(),
{_Chain, ModHPs} = calc_chain(write, State#bb.proj, State#bb.chain_map,
Prefix),
FoldFun = fun({Host, PortStr}, Acc) ->
case bb_do_write_chunk(Prefix, Value, Host, PortStr,
State) of
{ok, _} ->
Acc + 1;
_ ->
Acc
end
end,
case lists:foldl(FoldFun, 0, ModHPs) of
N when is_integer(N), N > 0 ->
{ok, State};
0 ->
{error, oh_some_problem_yo, State}
end;
run(read_raw_line_local, KeyGen, _ValueGen, State) ->
{RawLine, Size, _File} = setup_read_raw_line(KeyGen),
bb_do_read_chunk(RawLine, Size, State#bb.host, State#bb.port_str, State);
run(cc_read_raw_line_local, KeyGen, _ValueGen, State0) ->
State = check_projection_check(State0),
{RawLine, Size, File} = setup_read_raw_line(KeyGen),
Prefix = re:replace(File, "\\..*", "", [{return, binary}]),
{_Chain, ModHPs} = calc_chain(read, State#bb.proj, State#bb.chain_map,
Prefix),
FoldFun = fun(_, {ok, _}=Acc) ->
Acc;
({Host, PortStr}, _Acc) ->
bb_do_read_chunk(RawLine, Size, Host, PortStr, State)
end,
lists:foldl(FoldFun, undefined, ModHPs).
bb_do_read_chunk(RawLine, Size, Host, PortStr, State) ->
try
Sock = get_cached_sock(Host, PortStr),
try
ok = gen_tcp:send(Sock, [RawLine, <<"\n">>]),
read_chunk(Sock, Size, State)
catch X2:Y2 ->
invalidate_cached_sock(Sock),
{error, {X2,Y2}, State}
end
catch X:Y ->
{error, {X,Y}, State}
end.
bb_do_write_chunk(Prefix, Value, Host, PortStr, State) ->
try
Sock = get_cached_sock(Host, PortStr),
try
{_, _, _} = upload_chunk_append(Sock, Prefix, Value),
{ok, State}
catch X2:Y2 ->
invalidate_cached_sock(Sock),
{error, {X2,Y2}, State}
end
catch X:Y ->
{error, {X,Y}, State}
end.
read_chunk(Sock, Size, State) ->
{ok, <<"OK\n">>} = gen_tcp:recv(Sock, 3),
{ok, _Chunk} = gen_tcp:recv(Sock, Size),
{ok, State}.
setup_read_raw_line(KeyGen) ->
RawLine = KeyGen(),
<<"R ", Rest/binary>> = RawLine,
{_Offset, Size, File} = read_hex_size(Rest),
{RawLine, Size, File}.
read_hex_size(Line) ->
<<OffsetHex:16/binary, " ", SizeHex:8/binary, " ", File/binary>> = Line,
<<Offset:64/big>> = hexstr_to_bin(OffsetHex),
<<Size:32/big>> = hexstr_to_bin(SizeHex),
{Offset, Size, File}.
read_projection_info(#bb{proj_path=undefined}=State) ->
State;
read_projection_info(#bb{proj_path=ProjectionPathOrDir}=State) ->
Proj = read_projection_file(ProjectionPathOrDir),
ChainMap = read_chain_map_file(ProjectionPathOrDir),
ModChainMap =
[{Chain, [{binary_to_list(Host), integer_to_list(Port)} ||
{Host, Port} <- Members]} ||
{Chain, Members} <- ChainMap],
State#bb{proj=Proj, chain_map=ModChainMap}.
check_projection_check(#bb{proj_check_ticker_started=false} = State) ->
timer:send_interval(5*1000 - random:uniform(500), projection_check),
check_projection_check(State#bb{proj_check_ticker_started=true});
check_projection_check(#bb{proj_check_ticker_started=true} = State) ->
receive
projection_check ->
read_projection_info(State)
after 0 ->
State
end.