Current section

Files

Jump to
brucke src brucke_subscriber.erl
Raw

src/brucke_subscriber.erl

%%%
%%% Copyright (c) 2016-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.
%%%
-module(brucke_subscriber).
-export([ start_link/3
, loop/1
, stop/1
]).
-include("brucke_int.hrl").
-type partition() :: brod:partition().
-type offset() :: brod:offset().
-type state() :: map().
-type pending_acks() :: brucke_backlog:backlog().
-type cb_state() :: brucke_filter:cb_state().
-define(SUBSCRIBE_RETRY_LIMIT, 3).
-define(SUBSCRIBE_RETRY_SECONDS, 2).
%% Because the upstream messages might be dispatched to
%% different downstream partitions, there is no single downstream
%% producer process to monitor.
%% Instead, we periodically send a loop-back message to check if
%% the producer pids are still alive
-define(CHECK_PRODUCER_DELAY, timer:seconds(30)).
-define(CHECK_PRODUCER_MSG, check_producer).
-define(kafka_ack(UpstreamOffset, Ref), {kafka_ack, UpstreamOffset, Ref}).
%%%_* APIs =====================================================================
-spec start_link(route(), partition(), ?undef | offset()) -> {ok, pid()}.
start_link(Route, UpstreamPartition, BeginOffset) ->
Parent = self(),
#route{ upstream = {UpstreamClientId, UpstreamTopic}
, options = Options
} = Route,
#{ filter_module := FilterModule
, filter_init_arg := InitArg
} = Options,
UpstreamClusterName = brucke_config:get_cluster_name(UpstreamClientId),
State = #{ route => Route
, upstream_partition => UpstreamPartition
, parent => Parent
, consumer => subscribing
, pending_acks => brucke_backlog:new()
, upstream_cluster => UpstreamClusterName
},
Pid = proc_lib:spawn_link(
fun() ->
{ok, CbState} = brucke_filter:init(FilterModule, UpstreamTopic,
UpstreamPartition, InitArg),
loop(State#{filter_cb_state => CbState})
end),
Pid ! {subscribe, BeginOffset, 0},
_ = erlang:send_after(?CHECK_PRODUCER_DELAY, Pid, ?CHECK_PRODUCER_MSG),
{ok, Pid}.
stop(Pid) when is_pid(Pid) ->
erlang:monitor(process, Pid),
_ = exit(Pid, shutdown),
receive
{'DOWN', _Ref, process, Pid, _reason} ->
ok
end.
%%%_* Internal Functions =======================================================
-spec loop(state()) -> no_return().
loop(State) ->
receive
Msg ->
?MODULE:loop(handle_msg(State, Msg))
end.
-spec handle_msg(state(), term()) -> state() | no_return().
handle_msg(State, {subscribe, BeginOffset, Count}) ->
subscribe(State, BeginOffset, Count);
handle_msg(State, ?CHECK_PRODUCER_MSG) ->
check_producer(State);
handle_msg(State, {Pid, #kafka_message_set{} = MsgSet}) ->
handle_message_set(State, Pid, MsgSet);
handle_msg(State, ?kafka_ack(UpstreamOffset, Ref)) ->
handle_produce_reply(State, UpstreamOffset, Ref);
handle_msg(State, {'DOWN', _Ref, process, Pid, _Reason}) ->
handle_consumer_down(State, Pid);
handle_msg(_State, {_BrodConsumerPid,
#kafka_fetch_error{ topic = Topic
, partition = Partition
, error_code = 'UnknownTopicOrPartition'
}}) ->
lager:info("Topic ~s deleted? "
"subscriber for partition ~p is exiting with normal reason",
[Topic, Partition]),
erlang:exit(normal);
handle_msg(_State, Unknown) ->
erlang:exit({unknown_message, Unknown}).
-spec subscribe(state(), offset(), non_neg_integer()) -> state() | no_return().
subscribe(#{ route := Route
, upstream_partition := UpstreamPartition
, consumer := subscribing
} = State, BeginOffset, RetryCount) ->
#route{upstream = {UpstreamClientId, UpstreamTopic}} = Route,
SubscribeOptions =
case is_integer(BeginOffset) of
true ->
true = (BeginOffset >= 0), %% assert
[{begin_offset, BeginOffset}];
false ->
[] %% use the default begin offset in consumer config
end,
case brod:subscribe(UpstreamClientId, self(), UpstreamTopic,
UpstreamPartition, SubscribeOptions) of
{ok, Pid} ->
_ = erlang:monitor(process, Pid),
State#{consumer := Pid};
{error, _Reason} when RetryCount < ?SUBSCRIBE_RETRY_LIMIT ->
Msg = {subscribe, BeginOffset, RetryCount+1},
_ = erlang:send_after(timer:seconds(?SUBSCRIBE_RETRY_SECONDS), self(), Msg),
State;
{error, Reason} ->
exit({failed_to_subscribe, Reason})
end;
subscribe(State, _BeginOffset, _UnknownRef) ->
State.
-spec handle_message_set(state(), pid(), #kafka_message_set{}) -> state().
handle_message_set(#{ route := Route
, upstream_cluster := Cluster
, upstream_partition := Partition
} = State, Pid, MsgSet) ->
#route{ upstream = {_UpstreamClientId, Topic}
, options = RouteOptions
} = Route,
#{consumer := Pid} = State, %% assert
#kafka_message_set{high_wm_offset = HighWmOffset} = MsgSet,
?MX_HIGH_WM_OFFSET(Cluster, Topic, Partition, HighWmOffset),
?MX_TOTAL_VOLUME(Cluster, Topic, Partition, msg_set_bytes(MsgSet)),
NewState = State#{high_wm_offset => HighWmOffset},
do_handle_message_set(NewState, MsgSet, RouteOptions).
do_handle_message_set(#{ route := Route
, pending_acks := PendingAcks
, filter_cb_state := CbState
} = State, MsgSet, RouteOptions) ->
RepartStrategy = brucke_lib:get_repartitioning_strategy(RouteOptions),
#{filter_module := FilterModule} = RouteOptions,
#kafka_message_set{ topic = Topic
, partition = Partition
, messages = Messages
} = MsgSet,
#route{ upstream = {_UpstreamClientId, Topic}
, downstream = {DownstreamClientId, DownstreamTopic}
} = Route,
{ok, PartCnt} = brod_client:get_partitions_count(DownstreamClientId, DownstreamTopic),
FilterFun =
fun(#kafka_message{ offset = Offset
, key = Key
, value = Value
, headers = Headers
} = Msg, CbStateIn) ->
{FilterResult, NewCbState} =
brucke_filter:filter(FilterModule, Topic, Partition, Offset,
Key, Value, Headers, CbStateIn),
{make_batch(Msg, FilterResult), NewCbState}
end,
ProduceFun =
fun(Msg, Cb) ->
Key = maps:get(key, Msg, <<>>),
DownstreamPartition = partition(PartCnt, Partition, RepartStrategy, Key),
{ok, ProducerPid} = brod:get_producer(DownstreamClientId, DownstreamTopic, DownstreamPartition),
case brod:produce_cb(ProducerPid, Key, Msg, Cb) of
ok -> {ok, ProducerPid};
{error, Reason} -> erlang:exit(Reason)
end
end,
{NewPendingAcks, NewCbState} =
produce(FilterFun, ProduceFun, Messages, PendingAcks, CbState),
handle_acked(State#{ pending_acks := NewPendingAcks
, filter_cb_state := NewCbState
}).
%% Make a batch input for downstream producer
make_batch(_Message, false) ->
%% discard message
[];
make_batch(#kafka_message{ ts_type = TsType
, ts = Ts
, key = Key
, value = Value
, headers = Headers
}, true) ->
%% to downstream as-is
[mk_msg(Key, Value, resolve_ts(TsType, Ts), Headers)];
make_batch(#kafka_message{ ts_type = TsType
, ts = Ts
}, {K, V}) ->
%% old version filter return format k-v (without timestamp)
[mk_msg(K, V, resolve_ts(TsType, Ts), [])];
make_batch(#kafka_message{}, {T, K, V}) ->
%% old version filter return format t-k-v
[mk_msg(K, V, T, [])];
make_batch(#kafka_message{ ts_type = TsType
, ts = Ts
}, M) when is_map(M) ->
K = maps:get(key, M, <<>>),
V = maps:get(value, M, <<>>),
T = maps:get(ts, M, resolve_ts(TsType, Ts)),
H = maps:get(headers, M, []),
[mk_msg(K, V, T, H)];
make_batch(#kafka_message{}, L) when is_list(L) ->
%% filter retruned a batch
F = fun({K, V}) -> mk_msg(K, V, now_ts(), []);
({T, K, V}) -> mk_msg(K, V, T, []);
(M) when is_map(M) -> M
end,
lists:map(F, L).
mk_msg(K, V, T, Headers) ->
#{key => K, value => V, ts => T, headers => Headers}.
now_ts() -> kpro_lib:now_ts().
resolve_ts(create, Ts) when Ts > 0 -> Ts;
resolve_ts(_, _) -> now_ts().
-spec produce(fun((brod:message(), cb_state()) -> brod:batch_input()),
fun((brod:batch_input(), brod:produce_ack_cb()) -> {ok, pid()}),
[#kafka_message{}], pending_acks(),
cb_state()) -> {pending_acks(), cb_state()}.
produce(_FilterFun, _ProduceFun, [], PendingAcks, CbState) ->
{PendingAcks, CbState};
produce(FilterFun, ProduceFun,
[#kafka_message{offset = Offset} = Msg | Rest],
PendingAcks0, CbState0) ->
{FilterResult, CbState} = FilterFun(Msg, CbState0),
Caller = self(),
PendingRefs =
case FilterResult of
[] -> []; %% discard this message
Batch ->
lists:map(
fun(OneMsg) ->
Ref = make_ref(),
Cb = fun(_, _) -> Caller ! ?kafka_ack(Offset, Ref) end,
{ok, Pid} = ProduceFun(OneMsg, Cb),
{Pid, Ref}
end, Batch)
end,
PendingAcks = brucke_backlog:add(Offset, PendingRefs, PendingAcks0),
produce(FilterFun, ProduceFun, Rest, PendingAcks, CbState).
-spec handle_produce_reply(state(), offset(), reference()) -> state().
handle_produce_reply(#{pending_acks := PendingAcks0} = State, UpstreamOffset, Ref) ->
PendingAcks = brucke_backlog:ack(UpstreamOffset, Ref, PendingAcks0),
handle_acked(State#{pending_acks := PendingAcks}).
-spec handle_acked(state()) -> state().
handle_acked(#{ pending_acks := PendingAcks
, parent := Parent
, upstream_cluster := UpstreamCluster
, upstream_partition := UpstreamPartition
, consumer := ConsumerPid
, route := Route
, high_wm_offset := HighWmOffset
} = State) ->
#route{upstream = {_UpstreamClientId, UpstreamTopic}} = Route,
{OffsetToAck, NewPendingAcks} = brucke_backlog:prune(PendingAcks),
case is_integer(OffsetToAck) of
true ->
%% tell upstream consumer to fetch more
ok = brod:consume_ack(ConsumerPid, OffsetToAck),
?MX_CURRENT_OFFSET(UpstreamCluster, UpstreamTopic,
UpstreamPartition, OffsetToAck),
?MX_LAGGING_OFFSET(UpstreamCluster, UpstreamTopic,
UpstreamPartition, HighWmOffset - OffsetToAck),
%% tell parent to update my next begin_offset in case i crash
%% parent should also report it to coordinator and (later) commit to kafka
Parent ! {ack, UpstreamPartition, OffsetToAck};
false ->
ok
end,
State#{pending_acks := NewPendingAcks}.
%% Return 'true' if ALL producers of unacked requests are still alive.
-spec check_producer(state()) -> state() | no_return().
check_producer(#{pending_acks := Pendings} = State) ->
Pids = brucke_backlog:get_producers(Pendings),
NewState =
case lists:all(fun erlang:is_process_alive/1, Pids) of
true -> State;
false -> erlang:exit(producer_down)
end,
_ = erlang:send_after(?CHECK_PRODUCER_DELAY, self(), ?CHECK_PRODUCER_MSG),
NewState.
-spec handle_consumer_down(state(), pid()) -> state().
handle_consumer_down(#{consumer := Pid} = _State, Pid) ->
erlang:exit(consumer_down);
handle_consumer_down(State, _UnknownPid) ->
State.
-spec partition(integer(), partition(), repartitioning_strategy(), binary()) ->
brod_partition_fun().
partition(_PartitionCount, UpstreamPartition, strict_p2p, _Key) ->
UpstreamPartition;
partition(PartitionCount, _UpstreamPartition, key_hash, Key) ->
erlang:phash2(Key, PartitionCount);
partition(PartitionCount, _UpstreamPartition, random, _Key) ->
rand:uniform(PartitionCount) - 1.
msg_set_bytes(#kafka_message_set{messages = Messages}) ->
msg_set_bytes(Messages, 0).
msg_set_bytes([], Bytes) -> Bytes;
msg_set_bytes([#kafka_message{key = K, value = V,
headers = Headers} | Rest], Bytes) ->
msg_set_bytes(Rest, Bytes + size(K) + size(V) + header_bytes(Headers)).
header_bytes([]) -> 0;
header_bytes([{K, V} | Rest]) -> size(K) + size(V) + header_bytes(Rest).
%%%_* Emacs ====================================================================
%%% Local Variables:
%%% allout-layout: t
%%% erlang-indent-level: 2
%%% End: