%%% 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 Stop 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: