Packages
kafka_protocol
4.1.4
4.3.4
4.3.3
4.3.2
4.3.1
4.3.0
4.2.9
4.2.8
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.10
4.1.9
4.1.8
4.1.7
4.1.6
4.1.5
4.1.4
4.1.3
4.1.2
4.1.1
4.1.0
4.0.3
4.0.2
4.0.1
3.0.1
3.0.0
2.4.1
2.3.6
2.3.5
2.3.4
2.3.3
2.3.2
2.3.1
2.3.0
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.2
2.1.1
2.1.0
2.0.1
2.0.0
1.1.3
1.1.2
1.1.1
1.1.0
1.0.0
0.9.2
0.9.1
0.9.0
0.8.0
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.0
0.4.0
0.3.2
0.3.1
0.2.3
Kafka protocol library for Erlang/Elixir
Current section
Files
Jump to
Current section
Files
src/kpro.erl
%%% Copyright (c) 2014-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).
-export([provide_compression/1]).
%% Connection
-export([ close_connection/1
, connect/2
, connect_any/2
, connect_controller/2
, connect_controller/3
, connect_coordinator/3
, connect_partition_leader/4
, connect_partition_leader/5
]).
%% Broker properties
-export([ discover_coordinator/4
, discover_partition_leader/4
, get_api_versions/1
, get_api_vsn_range/2
]).
%% Primitive RPCs
-export([ request_sync/3
, request_async/2
, send/2
]).
%% Transactional RPCs
-export([ txn_abort/1
, txn_abort/2
, txn_commit/1
, txn_commit/2
, txn_init_ctx/2
, txn_init_ctx/3
, txn_offset_commit/4
, txn_offset_commit/5
, txn_send_cg/2
, txn_send_cg/3
, txn_send_partitions/2
, txn_send_partitions/3
]).
%% request makers/response parsers
%% there are more in `kpro_req_lib'
-export([ encode_request/3
, encode_batch/3
, make_request/3
, produce_api_vsn_to_magic_vsn/1
]).
%% misc
-export([ decode_batches/1
, find/2
, find/3
, parse_endpoints/1
, parse_endpoints/2
]).
-export_type([ api/0
, batch/0
, batch_decode_result/0
, batch_input/0
, batch_meta/0
, bytes/0
, client_id/0
, compress_option/0
, connection/0
, conn_config/0
, coordinator_type/0
, corr_id/0
, count/0
, endpoint/0
, error_code/0
, fetch_opts/0
, field_name/0
, field_value/0
, group_id/0
, headers/0
, header_key/0
, header_val/0
, hostname/0
, incomplete_batch/0
, int8/0
, int16/0
, int32/0
, int64/0
, isolation_level/0
, key/0
, leader_epoch/0
, magic/0
, message/0
, msg_input/0
, msg_ts/0
, offset/0
, offsets_to_commit/0
, partition/0
, portnum/0
, primitive/0
, primitive_type/0
, produce_opts/0
, producer_epoch/0
, producer_id/0
, protocol/0
, req/0
, required_acks/0
, rsp/0
, schema/0
, seqno/0
, stack/0
, str/0
, struct/0
, struct_schema/0
, timestamp_type/0
, transactional_id/0
, txn_ctx/0
, topic/0
, value/0
, vsn/0
, vsn_range/0
, vsn_ranges/0
, wait/0
]).
-include("kpro_private.hrl").
-type int8() :: -128..127.
-type int16() :: -32768..32767.
-type int32() :: -2147483648..2147483647.
-type int64() :: -9223372036854775808..9223372036854775807.
-type str() :: ?null | string() | binary().
-type bytes() :: binary().
-type error_code() :: int16() | atom().
-type msg_ts() :: int64().
-type producer_id() :: int64().
-type magic() :: 0..2.
-type client_id() :: binary().
-type hostname() :: binary() | inet:hostname() | inet:ip_address().
-type portnum() :: non_neg_integer().
-type endpoint() :: {hostname(), portnum()}.
-type corr_id() :: int32().
-type topic() :: binary().
-type partition() :: int32().
-type offset() :: int64().
-type header_key() :: binary().
-type header_val() :: binary().
-type headers() :: [{header_key(), header_val()}].
-type seqno() :: integer().
%% optional args to make produce request
-type produce_opts() :: #{ compression => compress_option() % common
, required_acks => required_acks() % common
, ack_timeout => wait() % common
, txn_ctx => txn_ctx() % txn only
, first_sequence => seqno() % txn only
}.
-type key() :: binary().
-type value() :: binary().
-type msg_input() :: #{ headers => headers() % default to []
, ts => msg_ts() % default to current ts
, key => key() % default to <<"">>
, value => value() % default to <<"">>
}.
-type batch_input() :: [msg_input()].
-type incomplete_batch() :: ?incomplete_batch(int32()).
-type message() :: #kafka_message{}.
-type vsn() :: non_neg_integer().
-type count() :: non_neg_integer().
-type wait() :: non_neg_integer().
-type required_acks() :: -1..1 | all_isr | none | leader_only.
-type primitive() :: integer() | string() | binary() | atom().
-type field_tag() :: integer().
-type field_name() :: atom() | field_tag().
-type field_value() :: primitive() | [primitive()] | struct() | [struct()].
-type struct() :: #{field_name() => field_value()}
| [{field_name(), field_value()}].
-type api() :: atom().
-type req() :: #kpro_req{}.
-type rsp() :: #kpro_rsp{}.
-type compress_option() :: ?no_compression
| ?gzip
| ?snappy
| ?lz4.
-type timestamp_type() :: undefined | create | append.
-type primitive_type() :: boolean
| int8
| int16
| int32
| int64
| varint
| string
| nullable_string
| bytes
| records
| unsigned_varint.
-type decode_fun() :: fun((binary()) -> {field_value(), binary()}).
-type struct_schema() :: [{field_name(), schema()}].
-type schema() :: primitive_type()
| struct_schema()
| {array, schema()}
| decode_fun(). %% caller defined decoder
-type stack() :: [{api(), vsn()} | field_name()]. %% encode / decode stack
-type isolation_level() :: read_committed | read_uncommitted.
-type connection() :: kpro_connection:connection().
-type conn_config() :: kpro_connection:config().
-type vsn_range() :: {vsn(), vsn()}.
-type vsn_ranges() :: #{api() => vsn_range()}.
-type protocol() :: plaintext | ssl | sasl_plaintext | sasl_ssl.
-type coordinator_type() :: group | txn.
-type group_id() :: binary().
-type transactional_id() :: binary().
-type producer_epoch() :: int16().
-type leader_epoch() :: int32().
-type txn_ctx() :: #{ connection => connection()
, transactional_id => transactional_id()
, producer_id => producer_id()
, producer_epoch => producer_id()
}.
-type batch_meta_key() ::
is_transaction % if this message was produced in a transaction
| is_control % for read_uncommitted clients
| last_offset % client won't have to do lists:last(Messages)
| max_ts % client don't have to scan the messages for max
| producer_id. % it can be referenced by a future fetch response in
% its aborted_transactions field
-type batch_meta_val() :: boolean() | atom() | integer().
-type batch_meta() :: ?KPRO_NO_BATCH_META %% magic 0-1
| #{batch_meta_key() => batch_meta_val()}.
-type batch() :: {batch_meta(), [message()]}.
-type batch_decode_result() :: ?incomplete_batch(int32()) | [batch()].
%% offset or offset + associated user-data to commit
-type offset_ud() :: offset() %% no user data
| {offset(), binary()}.
-type offsets_to_commit() :: #{{topic(), partition()} => offset_ud()}
| [{{topic(), partition()}, offset_ud()}].
-type fetch_opts() :: kpro_req_lib:fetch_opts().
%% All versions of kafka messages (records) share the same header:
%% Offset => int64
%% Length => int32
%% We need to at least fetch 12 bytes in order to fetch:
%% - One complete non-compressed message when it is magic v0-1
%% - Or one complete compressed batch when it is magic v0-1
%% (magic v0-1 compressed batch is embedded in a wrapper message in a
%% 'recursive' manner)
%% - Or one omplete batch when it is a magic v2 batch
-define(BATCH_LEADING_BYTES, 12).
%%%_* APIs =====================================================================
%% @doc Set snappy or lz4 compression modules.
%% This should override the default usage of `snappyer' and `lz4b_frame'.
-spec provide_compression([{?snappy | ?lz4, module()}]) -> ok.
provide_compression(Modules) -> kpro_compress:provide(Modules).
%% Get batch magic version from produce API version.
-spec produce_api_vsn_to_magic_vsn(vsn()) -> magic().
produce_api_vsn_to_magic_vsn(V) -> kpro_lib:produce_api_vsn_to_magic_vsn(V).
%% @see kpro_lib:parse_endpoints/2.
parse_endpoints(String) ->
parse_endpoints(undefined, String).
%% @see kpro_lib:parse_endpoints/2.
-spec parse_endpoints(protocol() | undefined, string()) -> [endpoint()].
parse_endpoints(Protocol, String) ->
kpro_lib:parse_endpoints(Protocol, String).
%% @doc Help function to make a request. See also kpro_req_lib for more help
%% functions.
-spec make_request(api(), vsn(), struct()) -> req().
make_request(Api, Vsn, Fields) ->
kpro_req_lib:make(Api, Vsn, Fields).
%% @doc Encode request to byte stream.
-spec encode_request(client_id(), corr_id(), req()) -> iodata().
encode_request(ClientId, CorrId, Req) ->
kpro_req_lib:encode(ClientId, CorrId, Req).
%% @doc Encode message batch for produce request.
-spec encode_batch(magic(), batch_input(), compress_option()) -> binary().
encode_batch(Magic, Batch, Compression) ->
iolist_to_binary(kpro_batch:encode(Magic, Batch, Compression)).
%% @doc The message-set is not decoded upon receiving (in connection process).
%% It is passed as binary to the consumer process and decoded there.
%% Return `?incomplete_batch(ExpectedSize)' if the fetch size is not big
%% enough for even one single message. Otherwise return `[{Meta, Messages}]'
%% where `Meta' is either `?KPRO_NO_BATCH_META' for magic-version 0-1 or
%% `kpro:batch_meta()' for magic-version 2 or above.
-spec decode_batches(binary()) -> batch_decode_result().
decode_batches(<<>>) -> []; %% no data
decode_batches(<<_:64/?INT, L:32, T/binary>> = Bin) when size(T) >= L ->
kpro_batch:decode(Bin);
decode_batches(<<_:64/?INT, L:32, _T/binary>>) ->
%% not enough to decode one single message for magic v0-1
?incomplete_batch(L + ?BATCH_LEADING_BYTES);
decode_batches(_) ->
%% not enough to even get the size header
?incomplete_batch(?BATCH_LEADING_BYTES).
%% @doc Send a request, wait for response.
%% Immediately return 'ok' if it is a produce request with `required_acks=0'.
-spec request_sync(pid(), req(), timeout()) ->
ok | {ok, rsp()} | {error, any()}.
request_sync(ConnectionPid, Request, Timeout) ->
kpro_connection:request_sync(ConnectionPid, Request, Timeout).
%% @doc Send a request without waiting for reply.
%% Reply will be delivered to caller in the future when response message is
%% received from kafka.
%% The message to expect should have spec `{msg, connection(), #kpro_rsp{}}'
%% where `#kpro_rsp.ref' matches the sent `Request#kpro_req.ref'.
%% When it is a produce request with `required_acks=0', there will be no reply.
-spec request_async(connection(), req()) -> ok | {error, any()}.
request_async(ConnectionPid, Request) ->
kpro_connection:request_async(ConnectionPid, Request).
%% @doc Same as @link request_async/2.
%% Only that the message towards connection process is a cast (not a call).
%% Always return 'ok'.
-spec send(connection(), req()) -> ok.
send(ConnectionPid, Request) when is_pid(ConnectionPid) ->
kpro_connection:send(ConnectionPid, Request).
%% @doc Connect to the given endpoint.
%% NOTE: Connection process is linked to caller unless `nolink => true'
%% is set in connection config
-spec connect(endpoint(), conn_config()) -> {ok, connection()} | {error, any()}.
connect(Endpoint, ConnConfig) ->
case connect_any([Endpoint], ConnConfig) of
{ok, Connection} -> {ok, Connection};
{error, [{Endpoint, Reason}]} -> {error, Reason}
end.
%% @doc Connect to any of the endpoints in the given list.
%% NOTE: Connection process is linked to caller unless `nolink => true'
%% is set in connection config
-spec connect_any([endpoint()], conn_config()) ->
{ok, connection()} | {error, any()}.
connect_any(Endpoints, ConnConfig) ->
kpro_brokers:connect_any(Endpoints, ConnConfig).
%% @doc Sotp connection process.
-spec close_connection(connection()) -> ok.
close_connection(Connection) ->
kpro_connection:stop(Connection).
%% @doc Connect partition leader.
%% If the fist arg is not an already established metadata connection
%% but a bootstrapping endpoint list, this function will first try to
%% establish a temp connection to any of the bootstrapping endpoints.
%% Then send metadata request to discover partition leader broker
%% Finally connect to the leader broker.
%% NOTE: Connection process is linked to caller unless `nolink => true'
%% is set in connection config.
-spec connect_partition_leader(connection() | [endpoint()], conn_config(),
topic(), partition()) ->
{ok, connection()} | {error, any()}.
connect_partition_leader(Bootstrap, ConnConfig, Topic, Partition) ->
connect_partition_leader(Bootstrap, ConnConfig, Topic, Partition, #{}).
%% @doc Connect partition leader.
-spec connect_partition_leader(connection() | [endpoint()], conn_config(),
topic(), partition(), #{timeout => timeout()}) ->
{ok, connection()} | {error, any()}.
connect_partition_leader(Bootstrap, ConnConfig, Topic, Partition, Opts) ->
kpro_brokers:connect_partition_leader(Bootstrap, ConnConfig,
Topic, Partition, Opts).
%% @doc Discover partition leader broker endpoint.
%% An implicit step performed in `connect_partition_leader'.
%% This is useful when the caller wants to re-use already established
%% towards the discovered endpoint.
-spec discover_partition_leader(connection(), topic(),partition(),
timeout()) -> {ok, endpoint()} | {error, any()}.
discover_partition_leader(Connection, Topic, Partition, Timeout) ->
kpro_brokers:discover_partition_leader(Connection, Topic, Partition, Timeout).
%% @see connect_controller/3.
-spec connect_controller(connection() | [endpoint()], conn_config()) ->
{ok, connection()} | {error, any()}.
connect_controller(Bootstrap, ConnConfig) ->
connect_controller(Bootstrap, ConnConfig, #{}).
%% @doc Connect to the controller broker of the kafka cluster.
-spec connect_controller(connection() | [endpoint()], conn_config(),
#{timeout => timeout()}) ->
{ok, connection()} | {error, any()}.
connect_controller(Bootstrap, ConnConfig, Opts) ->
kpro_brokers:connect_controller(Bootstrap, ConnConfig, Opts).
%% @doc Connect to group or transaction coordinator.
%% If the first arg is not a connection pid but a list of bootstrapping
%% endpoints, it will first try to connect to any of the nodes
%% NOTE: 'txn' type only applicable to kafka 0.11 or later
-spec connect_coordinator(connection() | [endpoint()], conn_config(),
#{ type => coordinator_type()
, id => binary()
, timeout => timeout()
}) -> {ok, connection()} | {error, any()}.
connect_coordinator(Bootstrap, ConnConfig, Args) ->
kpro_brokers:connect_coordinator(Bootstrap, ConnConfig, Args).
%% @doc Discover group or transactional coordinator.
%% An implicit step performed in `connect_coordinator'.
%% This is useful when the caller wants to re-use already established
%% towards the discovered endpoint.
-spec discover_coordinator(connection(), coordinator_type(),
group_id() | transactional_id(), timeout()) ->
{ok, endpoint()} | {error, any()}.
discover_coordinator(Connection, Type, Id, Timeout) ->
kpro_brokers:discover_coordinator(Connection, Type, Id, Timeout).
%% @doc Qury API versions using the given `kpro_connection' pid.
-spec get_api_versions(connection()) ->
{ok, vsn_ranges()} | {error, any()}.
get_api_versions(Connection) ->
kpro_brokers:get_api_versions(Connection).
%% @doc Get version range for the given API.
-spec get_api_vsn_range(connection(), api()) ->
{ok, vsn_range()} | {error, any()}.
get_api_vsn_range(Connection, API) ->
kpro_brokers:get_api_vsn_range(Connection, API).
%% @see kpro_lib:find/2.
-spec find(field_name(), struct()) -> field_value().
find(Field, Struct) -> kpro_lib:find(Field, Struct).
%% @see kpro_lib:find/3
-spec find(field_name(), struct(), field_value()) -> field_value().
find(Field, Struct, Default) -> kpro_lib:find(Field, Struct, Default).
%%%_* Transactional APIs =======================================================
%% @doc Initialize a transaction context, the connection should be established
%% towards transactional coordinator broker.
%% By default the request timeout `timeout' is 5 seconds. This is for client
%% to abort waiting for response and consider it an error `{error, timeout}'.
%% Transaction timeout `txn_timeout' is `-1' by default, which means use kafka
%% broker setting. The default timeout in kafka broker is 1 minute.
%% `txn_timeout' is for kafka transaction coordinator to abort transaction if
%% a transaction did not end (commit or abort) in time.
-spec txn_init_ctx(connection(), transactional_id()) ->
{ok, txn_ctx()} | {error, any()}.
txn_init_ctx(Connection, TxnId) ->
txn_init_ctx(Connection, TxnId, #{}).
%% @doc Initialize a transaction context, the connection should be established
%% towards transactional coordinator broker.
-spec txn_init_ctx(connection(), transactional_id(),
#{ timeout => timeout()
, txn_timeout => pos_integer()
}) -> {ok, txn_ctx()} | {error, any()}.
txn_init_ctx(Connection, TxnId, Opts) ->
kpro_txn_lib:txn_init_ctx(Connection, TxnId, Opts).
%% @doc Abort transaction.
-spec txn_abort(txn_ctx()) -> ok | {errory, any()}.
txn_abort(TxnCtx) ->
txn_abort(TxnCtx, #{}).
%% @doc Abort transaction.
-spec txn_abort(txn_ctx(), #{timeout => timeout()}) -> ok | {error, any()}.
txn_abort(TxnCtx, Opts) ->
kpro_txn_lib:end_txn(TxnCtx, abort, Opts).
%% @see txn_send_partitions/3
-spec txn_send_partitions(txn_ctx(), [{topic(), partition()}]) ->
ok | {error, any()}.
txn_send_partitions(TxnCtx, TPL) ->
txn_send_partitions(TxnCtx, TPL, #{}).
%% @doc Add partitions to transaction.
-spec txn_send_partitions(txn_ctx(), [{topic(), partition()}],
#{timeout => timeout()}) -> ok | {error, any()}.
txn_send_partitions(TxnCtx, TPL, Opts) ->
kpro_txn_lib:add_partitions_to_txn(TxnCtx, TPL, Opts).
%% @doc see txn_offset_commit/5
-spec txn_offset_commit(connection(), group_id(), txn_ctx(),
offsets_to_commit()) -> ok | {error, any()}.
txn_offset_commit(GrpConnection, GrpId, TxnCtx, Offsets) ->
txn_offset_commit(GrpConnection, GrpId, TxnCtx, Offsets, #{}).
%% @doc Send transactional offset commit request to group coordinator.
%% `user_data' in `Opts' is used as default user-data in offset commit request
%% if there is no user-data associated with offset in `offsets_to_commit()'
-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) ->
kpro_txn_lib:txn_offset_commit(GrpConnection, GrpId, TxnCtx, Offsets, Opts).
%% @see txn_send_cg/3
txn_send_cg(TxnCtx, CgId) ->
txn_send_cg(TxnCtx, CgId, #{}).
%% @doc Add consumed offsets to transaction.
%% This is done by sending the consumer group ID to transaction coordinator.
-spec txn_send_cg(txn_ctx(), group_id(), #{timeout => timeout()}) ->
ok | {error, any()}.
txn_send_cg(TxCtx, CgId, Opts) ->
kpro_txn_lib:add_offsets_to_txn(TxCtx, CgId, Opts).
%% @doc Commit transaction.
-spec txn_commit(txn_ctx()) -> ok | {error, any()}.
txn_commit(TxnCtx) ->
txn_commit(TxnCtx, #{}).
%% @doc Commit transaction.
-spec txn_commit(txn_ctx(), #{timeout => timeout()}) -> ok | {error, any()}.
txn_commit(TxnCtx, Opts) ->
kpro_txn_lib:end_txn(TxnCtx, commit, Opts).
%%%_* Emacs ====================================================================
%%% Local Variables:
%%% allout-layout: t
%%% erlang-indent-level: 2
%%% End: