Packages
brod
3.7.1
4.5.7
4.5.6
4.5.5
4.5.4
4.5.3
4.5.2
4.5.1
4.5.0
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.3
4.3.2
4.3.1
4.3.0
4.2.0
4.1.1
4.1.0
4.0.0
3.19.1
3.19.0
3.18.0
3.17.1
3.17.0
3.16.5
3.16.4
3.16.3
3.16.2
3.16.1
3.16.0
3.15.6
3.15.5
3.15.4
3.15.3
3.15.1
3.15.0
3.14.0
3.13.0
3.12.0
3.11.0
3.10.0
3.9.5
3.9.3
3.9.2
3.9.1
3.9.0
3.8.1
3.8.0
3.7.11
3.7.10
3.7.9
3.7.8
3.7.7
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
3.6.2
3.6.1
3.6.0
3.5.2
3.5.1
3.5.0
3.4.0
3.3.5
3.3.4
3.3.3
3.3.2
3.3.1
3.3.0
3.2.0
3.0.0
2.5.0
2.4.1
2.4.0
2.3.7
2.3.6
2.3.5
2.3.4
2.3.3
2.3.1
2.2.16
2.2.15
2.2.14
2.2.12
2.2.11
2.2.10
2.2.9
2.2.8
2.2.7
2.2.6
2.2.5
2.2.4
2.2.3
2.2.2
2.2.1
2.2.0
2.1.12
2.1.11
2.1.10
2.1.8
2.1.7
2.1.4
2.1.2
2.0.0
Apache Kafka Erlang client library
Current section
Files
Jump to
Current section
Files
src/brod_cli_pipe.erl
%%%
%%% Copyright (c) 2017-2018, Klarna Bank AB (publ)
%%%
%%% Licensed 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.
%%%
%% @doc The input source of brod-cli pipe command
%% This module implements a process that reads off the bytes
%% from the data source (either stdin or a file)
%% and sends the bytes to parent process.
%% Messages sent to parent process:
%% {pipe, self(), [{Ts :: integer(), Key :: binary(), Val :: binary()}]}
%% @end
-module(brod_cli_pipe).
-ifdef(build_brod_cli).
-behaviour(gen_server).
-export([ start_link/1
, stop/1
]).
-export([ code_change/3
, handle_call/3
, handle_cast/2
, handle_info/2
, init/1
, terminate/2
]).
-include("brod_int.hrl").
-type arg_name() :: source
| kv_deli
| msg_deli
| prompt
| tail
| no_exit
| blk_size
| retry_delay.
-type arg_value() :: term().
-define(LINE_BREAK, <<"\n">>).
-define(STDIN, standard_io).
-define(EOF_RETRY_DELAY_MS, 100).
-define(NOT_APPLICABLE, 'N/A').
-define(CONTINUE_MSG, continue).
-define(PARENT_BUSY_MSG_QUEUE_LEN_THRESHOLD, 100).
-type delimiter() :: binary().
-type epoch_ms() :: integer().
-type read_fun() ::
fun((?STDIN | file:io_device(), [binary()]) -> eof |
{[{epoch_ms(), Key :: binary(), Val :: binary()}], [binary()]}).
-record(state, { parent :: pid()
, source :: ?STDIN | {file, string()}
, read_fun :: read_fun()
, is_eof_exit :: boolean()
, is_tail :: boolean()
, io_device :: ?undef | ?STDIN | file:io_device()
, acc_bytes = [] :: [binary()]
, retry_delay :: timeout()
}).
%% @doc Args explained:
%% source: 'standard_io' | {file, "path/to/srouce"}
%% kv_deli: 'none' | binary().
%% Delimiter bytes for message key and value
%% msg_deli: binary(). Delimiter between kafka messages
%% NOTE: eof is always considered a message delimiter
%% prompt: boolean(). Applicable when source is standard_io AND
%% when kv_deli and msg_deli are both '\n'
%% prompts 'key> ' for key input and 'val> ' for value input
%% tail: boolean(). Applicable when source is a file
%% tell brod-cli to start reading from EOF
%% no_exit: boolean(). Do not exit when reaching EOF
%% blk_size: Read block size
-spec start_link([{arg_name(), arg_value()}]) -> {ok, pid()}.
start_link(Args) ->
Parent = self(),
Arg = fun(Name) -> {_, V} = lists:keyfind(Name, 1, Args), V end,
KvDeli = Arg(kv_deli),
MsgDeli = Arg(msg_deli),
Source = Arg(source),
IsLineMode = MsgDeli =:= ?LINE_BREAK,
BlkSize = Arg(blk_size),
IsPrompt = Arg(prompt),
IsTail = Arg(tail),
IsNoExit = Arg(no_exit),
IsEofExit = not (IsTail orelse IsNoExit),
ReadFun =
case IsLineMode of
true when IsPrompt andalso Source =:= ?STDIN ->
make_prompt_line_reader(KvDeli);
true ->
make_line_reader(KvDeli, _PromptStr = "");
false ->
make_stream_reader(KvDeli, MsgDeli, BlkSize, IsEofExit)
end,
State = #state{ parent = Parent
, source = Source
, read_fun = ReadFun
, is_tail = IsTail
, is_eof_exit = IsEofExit
, retry_delay = Arg(retry_delay)
},
gen_server:start_link({local, ?MODULE}, ?MODULE, State, []).
%% @doc Stop gen_server.
stop(Pid) -> gen_server:cast(Pid, stop).
%% @doc Tell reader to continue.
continue() -> self() ! ?CONTINUE_MSG.
%% @hidden
init(#state{source = Source, is_tail = IsTail} = State0) ->
IoDevice =
case Source of
?STDIN ->
?STDIN;
{file, File} ->
{ok, Fd} = file:open(File, [read, binary]),
IsTail andalso file:position(Fd, eof),
Fd
end,
State = State0#state{io_device = IoDevice},
_ = continue(),
{ok, State}.
%% @hidden
handle_info(?CONTINUE_MSG, #state{parent = Parent} = State) ->
case erlang:process_info(Parent, message_queue_len) of
{_, Len} when Len >= ?PARENT_BUSY_MSG_QUEUE_LEN_THRESHOLD ->
ok = delay_continue(State),
{noreply, State};
_ ->
handle_read(State)
end;
handle_info(_Info, State) ->
{noreply, State}.
%% @hidden
handle_cast(stop, State) ->
{stop, normal, State};
handle_cast(_Cast, State) ->
{noreply, State}.
%% @hidden
handle_call(Call, _From, State) ->
{reply, {error, {unknown_call, Call}}, State}.
%% @hidden
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% @hidden
terminate(_Reason, _State) ->
ok.
%%%_* Privates =================================================================
send_to_parent(Parent, Msgs0) ->
FilterF = fun(?TKV(_T, K, V)) -> K =/= <<>> orelse V =/= <<>> end,
Msgs = lists:filter(FilterF, Msgs0),
Msgs =/= [] andalso erlang:send(Parent, {pipe, self(), Msgs}),
ok.
handle_read(#state{ read_fun = ReadFun
, acc_bytes = Acc0
, io_device = IoDevice
, parent = Parent
} = State0) ->
case ReadFun(IoDevice, Acc0) of
eof ->
handle_eof(State0);
{Msgs, Acc} ->
_ = continue(), %% continue next trunk
ok = send_to_parent(Parent, Msgs),
State = State0#state{acc_bytes = Acc},
{noreply, State}
end.
handle_eof(#state{io_device = ?STDIN} = State) ->
%% standard_io pipe closed
{stop, normal, State};
handle_eof(#state{is_eof_exit = true} = State) ->
{stop, normal, State};
handle_eof(#state{io_device = Fd} = State) ->
%% Get current position
{ok, LastPos} = file:position(Fd, {cur, 0}),
%% Try set position to EOF,
%% see if it is the current position
case file:position(Fd, eof) of
{ok, NewPos} when NewPos < LastPos ->
%% File has been truncated.
%% Don't know what to do because
%% we can not assume the file is truncated to empty
{stop, pipe_source_truncated, State};
{ok, _Pos} ->
_ = file:position(Fd, LastPos),
ok = delay_continue(State),
{noreply, State}
end.
delay_continue(#state{retry_delay = Delay}) ->
_ = erlang:send_after(Delay, self(), ?CONTINUE_MSG),
ok.
-spec make_prompt_line_reader(none | delimiter()) -> read_fun().
make_prompt_line_reader(_KvDeli = none) ->
%% Read only value, no key
fun(?STDIN, _Acc) ->
case read_line(?STDIN, "VAL> ") of
eof -> eof;
Value -> {[{_Key = <<>>, Value}], []}
end
end;
make_prompt_line_reader(_KvDeli = ?LINE_BREAK) ->
fun(?STDIN, _Acc) ->
case read_line(?STDIN, "KEY> ") of
eof -> eof;
Key ->
case read_line(?STDIN, "VAL> ") of
eof -> {[make_msg(Key, <<>>)], []};
Value -> {[make_msg(Key, Value)], []}
end
end
end;
make_prompt_line_reader(KvDeli) ->
Prompt = "KEY" ++ binary_to_list(KvDeli) ++ "VAL> ",
make_line_reader(KvDeli, Prompt).
-spec make_line_reader(none | binary(), string()) -> read_fun().
make_line_reader(KvDeli, Prompt) ->
fun(IoDevice, _Acc) ->
case read_line(IoDevice, Prompt) of
eof ->
eof;
Key when KvDeli =:= <<"\n">> ->
case read_line(IoDevice, Prompt) of
eof ->
eof;
Val ->
{[make_msg(Key, Val)], []}
end;
Val when KvDeli =:= none ->
{[make_msg(<<>>, Val)], []};
Line ->
[Key, Value] = binary:split(Line, bin(KvDeli)),
{[make_msg(Key, Value)], []}
end
end.
-spec make_stream_reader(none | delimiter(), delimiter(),
pos_integer(), boolean()) -> read_fun().
make_stream_reader(KvDeli, MsgDeli, BlkSize, IsEofExit) ->
IsSameDeli = MsgDeli =:= KvDeli,
KvDeliCp = case is_binary(KvDeli) of
true -> binary:compile_pattern(KvDeli);
false -> none
end,
MsgDeliCp = binary:compile_pattern(MsgDeli),
fun(IoDevice, Acc) ->
case file:read(IoDevice, BlkSize) of
eof ->
case IsEofExit of
true when Acc =:= [] ->
%% Reached EOF
eof;
true ->
%% Configured to exit when reaching EOF
%% try split kv-pairs NOW
LastMsg = bin(lists:reverse(Acc)),
KvPairs = split_kv_pairs([LastMsg], KvDeliCp, IsSameDeli),
{KvPairs, []};
false ->
%% Keep looping for the next message delimiter
{[], Acc}
end;
{ok, Bytes} ->
Acc1 = add_acc(size(MsgDeli), Bytes, Acc),
{Messages, NewAcc} = split_messages(MsgDeliCp, Acc1),
KvPairs = split_kv_pairs(Messages, KvDeliCp, IsSameDeli),
{KvPairs, NewAcc}
end
end.
-spec add_acc(pos_integer(), binary(), [binary()]) -> [binary()].
add_acc(_DeliSize = 1, Bytes, Acc) ->
%% Delimiter is only one byte, in no way coult it be cut in half
[Bytes | Acc];
add_acc(_DeliSize, Bytes, []) ->
[Bytes];
add_acc(DeliSize, Bytes, [Tail | Header]) ->
Size = size(Tail) - DeliSize,
case Size =< 0 of
true ->
[<<Tail/binary, Bytes/binary>> | Header];
false ->
%% cut a DeliSize tail from acc and prepend as current head
%% to make sure we will not cut delimiter into two chunks
<<TailH:Size/binary, TailT/binary>> = Tail, %% cut
NewTail = <<TailT/binary, Bytes/binary>>, %% new tail
[NewTail, TailH | Header]
end.
-spec split_messages(binary:cp(), [binary()]) -> {[binary()], [binary()]}.
split_messages(MsgDeliCp, [Tail | Header]) ->
case binary:split(Tail, MsgDeliCp, [global]) of
[_] ->
%% no delimiter found
{[], [Tail | Header]};
[First0 | More] ->
First = bin([lists:reverse(Header), First0]),
case lists:reverse(More) of
[<<>> | Msgs] ->
{[First | lists:reverse(Msgs)], []};
[NewTail | Msgs] ->
{[First | lists:reverse(Msgs)], [NewTail]}
end
end.
-spec split_kv_pairs([binary()], none | delimiter(), boolean()) -> brod:value().
split_kv_pairs(Msgs, none, _IsSameDeli) ->
lists:map(fun(Msg) -> make_msg(<<>>, Msg) end, Msgs);
split_kv_pairs(Msgs, _KvDeliCp, _IsSameDeli = true) ->
make_msgs(Msgs);
split_kv_pairs(Msgs, KvDeliCp, _IsSameDeli = false) ->
lists:map(fun(Msg) ->
[K, V] = binary:split(Msg, KvDeliCp),
make_msg(K, V)
end, Msgs).
make_msgs([]) -> [];
make_msgs([K, V | Rest]) ->
[make_msg(K, V) | make_msgs(Rest)].
make_msg(K, V) ->
CreateTs = brod_utils:epoch_ms(),
?TKV(CreateTs, K, V).
-spec read_line(?STDIN | file:io_device(), string()) -> eof | binary().
read_line(IoDevice, Prompt) ->
case io:get_line(IoDevice, Prompt) of
eof -> eof;
Line ->
Chars = unicode:characters_to_list(Line),
unicode:characters_to_binary(rstrip(Chars, "\n"))
end.
-spec rstrip(string(), string()) -> string().
rstrip(Str, CharSet) ->
lists:reverse(lstrip(lists:reverse(Str), CharSet)).
-spec lstrip(string(), string()) -> string().
lstrip([], _) -> [];
lstrip([C | Rest] = Str, CharSet) ->
case lists:member(C, CharSet) of
true -> lstrip(Rest, CharSet);
false -> Str
end.
-spec bin(iodata()) -> binary().
bin(X) -> iolist_to_binary(X).
-endif.
%%%_* Emacs ====================================================================
%%% Local Variables:
%%% allout-layout: t
%%% erlang-indent-level: 2
%%% End: