diff --git a/CHANGELOG.md b/CHANGELOG.md index de7f7056..efe60210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +- 4.2.0 + - Optimize consumer fetch latency. + Introduced the `share_leader_conn` consumer configuration option (default: `false`). + This setting allows users to opt for the previous behavior if preferred (set to `true`). + - 4.1.1 - Upgrade `kafka_protocol` from version 4.1.5 to 4.1.9. diff --git a/src/brod.erl b/src/brod.erl index ee866a5c..d7dbedfb 100644 --- a/src/brod.erl +++ b/src/brod.erl @@ -262,6 +262,7 @@ | {offset_reset_policy, brod_consumer:offset_reset_policy()} | {size_stat_window, non_neg_integer()} | {isolation_level, brod_consumer:isolation_level()} + | {share_leader_conn, boolean()} ]. %% Consumer configuration. %% diff --git a/src/brod_client.erl b/src/brod_client.erl index 8f4aad24..d52f1b38 100644 --- a/src/brod_client.erl +++ b/src/brod_client.erl @@ -1,5 +1,5 @@ -%%% %%% Copyright (c) 2015-2021 Klarna Bank AB (publ) +%%% Copyright (c) 2022-2024 kafka4beam contributors %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ , get_group_coordinator/2 , get_transactional_coordinator/2 , get_leader_connection/3 + , get_bootstrap/1 , get_metadata/2 , get_metadata_safe/2 , get_partitions_count/2 @@ -227,6 +228,10 @@ stop_consumer(Client, TopicName) -> get_leader_connection(Client, Topic, Partition) -> safe_gen_call(Client, {get_leader_connection, Topic, Partition}, infinity). +-spec get_bootstrap(client()) -> {ok, brod:bootstrap()} | {error, any()}. +get_bootstrap(Client) -> + safe_gen_call(Client, get_bootstrap, infinity). + %% @doc Get connection to a kafka broker. %% %% Return already established connection towards the broker, @@ -388,6 +393,10 @@ handle_call({stop_consumer, Topic}, _From, State) -> handle_call({get_leader_connection, Topic, Partition}, _From, State) -> {Result, NewState} = do_get_leader_connection(State, Topic, Partition), {reply, Result, NewState}; +handle_call(get_bootstrap, _From, State) -> + #state{bootstrap_endpoints = Endpoints} = State, + ConnConfig = conn_config(State), + {reply, {ok, {Endpoints, ConnConfig}}, State}; handle_call({get_connection, Host, Port}, _From, State) -> {Result, NewState} = maybe_connect(State, {Host, Port}), {reply, Result, NewState}; diff --git a/src/brod_consumer.erl b/src/brod_consumer.erl index b84e001f..a2647e69 100644 --- a/src/brod_consumer.erl +++ b/src/brod_consumer.erl @@ -1,4 +1,5 @@ %%% Copyright (c) 2014-2021 Klarna Bank AB (publ) +%%% Copyright (c) 2022-2024 kafka4beam contributors %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. @@ -93,7 +94,10 @@ -type pending_acks() :: #pending_acks{}. -type isolation_level() :: kpro:isolation_level(). --record(state, { bootstrap :: pid() | brod:bootstrap() +-define(GET_FROM_CLIENT, get). +-define(IGNORE, ignore). +-record(state, { client_pid :: ?IGNORE | pid() + , bootstrap :: ?IGNORE | ?GET_FROM_CLIENT | brod:bootstrap() , connection :: ?undef | pid() , topic :: binary() , partition :: integer() @@ -136,6 +140,7 @@ -define(INIT_CONNECTION, init_connection). -define(DEFAULT_AVG_WINDOW, 5). -define(DEFAULT_ISOLATION_LEVEL, ?kpro_read_committed). +-define(DEFAULT_SHARE_LEADER_CONN, false). %%%_* APIs ===================================================================== %% @equiv start_link(ClientPid, Topic, Partition, Config, []) @@ -220,6 +225,16 @@ start_link(Bootstrap, Topic, Partition, Config) -> %% and `read_committed' to get only the records from committed %% transactions %% +%%
  • `share_leader_conn': (optional, default = `false') +%% +%% Whether or not share the partition leader connection with +%% other producers or consumers. +%% Set to `true' to consume less TCP connections towards Kafka, +%% but may lead to higher fetch latency. This is because Kafka can +%% ony accumulate messages for the oldest fetch request, later +%% requests behind it may get blocked until `max_wait_time' expires +%% for the oldest one
  • +%% %% %% @end -spec start_link(pid() | brod:bootstrap(), @@ -286,7 +301,7 @@ get_connection(Pid) -> %%%_* gen_server callbacks ===================================================== -init({Bootstrap, Topic, Partition, Config}) -> +init({Bootstrap0, Topic, Partition, Config}) -> erlang:process_flag(trap_exit, true), Cfg = fun(Name, Default) -> proplists:get_value(Name, Config, Default) @@ -300,15 +315,33 @@ init({Bootstrap, Topic, Partition, Config}) -> BeginOffset = Cfg(begin_offset, ?DEFAULT_BEGIN_OFFSET), OffsetResetPolicy = Cfg(offset_reset_policy, ?DEFAULT_OFFSET_RESET_POLICY), IsolationLevel = Cfg(isolation_level, ?DEFAULT_ISOLATION_LEVEL), - - %% If bootstrap is a client pid, register self to the client - case is_shared_conn(Bootstrap) of + IsShareConn = Cfg(share_leader_conn, ?DEFAULT_SHARE_LEADER_CONN), + + %% resolve connection bootstrap args + {ClientPid, Bootstrap} = + case is_pid(Bootstrap0) of + true when IsShareConn -> + %% share leader connection with other producers/consumers + %% the connection is to be managed by brod_client + {Bootstrap0, ?IGNORE}; + true -> + %% not sharing leader connection with other producers/consumers + %% the bootstrap args will be resolved later when it's + %% time to establish a connection to partition leader + {Bootstrap0, ?GET_FROM_CLIENT}; + false -> + %% this consumer process is not started from `brod' APIs + %% maybe managed by other supervisors. + {?IGNORE, Bootstrap0} + end, + case is_pid(ClientPid) of true -> ok = brod_client:register_consumer(Bootstrap, Topic, Partition); false -> ok end, - {ok, #state{ bootstrap = Bootstrap + {ok, #state{ client_pid = ClientPid + , bootstrap = Bootstrap , topic = Topic , partition = Partition , begin_offset = BeginOffset @@ -418,20 +451,26 @@ handle_cast(Cast, State) -> {noreply, State}. %% @private -terminate(Reason, #state{ bootstrap = Bootstrap +terminate(Reason, #state{ client_pid = ClientPid , topic = Topic , partition = Partition , connection = Connection + , connection_mref = Mref }) -> - IsShared = is_shared_conn(Bootstrap), IsNormal = brod_utils:is_normal_reason(Reason), %% deregister consumer if it's shared connection and normal shutdown - IsShared andalso IsNormal andalso - brod_client:deregister_consumer(Bootstrap, Topic, Partition), - %% close connection if it's working standalone - case not IsShared andalso is_pid(Connection) of - true -> kpro:close_connection(Connection); - false -> ok + case is_pid(ClientPid) andalso IsNormal of + true -> + brod_client:deregister_consumer(ClientPid, Topic, Partition); + false -> + ok + end, + %% close connection if it's owned by this consumer + case Mref =:= ?undef andalso is_pid(Connection) andalso is_process_alive(Connection) of + true -> + kpro:close_connection(Connection); + false -> + ok end, %% write a log if it's not a normal reason IsNormal orelse ?BROD_LOG_ERROR("Consumer ~s-~w terminate reason: ~p", @@ -858,17 +897,19 @@ safe_gen_call(Server, Call, Timeout) -> -spec maybe_init_connection(state()) -> {ok, state()} | {{error, any()}, state()}. maybe_init_connection( - #state{ bootstrap = Bootstrap + #state{ client_pid = ClientPid + , bootstrap = Bootstrap , topic = Topic , partition = Partition , connection = ?undef } = State0) -> %% Lookup, or maybe (re-)establish a connection to partition leader - case connect_leader(Bootstrap, Topic, Partition) of + {MonitorOrLink, Result} = connect_leader(ClientPid, Bootstrap, Topic, Partition), + case Result of {ok, Connection} -> - Mref = case is_shared_conn(Bootstrap) of - true -> erlang:monitor(process, Connection); - false -> ?undef %% linked + Mref = case MonitorOrLink of + monitor -> erlang:monitor(process, Connection); + linked -> ?undef end, %% Switching to a new connection %% the response for last_req_ref will be lost forever @@ -883,13 +924,23 @@ maybe_init_connection( maybe_init_connection(State) -> {ok, State}. -connect_leader(ClientPid, Topic, Partition) when is_pid(ClientPid) -> - brod_client:get_leader_connection(ClientPid, Topic, Partition); -connect_leader(Endpoints, Topic, Partition) when is_list(Endpoints) -> - connect_leader({Endpoints, []}, Topic, Partition); -connect_leader({Endpoints, ConnCfg}, Topic, Partition) -> +connect_leader(ClientPid, ?IGNORE, Topic, Partition) when is_pid(ClientPid) -> + {monitor, brod_client:get_leader_connection(ClientPid, Topic, Partition)}; +connect_leader(ClientPid, ?GET_FROM_CLIENT, Topic, Partition) when is_pid(ClientPid) -> + case brod_client:get_bootstrap(ClientPid) of + {ok, Bootstrap} -> + link_connect_leader(Bootstrap, Topic, Partition); + {error, Reason} -> + {linked, {error, Reason}} + end; +connect_leader(?IGNORE, Bootstrap, Topic, Partition) -> + link_connect_leader(Bootstrap, Topic, Partition). + +link_connect_leader(Endpoints, Topic, Partition) when is_list(Endpoints) -> + link_connect_leader({Endpoints, []}, Topic, Partition); +link_connect_leader({Endpoints, ConnCfg}, Topic, Partition) -> %% connection pid is linked to self() - kpro:connect_partition_leader(Endpoints, ConnCfg, Topic, Partition). + {linked, kpro:connect_partition_leader(Endpoints, ConnCfg, Topic, Partition)}. %% Send a ?INIT_CONNECTION delayed loopback message to re-init. -spec maybe_send_init_connection(state()) -> ok. @@ -900,9 +951,6 @@ maybe_send_init_connection(#state{subscriber = Subscriber}) -> erlang:send_after(Timeout, self(), ?INIT_CONNECTION), ok. -%% In case 'bootstrap' is a client pid, connection is shared with other workers. -is_shared_conn(Bootstrap) -> is_pid(Bootstrap). - %%%_* Tests ==================================================================== -ifdef(TEST).