Packages
brod
4.3.2
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_transaction.erl
%%%
%%% Copyright (c) 2023 @axs-mvd and contributors
%%%
%%% 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 A `brod_transaction' is a process that orchestates a set of
%% producers to store messages within a transaction, it also supports
%% committing offsets in the same transaction.
%%
%% Simple produce sample:
%%
%% ```
%% {ok, Tx} = brod_transaction:new(Client, TxId, []),
%% lists:foreach(fun(Partition) ->
%% Key = rand(), Value = rand(),
%% {ok, _Offset} =
%% brod_transaction:produce(Tx,
%% Topic,
%% Partition,
%% Key,
%% Value),
%% end, Partitions),
%% brod_transaction:commit(Tx),
%% '''
%%
%% handle callback of a group subscriber using offset commit within a
%% transaction:
%%
%% ```
%% handle_message(Topic,
%% Partition,
%% #kafka_message{ offset = Offset
%% , key = Key
%% , value = Value},
%% #{ client := Client
%% , group_id := GroupId} = State) ->
%% {ok, Tx} = brod_transaction:new(Client),
%% {ok, _ProducedOffset} = brod_transaction:produce(Tx, ?TOPIC_OUTPUT, Partition, Key, Value),
%% ok = brod_transaction:txn_add_offsets(Tx, GroupId, #{{Topic, Partition} => Offset}),
%% ok = brod_transaction:commit(Tx)
%%
%% {ok, ack_no_commit, State}.
%% '''
%%
-module(brod_transaction).
-behaviour(gen_server).
% public API
-export([ produce/5
, produce/4
, add_offsets/3
, commit/1
, abort/1
, stop/1
, new/3
, start_link/3
]).
% gen_server callbacks
-export([ init/1
, handle_cast/2
, handle_call/3
, terminate/2
]).
% type exports
-export_type([ batch_input/0
, call_ref/0
, client/0
, client_id/0
, transaction_config/0
, group_id/0
, key/0
, offset/0
, offsets_to_commit/0
, partition/0
, topic/0
, transaction/0
, transactional_id/0
, txn_ctx/0
, value/0
]).
%%==============================================================================
%% Type declarations
%%==============================================================================
-type call_ref() :: brod:call_ref().
-type client() :: client_id() | pid().
-type client_id() :: atom().
-type transaction_config() :: [ {timeout, non_neg_integer()}
| {backoff_step, non_neg_integer()}
| {max_retries, non_neg_integer()}
].
-type group_id() :: kpro:group_id().
-type key() :: brod:key().
-type offset() :: kpro:offset().
-type offsets_to_commit() :: kpro:offsets_to_commit().
-type partition() :: kpro:partition().
-type topic() :: kpro:topic().
-type transaction() :: pid().
-type transactional_id() :: kpro:transactional_id().
-type txn_ctx() :: kpro:txn_ctx().
-type value() :: brod:value().
-type batch_input() :: kpro:batch_input().
-record(state,
{ client_pid :: client()
, context :: txn_ctx()
, timeout :: pos_integer()
, sequences :: map()
, sent_partitions :: map()
, max_retries :: pos_integer()
, backoff_step :: pos_integer()
}).
-type state() :: #state{}.
%%==============================================================================
%% API functions
%%==============================================================================
%% @see start_link/3
-spec new(client(), transactional_id(), transaction_config()) -> {ok, transaction()}.
new(Client, TxId, Config) ->
gen_server:start_link(?MODULE,
{Client, TxId, Config},
[]).
%% @doc Start a new transaction, `TxId'will be the id of the transaction
%% `Config' is a proplist, all values are optional:
%% `timeout':`Connection timeout in millis
%% `backoff_step': after each retry it will sleep for 2^Attempt * backoff_step
%% millis
%% `max_retries'
-spec start_link(client(), transactional_id(), transaction_config()) -> {ok, pid()}.
start_link(Client, TxId, Config) ->
gen_server:start_link(?MODULE, {Client, TxId, Config}, []).
%% @doc Produce the message (key and value) to the indicated topic-partition
%% synchronously.
-spec produce(transaction(), topic(), partition(), key(), value()) ->
{ok, offset()} | {error, any()}.
produce(Transaction, Topic, Partition, Key, Value) ->
gen_server:call(Transaction, {produce, Topic, Partition, Key, Value}).
%% @doc Synchronously produce the batch of messages to the indicated
%% topic-partition
-spec produce(transaction(), topic(), partition(), batch_input()) ->
{ok, offset()} | {error, any()}.
produce(Transaction, Topic, Partition, Batch) ->
gen_server:call(Transaction, {produce, Topic, Partition, Batch}).
%% @doc Add the offset consumed by a group to the transaction.
-spec add_offsets(transaction(), group_id(), offsets_to_commit()) -> ok | {error, any()}.
add_offsets(Transaction, ConsumerGroup, Offsets) ->
gen_server:call(Transaction, {add_offsets, ConsumerGroup, Offsets}).
%% @doc Commit the transaction, after this, the gen_server will stop
-spec commit(transaction()) -> ok | {error, any()}.
commit(Transaction) ->
gen_server:call(Transaction, commit).
%% @doc Abort the transaction, after this, the gen_server will stop
-spec abort(transaction()) -> ok | {error, any()}.
abort(Transaction) ->
gen_server:call(Transaction, abort).
%% @doc Stop the transaction.
-spec stop(transaction()) -> ok | {error, any()}.
stop(Transaction) ->
gen_server:call(Transaction, terminate).
%%==============================================================================
%% gen_server callbacks
%%==============================================================================
init({Client, TxId, PropListConfig}) ->
ClientPid = pid(Client),
erlang:process_flag(trap_exit, true),
Config =
#{ max_retries := MaxRetries
, backoff_step := BackOffStep
, timeout := Timeout
} = lists:foldl(fun({K, V}, M) ->
M#{K => V}
end,
#{ max_retries => 5
, backoff_step => 100
, timeout => 1000
}, PropListConfig),
{ok, CTX} = make_txn_context(ClientPid, TxId, Config),
{ok, #state{ client_pid = ClientPid
, context = CTX
, max_retries = MaxRetries
, backoff_step= BackOffStep
, timeout = Timeout
, sequences = #{}
, sent_partitions = #{}
}}.
handle_call({add_offsets, ConsumerGroup, Offsets}, _From,
#state{ client_pid = Client
, context = CTX
, max_retries = MaxRetries
, backoff_step = BackOffStep
} = State) ->
Resp = do_add_offsets(Client, CTX, ConsumerGroup, Offsets,
#{ max_retries => MaxRetries
, backoff_step => BackOffStep
}),
{reply, Resp, State};
handle_call({produce, Topic, Partition, Key, Value}, _From,
#state{} = OldState) ->
case do_produce(Topic, Partition, Key, Value, OldState) of
{ok, {Offset, State}} -> {reply, {ok, Offset}, State};
{error, Reason} -> {reply, {error, Reason}, OldState}
end;
handle_call({produce, Topic, Partition, Batch}, _From,
#state{} = OldState) ->
case do_batch_produce(Topic, Partition, Batch, OldState) of
{ok, {Offset, State}} -> {reply, {ok, Offset}, State};
{error, Reason} -> {reply, {error, Reason}, OldState}
end;
handle_call(commit, _From, #state{context = CTX} = State) ->
{stop, normal, kpro:txn_commit(CTX), State};
handle_call(terminate, _From, State) ->
{stop, normal, ok, #state{} = State};
handle_call(abort, _From,
#state{context = CTX} = State) ->
{stop, normal, kpro:txn_abort(CTX), State};
handle_call({abort, Timeout}, _From,
#state{context = CTX} = State) ->
{stop, normal,
kpro:txn_abort(CTX, #{timeout => Timeout}),
State};
handle_call(stop, _From, #state{} = State) ->
{stop, normal, ok, State};
handle_call(Call, _From, #state{} = State) ->
{reply, {error, {unsupported_call, Call}}, State}.
handle_cast(_Cast, #state{} = State) ->
{noreply, State}.
terminate(_Reason, #state{context = CTX}) ->
kpro:txn_abort(CTX).
%%==============================================================================
%% Internal functions
%%==============================================================================
make_txn_context(Client, TxId, #{ max_retries := MaxRetries
, backoff_step := BackOffStep
})->
persistent_call(fun() ->
make_txn_context_internal(Client, TxId)
end, MaxRetries, BackOffStep).
make_txn_context_internal(Client, TxId) ->
case brod_client:get_transactional_coordinator(Client, TxId) of
{ok, {{Host, Port}, _}} ->
case brod_client:get_connection(Client, Host, Port) of
{ok, Conn} -> kpro:txn_init_ctx(Conn, TxId);
{error, Reason} -> {error, Reason}
end;
{error, Reason} -> {error, Reason}
end.
do_add_offsets(Client, CTX, ConsumerGroup, Offsets,
#{ max_retries := MaxRetries
, backoff_step := BackOffStep
}) ->
persistent_call(fun() ->
do_add_offsets_internal(Client, CTX,
ConsumerGroup, Offsets)
end,
MaxRetries, BackOffStep).
do_add_offsets_internal(Client, CTX, ConsumerGroup, Offsets) ->
case brod_client:get_group_coordinator(Client, ConsumerGroup) of
{ok, {{Host, Port}, _}} ->
case brod_client:get_connection(Client, Host, Port) of
{ok, Conn} -> send_cg_and_offset(Conn, CTX, ConsumerGroup, Offsets);
{error, Reason} -> {error, Reason}
end;
{error, Reason} -> {error, Reason}
end.
send_cg_and_offset(GroupCoordConn, CTX, ConsumerGroup, Offsets) ->
%% before adding the offset we need to let kafka know we are going to commit
%% the offsets.
case kpro:txn_send_cg(CTX, ConsumerGroup) of
ok -> kpro:txn_offset_commit(GroupCoordConn, ConsumerGroup, CTX, Offsets);
{error, Reason} -> {error, Reason}
end.
-spec do_produce(topic(), partition(), key(), value(), state()) ->
{error, string()} | {ok, {offset(), state()}}.
do_produce(Topic, Partition, Key, Value, State) ->
do_batch_produce(Topic, Partition, [#{ key => Key
, value => Value
, ts => kpro_lib:now_ts()
}], State).
-spec do_batch_produce(topic(), partition(), batch_input(), state()) ->
{error, string()} | {ok, {offset(), state()}}.
do_batch_produce(Topic, Partition, Batch, #state{ max_retries = MaxRetries
, backoff_step = BackOffStep
} = State) ->
persistent_call(fun() ->
do_batch_produce_internal(Topic, Partition,
Batch, State)
end, MaxRetries, BackOffStep).
do_batch_produce_internal(Topic, Partition, Batch,
#state{ client_pid = ClientPid
, timeout = Timeout
, context = CTX
, sequences = Sequences
, sent_partitions = OldSentPartitions
} = State) ->
case conn_and_vsn(ClientPid, Topic, Partition) of
{ok, {Connection, Vsn}} ->
FirstSequence = maps:get({Topic, Partition}, Sequences, 0),
ProduceReq = kpro_req_lib:produce(Vsn, Topic, Partition,
Batch,
#{ txn_ctx => CTX
, first_sequence => FirstSequence
}),
SentPartitions =
case maps:get({Topic, Partition}, OldSentPartitions, not_found) of
not_found ->
ok = kpro:txn_send_partitions(CTX, [{Topic, Partition}]),
maps:put({Topic, Partition}, true, OldSentPartitions);
_ -> OldSentPartitions
end,
case send_req(Connection, ProduceReq, Timeout) of
{ok, Offset} ->
{ok, {Offset, State#state{ sent_partitions = SentPartitions
, sequences = maps:put({Topic, Partition},
FirstSequence + length(Batch),
Sequences)
}}};
{error, Reason} -> {error, Reason}
end;
{error, Reason} -> {error, Reason}
end.
send_req(Connection, ProduceReq, Timeout) ->
case kpro:request_sync(Connection, ProduceReq, Timeout) of
{ok, Rsp} -> brod_utils:parse_rsp(Rsp);
{error, Reason} -> {error, Reason}
end.
conn_and_vsn(ClientPid, Topic, Partition) ->
case brod_client:get_leader_connection(ClientPid, Topic, Partition) of
{ok, Connection} ->
case kpro:get_api_versions(Connection) of
{ok, #{ produce := {_, Vsn}
, fetch := {_, _}
}} -> {ok, {Connection, Vsn}};
{error, Reason} -> {error, Reason}
end;
{error, Reason} -> {error, Reason}
end.
-spec pid(client()) -> pid().
pid(Client) when is_atom(Client) -> whereis(Client);
pid(Client) when is_pid(Client) -> Client.
backoff(Attempt, BackOffStep) ->
timer:sleep(trunc(math:pow(2, Attempt) * BackOffStep)).
persistent_call(Fun, MaxRetries, BackOffStep) ->
persistent_call(Fun, 0, MaxRetries, BackOffStep).
persistent_call(Fun, Attempt, MaxRetries, BackOffStep) ->
case Fun() of
ok -> ok;
{ok, R} -> {ok, R};
{error, _} when Attempt + 1 < MaxRetries ->
backoff(Attempt, BackOffStep),
persistent_call(Fun, Attempt + 1, MaxRetries, BackOffStep);
{error, Reason} -> {error, Reason}
end.
%%%_* Emacs ====================================================================
%%% Local Variables:
%%% allout-layout: t
%%% erlang-indent-level: 2
%%% End: