Current section

Files

Jump to
kafka_protocol src kpro_txn_lib.erl
Raw

src/kpro_txn_lib.erl

%%% Copyright (c) 2018-2021, 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(kpro_txn_lib).
-export([ add_offsets_to_txn/3
, add_partitions_to_txn/3
, end_txn/3
, txn_init_ctx/3
, txn_offset_commit/5
]).
-type txn_ctx() :: kpro:txn_ctx().
-type topic() :: kpro:topic().
-type partition() :: kpro:partition().
-type offsets_to_commit() :: kpro:offsets_to_commit().
-type group_id() :: kpro:group_id().
-type connection() :: kpro:connection().
-define(DEFAULT_TIMEOUT, timer:seconds(5)).
-define(DEFAULT_TXN_TIMEOUT, timer:seconds(30)).
-include("kpro_private.hrl").
%% @see kpro:txn_init_ctx/3.
txn_init_ctx(Connection, TxnId, Opts) ->
ReqTimeout = maps:get(timeout, Opts, ?DEFAULT_TIMEOUT),
TxnTimeout = maps:get(txn_timeout, Opts, ?DEFAULT_TXN_TIMEOUT),
Req = kpro_req_lib:make(init_producer_id, _Vsn = 0,
#{ transactional_id => TxnId
, transaction_timeout_ms => TxnTimeout
}),
FL =
[ fun() -> kpro_connection:request_sync(Connection, Req, ReqTimeout) end
, fun(#kpro_rsp{msg = Rsp}) -> make_txn_ctx(Connection, TxnId, Rsp) end
],
kpro_lib:ok_pipe(FL).
%% @doc Commit or abort transaction.
-spec end_txn(txn_ctx(), commit | abort, #{timeout => timeout()}) ->
ok | {error, any()}.
end_txn(TxnCtx, AbortOrCommit, Opts) ->
Connection = maps:get(connection, TxnCtx),
Timeout = maps:get(timeout, Opts, ?DEFAULT_TIMEOUT),
Req = kpro_req_lib:end_txn(TxnCtx, AbortOrCommit),
FL =
[ fun() -> kpro_connection:request_sync(Connection, Req, Timeout) end
, fun(#kpro_rsp{msg = #{error_code := EC}}) -> ok_or_error_tuple(EC) end
],
kpro_lib:ok_pipe(FL).
%% @doc Add partitions to transaction.
-spec add_partitions_to_txn(txn_ctx(), [{topic(), partition()}],
#{timeout => timeout()}) -> ok | {error, any()}.
add_partitions_to_txn(TxnCtx, TPL, Opts) ->
Connection = maps:get(connection, TxnCtx),
Timeout = maps:get(timeout, Opts, ?DEFAULT_TIMEOUT),
Req = kpro_req_lib:add_partitions_to_txn(TxnCtx, TPL),
FL =
[ fun() -> kpro_connection:request_sync(Connection, Req, Timeout) end
, fun(#kpro_rsp{msg = Body}) -> parse_add_partitions_rsp(Body) end
],
kpro_lib:ok_pipe(FL).
%% @doc Send consumer group ID to transaction coordinator.
%% Transaction coordinator will map the group ID to its internal
%% partition number in __consumer_offsets topic.
%% then add that topic-partition to transaction like what the
%% `add_partitions_to_txn' API would achieve.
-spec add_offsets_to_txn(txn_ctx(), group_id(),
#{timeout => timeout()}) -> ok | {error, any()}.
add_offsets_to_txn(TxnCtx, CgId, Opts) ->
Connection = maps:get(connection, TxnCtx),
Timeout = maps:get(timeout, Opts, ?DEFAULT_TIMEOUT),
Req = kpro_req_lib:add_offsets_to_txn(TxnCtx, CgId),
FL =
[ fun() -> kpro_connection:request_sync(Connection, Req, Timeout) end
, fun(#kpro_rsp{msg = #{error_code := EC}}) -> ok_or_error_tuple(EC) end
],
kpro_lib:ok_pipe(FL).
-spec txn_offset_commit(connection(), group_id(), txn_ctx(),
offsets_to_commit(),
#{timeout => timeout(),
user_data => binary()}) -> ok | {error, any()}.
txn_offset_commit(GrpConnection, GrpId, TxnCtx, Offsets, Opts) ->
Timeout = maps:get(timeout, Opts, ?DEFAULT_TIMEOUT),
UserData = maps:get(user_data, Opts, <<>>),
Req = kpro_req_lib:txn_offset_commit(GrpId, TxnCtx, Offsets, UserData),
FL =
[ fun() -> kpro_connection:request_sync(GrpConnection, Req, Timeout) end
, fun(#kpro_rsp{msg = Body}) -> parse_txn_offset_commit_rsp(Body) end
],
kpro_lib:ok_pipe(FL).
%%%_* Internals ================================================================
parse_txn_offset_commit_rsp(#{topics := Topics}) ->
FP = fun(#{ partition_index := Partition
, error_code := EC
}, Acc) ->
case ?no_error =:= EC of
true -> Acc;
false -> [{Partition, EC} | Acc]
end
end,
FT = fun(#{ name := Topic
, partitions := Partitions
}, Acc) ->
PartitionErrs = lists:foldl(FP, [], Partitions),
[{{Topic, Partition}, EC} || {Partition, EC} <- PartitionErrs] ++ Acc
end,
case lists:foldl(FT, [], Topics) of
[] -> ok;
Errors -> {error, Errors}
end.
parse_add_partitions_rsp(#{errors := Errors0}) ->
Errors =
lists:flatten(
lists:map(
fun(#{topic := Topic, partition_errors := PartitionErrors}) ->
lists:map(
fun(#{partition := Partition, error_code := ErrorCode}) ->
case ErrorCode =:= ?no_error of
true -> [];
false -> {Topic, Partition, ErrorCode}
end
end, PartitionErrors)
end, Errors0)),
case Errors =:= [] of
true -> ok;
_ -> {error, Errors}
end.
make_txn_ctx(Connection, TxnId,
#{ error_code := ?no_error
, producer_id := ProducerId
, producer_epoch := ProducerEpoch
}) ->
{ok, #{ connection => Connection
, transactional_id => TxnId
, producer_id => ProducerId
, producer_epoch => ProducerEpoch
}};
make_txn_ctx(_Connection, _TxnId, #{error_code := EC}) ->
{error, EC}.
ok_or_error_tuple(?no_error) -> ok;
ok_or_error_tuple(EC) -> {error, EC}.
%%%_* Emacs ====================================================================
%%% Local Variables:
%%% allout-layout: t
%%% erlang-indent-level: 2
%%% End: