Packages
kafka_protocol
2.2.2
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_req_lib.erl
%%% Copyright (c) 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.
%%%
%% Help functions to make requests
-module(kpro_req_lib).
-export([ list_offsets/4
, list_offsets/5
]).
-export([ fetch/5
]).
-export([ produce/4
, produce/5
]).
-export([ metadata/2
, metadata/3
]).
-export([ add_offsets_to_txn/2
, add_partitions_to_txn/2
, end_txn/2
, txn_offset_commit/4
]).
-export([ create_partitions/3
, create_topics/3
, delete_topics/3
]).
-export([ encode/3
, make/3
]).
-export_type([ fetch_opts/0
]).
-include("kpro_private.hrl").
-define(DEFAULT_ACK_TIMEOUT, 10000).
-define(FIELD_ENCODE_ERROR(Reason, EncoderStack),
{field_encode_error, Reason, EncoderStack}).
-type vsn() :: kpro:vsn().
-type topic() :: kpro:topic().
-type req() :: kpro:req().
-type msg_ts() :: kpro:msg_ts().
-type isolation_level() :: kpro:isolation_level().
-type count() :: kpro:count().
-type batch_input() :: kpro:batch_input().
-type produce_opts() :: kpro:produce_opts().
-type txn_ctx() :: kpro:txn_ctx().
-type client_id() :: kpro:client_id().
-type corr_id() :: kpro:corr_id().
-type api() :: kpro:api().
-type struct() :: kpro:struct().
-type offset() :: kpro:offset().
-type wait() :: kpro:wait().
-type partition() :: kpro:partition().
-type group_id() :: kpro:group_id().
-type offsets_to_commit() :: kpro:offsets_to_commit().
-type fetch_opts() :: #{ max_wait_time => wait()
, min_bytes => count()
, max_bytes => count()
, isolation_level => isolation_level()
, session_id => kpro:int32()
, epoch => kpro:int32()
}.
%% @doc Make a `metadata' request
-spec metadata(vsn(), all | [topic()]) -> req().
metadata(Vsn, Topics) ->
metadata(Vsn, Topics, _IsAutoCreateAllowed = false).
%% @doc Make a `metadata' request
-spec metadata(vsn(), all | [topic()], boolean()) -> req().
metadata(Vsn, [], IsAutoCreateAllowed) ->
metadata(Vsn, all, IsAutoCreateAllowed);
metadata(Vsn, Topics0, IsAutoCreateAllowed) ->
Topics = case Topics0 of
all when Vsn =:= 0 -> [];
all -> ?kpro_null;
List -> List
end,
make(metadata, Vsn, [{topics, Topics},
{allow_auto_topic_creation, IsAutoCreateAllowed}]).
%% @doc Help function to contruct a `list_offset' request
%% against one single topic-partition.
-spec list_offsets(vsn(), topic(), partition(), msg_ts()) -> req().
list_offsets(Vsn, Topic, Partition, Time) ->
list_offsets(Vsn, Topic, Partition, Time, ?kpro_read_committed).
%% @doc Help function to contruct a `list_offset' request against one single
%% topic-partition. In transactional mode,
%% set `IsolationLevel = ?kpro_read_uncommitted' to list uncommitted offsets.
-spec list_offsets(vsn(), topic(), partition(),
latest | earliest | msg_ts(),
isolation_level()) -> req().
list_offsets(Vsn, Topic, Partition, latest, IsolationLevel) ->
list_offsets(Vsn, Topic, Partition, -1, IsolationLevel);
list_offsets(Vsn, Topic, Partition, earliest, IsolationLevel) ->
list_offsets(Vsn, Topic, Partition, -2, IsolationLevel);
list_offsets(Vsn, Topic, Partition, Time, IsolationLevel) ->
PartitionFields =
case Vsn of
0 ->
[{partition, Partition},
{timestamp, Time},
{max_num_offsets, 1}];
_ ->
%% max_num_offsets is removed since version 1
[{partition, Partition},
{timestamp, Time}
]
end,
Fields =
[{replica_id, ?KPRO_REPLICA_ID},
{isolation_level, IsolationLevel},
{topics, [ [{topic, Topic},
{partitions, [ PartitionFields ]}]
]}
],
make(list_offsets, Vsn, Fields).
%% @doc Help function to construct a `fetch' request
%% against one single topic-partition. In transactional mode, set
%% `IsolationLevel = kpro_read_uncommitted' to fetch uncommitted messages.
-spec fetch(vsn(), topic(), partition(), offset(), fetch_opts()) -> req().
fetch(Vsn, Topic, Partition, Offset, Opts) ->
MaxWaitTime = maps:get(max_wait_time, Opts, timer:seconds(1)),
MinBytes = maps:get(min_bytes, Opts, 0),
MaxBytes = maps:get(max_bytes, Opts, 1 bsl 20), %% 1M
IsolationLevel = maps:get(isolation_level, Opts, ?kpro_read_committed),
SessionID = maps:get(session_id, Opts, 0),
Epoch = maps:get(epoch, Opts, -1),
Fields =
[{replica_id, ?KPRO_REPLICA_ID},
{max_wait_time, MaxWaitTime},
{max_bytes, MaxBytes},
{min_bytes, MinBytes},
{isolation_level, IsolationLevel},
{session_id, SessionID},
{epoch, Epoch},
{topics,[[{topic, Topic},
{partitions,
[[{partition, Partition},
{fetch_offset, Offset},
{max_bytes, MaxBytes},
{log_start_offset, -1} %% irelevant to clients
]]}]]},
% we alwyas fetch from one single topic-partition
% never need to forget any
{forgetten_topics_data, []}
],
make(fetch, Vsn, Fields).
%% @doc Help function to construct a produce request.
produce(Vsn, Topic, Partition, Batch) ->
produce(Vsn, Topic, Partition, Batch, #{}).
%% @doc Help function to construct a produce request.
%% By default, it constructs a non-transactional produce request.
%% For transactional produce requests, below conditions should be met.
%% 1. `Batch' arg must be be a `[map()]' to indicate magic v2,
%% for example: `[#{key => Key, value => Value, ts => Ts}]'.
%% Current system time will be taken if `ts' is missing in batch input.
%% It may also be `binary()' if user choose to encode a batch beforehand.
%% This could be helpful when a large batch can be encoded in another
%% process, so it may pass large binary instead of list between processes.
%% 2. `first_sequence' must exist in `Opts'.
%% It should be the sequence number for the fist message in batch.
%% Maintained by producer, sequence numbers should start from zero and be
%% monotonically increasing, with one sequence number per topic-partition.
%% 3. `txn_ctx' (which is of spec `kpro:txn_ctx()') must exist in `Opts'
-spec produce(vsn(), topic(), partition(),
binary() | batch_input(), produce_opts()) -> req().
produce(Vsn, Topic, Partition, Batch, Opts) ->
RequiredAcks = required_acks(maps:get(required_acks, Opts, all_isr)),
Compression = maps:get(compression, Opts, ?no_compression),
AckTimeout = maps:get(ack_timeout, Opts, ?DEFAULT_ACK_TIMEOUT),
TxnCtx = maps:get(txn_ctx, Opts, false),
FirstSequence = maps:get(first_sequence, Opts, -1),
MagicV = kpro_lib:produce_api_vsn_to_magic_vsn(Vsn),
EncodedBatch =
case is_binary(Batch) of
true ->
%% already encoded non-transactional batch
Batch;
false when TxnCtx =:= false ->
%% non-transactional batch
kpro_batch:encode(MagicV, Batch, Compression);
false ->
%% transactional batch
true = FirstSequence >= 0, %% assert
kpro_batch:encode_tx(Batch, Compression, FirstSequence, TxnCtx)
end,
Fields =
[{transactional_id, transactional_id(TxnCtx)},
{acks, RequiredAcks},
{timeout, AckTimeout},
{topic_data, [[{topic, Topic},
{data, [[{partition, Partition},
{record_set, EncodedBatch}
]]}
]]}
],
Req = make(produce, Vsn, Fields),
Req#kpro_req{no_ack = RequiredAcks =:= 0}.
%% @doc Make `end_txn' request.
-spec end_txn(txn_ctx(), commit | abort) -> req().
end_txn(TxnCtx, CommitOrAbort) ->
Result = case CommitOrAbort of
commit -> true;
abort -> false
end,
Body = TxnCtx#{transaction_result => Result},
make(end_txn, _Vsn = 0, Body).
%% @doc Make `add_partitions_to_txn' request.
-spec add_partitions_to_txn(txn_ctx(), [{topic(), partition()}]) -> req().
add_partitions_to_txn(TxnCtx, TopicPartitionList) ->
Grouped =
lists:foldl(
fun({Topic, Partition}, Acc) ->
kpro_lib:update_map(
Topic, fun(PL) -> [Partition | PL] end,
[Partition], Acc)
end, #{}, TopicPartitionList),
Body = TxnCtx#{topics => tp_map_to_array(Grouped)},
make(add_partitions_to_txn, _Vsn = 0, Body).
%% @doc Make a `txn_offset_commit' request.
-spec txn_offset_commit(group_id(), txn_ctx(),
offsets_to_commit(), binary()) -> req().
txn_offset_commit(GrpId, TxnCtx, Offsets, UserData) when is_map(Offsets) ->
txn_offset_commit(GrpId, TxnCtx, maps:to_list(Offsets), UserData);
txn_offset_commit(GrpId, TxnCtx, Offsets, DefaultUserData) ->
All =
lists:foldl(
fun({{Topic, Partition}, OffsetUd}, Acc) ->
{Offset, UserData} =
case OffsetUd of
{O, D} -> {O, D};
O -> {O, DefaultUserData}
end,
PD = #{ partition => Partition
, offset => Offset
, metadata => UserData
},
kpro_lib:update_map(Topic, fun(PDL) -> [PD | PDL] end, [PD], Acc)
end, #{}, Offsets),
Body = TxnCtx#{topics => tp_map_to_array(All), group_id => GrpId},
make(txn_offset_commit, _Vsn = 0, Body).
%% @doc Make `add_offsets_to_txn' request.
-spec add_offsets_to_txn(txn_ctx(), group_id()) -> req().
add_offsets_to_txn(TxnCtx, CgId) ->
Body = TxnCtx#{group_id => CgId},
make(add_offsets_to_txn, _Vsn = 0, Body).
%% @doc Make `create_topics' request.
%% if 0 is given as `timeout' option the request will trigger a creation
%% but return immediately.
%% `validate_only' option is only relavent when the API version is
%% greater than 0.
-spec create_topics(vsn(), [Topics :: kpro:struct()],
#{timeout => kpro:int32(),
validate_only => boolean()}) -> req().
create_topics(Vsn, Topics, Opts) ->
Timeout = maps:get(timeout, Opts, 0),
ValidateOnly = maps:get(validate_only, Opts, false),
Body = #{ create_topic_requests => Topics
, timeout => Timeout
, validate_only => ValidateOnly
},
make(create_topics, Vsn, Body).
%% @doc Make a `create_partitions' request.
-spec create_partitions(vsn(), [Topics :: kpro:struct()],
#{timeout => kpro:int32(),
validate_only => boolean()}) -> req().
create_partitions(Vsn, Topics, Opts) ->
Timeout = maps:get(timeout, Opts, 0),
ValidateOnly = maps:get(validate_only, Opts, false),
Body = #{ topic_partitions => Topics
, timeout => Timeout
, validate_only => ValidateOnly
},
make(create_partitions, Vsn, Body).
%% @doc Make `delete_topics' request.
-spec delete_topics(vsn(), [topic()], #{timeout => kpro:int32()}) -> req().
delete_topics(Vsn, Topics, Opts) ->
Timeout = maps:get(timeout, Opts, 0),
Body = #{ topics => Topics
, timeout => Timeout
},
make(delete_topics, Vsn, Body).
%% @doc Help function to make a request body.
-spec make(api(), vsn(), struct()) -> req().
make(API, Vsn, Fields) ->
ok = assert_known_api_and_vsn(API, Vsn),
#kpro_req{ api = API
, vsn = Vsn
, msg = encode_struct(API, Vsn, Fields)
, ref = make_ref()
}.
%% @doc Encode a request to bytes that can be sent on wire.
-spec encode(client_id(), corr_id(), req()) -> iodata().
encode(ClientName, CorrId, Req) ->
#kpro_req{api = API, vsn = Vsn, msg = Msg} = Req,
ApiKey = kpro_schema:api_key(API),
[ encode(int16, ApiKey)
, encode(int16, Vsn)
, encode(int32, CorrId)
, encode(string, ClientName)
, encode_struct(API, Vsn, Msg)
].
%%%_* Internal functions =======================================================
%% Turn #{Topic => PartitionsArray} into
%% [#{topic => Topic, partitions => PartitionsArray}]
tp_map_to_array(TPM) ->
maps:fold(
fun(Topic, Partitions, Acc) ->
[ #{ topic => Topic
, partitions => Partitions
} | Acc ]
end, [], TPM).
required_acks(none) -> 0;
required_acks(leader_only) -> 1;
required_acks(all_isr) -> -1;
required_acks(I) when I >= -1 andalso I =< 1 -> I.
encode_struct(_API, _Vsn, Bin) when is_binary(Bin) -> Bin;
encode_struct(API, Vsn, Fields) ->
Schema = kpro_lib:get_req_schema(API, Vsn),
try
bin(enc_struct(Schema, Fields, [{API, Vsn}]))
catch
throw : ?FIELD_ENCODE_ERROR(Reason, Stack) ?BIND_STACKTRACE(Trace) ->
?GET_STACKTRACE(Trace),
erlang:raise(error, {Reason, Stack, Fields}, Trace)
end.
%% Encode struct.
enc_struct([], _Values, _Stack) -> [];
enc_struct([{Name, FieldSc} | Schema], Values, Stack) ->
NewStack = [Name | Stack],
Value0 =
try
kpro_lib:find(Name, Values)
catch
error : {no_such_field, _} ->
erlang:error({field_missing, [ {stack, lists:reverse(NewStack)}
, {input, Values}]})
end,
Value = translate(NewStack, Value0),
[ enc_struct_field(FieldSc, Value, NewStack)
| enc_struct(Schema, Values, Stack)
].
enc_struct_field({array, _Schema}, ?null, _Stack) ->
encode(int32, -1); %% NULL
enc_struct_field({array, Schema}, Values, Stack) ->
case is_list(Values) of
true ->
[ encode(int32, length(Values))
| [enc_struct_field(Schema, Value, Stack) || Value <- Values]
];
false ->
erlang:throw(?FIELD_ENCODE_ERROR(not_array, Stack))
end;
enc_struct_field(Schema, Value, Stack) when ?IS_STRUCT(Schema) ->
enc_struct(Schema, Value, Stack);
enc_struct_field(Primitive, Value, Stack) when is_atom(Primitive) ->
try
encode(Primitive, Value)
catch
error : Reason ?BIND_STACKTRACE(Trace) ->
?GET_STACKTRACE(Trace),
erlang:raise(throw, ?FIELD_ENCODE_ERROR(Reason, Stack), Trace)
end.
%% Translate embedded bytes to structs or enum values to enum symbols.
translate([isolation_level | _] , Value) ->
?ISOLATION_LEVEL_INTEGER(Value);
translate([protocol_metadata | _] = Stack, Value) ->
Schema = kpro_lib:get_prelude_schema(cg_protocol_metadata, 0),
bin(enc_struct(Schema, Value, Stack));
translate([member_assignment | _] = Stack, Value) ->
Schema = kpro_lib:get_prelude_schema(cg_memeber_assignment, 0),
bin(enc_struct(Schema, Value, Stack));
translate([coordinator_type | _], Value) ->
case Value of
group -> 0;
txn -> 1;
0 -> 0;
1 -> 1
end;
translate(_Stack, Value) -> Value.
%% Encode prmitives.
encode(Type, Value) -> kpro_lib:encode(Type, Value).
bin(X) -> iolist_to_binary(X).
assert_known_api_and_vsn(API, Vsn) ->
{Min, Max} =
try
kpro_api_vsn:range(API)
catch
error : function_clause ->
erlang:error({unknown_api, API})
end,
case Min =< Vsn andalso Vsn =< Max of
true -> ok;
false ->
erlang:error({unknown_vsn, [ {api, API}
, {vsn, Vsn}
, {known_vsn_range, {Min, Max}}
]})
end.
transactional_id(false) -> ?kpro_null;
transactional_id(#{transactional_id := TxnId}) -> TxnId.
%%%_* Emacs ====================================================================
%%% Local Variables:
%%% allout-layout: t
%%% erlang-indent-level: 2
%%% End: