Packages

Cafex is a pure Elixir implementation of Kafka client.

Current section

Files

Jump to
misha_cafex lib cafex consumer worker.ex
Raw

lib/cafex/consumer/worker.ex

defmodule Cafex.Consumer.Worker do
@behaviour :gen_fsm
require Logger
@pre_fetch_size 50
@max_wait_time 100
@min_bytes 32 * 1024
@max_bytes 1024 * 1024
@client_id "cafex"
@typedoc "Options used by the `start_link/9` functions"
@type options :: [option]
@type option :: {:max_wait_time, non_neg_integer} |
{:min_bytes, non_neg_integer} |
{:max_bytes, non_neg_integer}
@type handler :: {module, args :: [Keyword.t]}
defmodule State do
@moduledoc false
defstruct topic: nil,
group: nil,
client_id: nil,
partition: nil,
broker: nil,
max_wait_time: nil,
min_bytes: nil,
max_bytes: nil,
conn: nil, # partition leader connection
lock: {false, nil},
lock_cfg: nil,
buffer: [],
hwm_offset: 0,
fetching: false,
pre_fetch_size: 50,
coordinator: nil,
handler: nil,
handler_data: nil,
connection_mod: Connection
end
alias Cafex.Connection
alias Cafex.Protocol.Fetch
alias Cafex.Consumer.OffsetManager
# ===================================================================
# API
# ===================================================================
def start_link(coordinator, handler, topic, group, partition, broker, opts \\ []) do
:gen_fsm.start_link __MODULE__, [coordinator, handler, topic, group, partition, broker, opts], []
end
def stop(pid) do
:gen_fsm.sync_send_all_state_event pid, :stop, :infinity
end
# ===================================================================
# :gen_fsm callbacks
# ===================================================================
@doc false
def init([coordinator, handler, topic, group, partition, broker, opts]) do
opts = opts || []
state = %State{topic: topic,
group: group,
client_id: Keyword.get(opts, :client_id) || @client_id,
partition: partition,
broker: broker,
coordinator: coordinator,
handler: handler,
lock_cfg: Keyword.get(opts, :lock_cfg),
pre_fetch_size: Keyword.get(opts, :pre_fetch_size) || @pre_fetch_size,
max_wait_time: Keyword.get(opts, :max_wait_time) || @max_wait_time,
min_bytes: Keyword.get(opts, :min_bytes) || @min_bytes,
max_bytes: Keyword.get(opts, :max_bytes) || @max_bytes}
|> conn_mod(Connection)
{:ok, :acquire_lock, state, 0}
end
@lock_timeout 60000 * 5
@doc false
def acquire_lock(:timeout, %{partition: partition,
lock_cfg: {lock_mod, args},
group: group,
topic: topic} = state) do
path = Path.join [group, topic, "partitions", Integer.to_string(partition)]
lock_mod.acquire(path, args)
|> case do
{:wait, pid} ->
{:next_state, :waiting_lock, %{state | lock: {false, pid}}, @lock_timeout}
{:ok, lock} ->
{:next_state, :prepare, %{state | lock: {true, lock}}, 0}
end
end
@doc false
def waiting_lock(:timeout, state) do
{:stop, :lock_timeout, state}
end
@doc false
def prepare(:timeout, %{partition: partition,
broker: {host, port},
handler: {handler, args},
client_id: client_id,
coordinator: coordinator} = state) do
{:ok, conn} = conn_mod(state).start_link(host, port, client_id: client_id)
{:ok, data} = handler.init(args)
{:ok, {offset, _}} = OffsetManager.fetch(coordinator, partition, conn)
{:next_state, :consuming, %{state | conn: conn,
hwm_offset: offset,
handler: handler,
handler_data: data}, 0}
end
@doc false
def consuming({:kafka_response, response}, state) do
handle_fetch_response(response, state)
end
def consuming(:timeout, state) do
consume(state)
end
@doc false
def waiting_messages(:timeout, state) do
{:stop, :fetch_timeout, state}
end
def waiting_messages({:kafka_response, response}, state) do
handle_fetch_response(response, state)
end
def pausing({:kafka_response, response}, state) do
# handle fetch response and continue pausing status
case handle_fetch_response(response, state) do
{:next_state, :consuming, state, _timeout} ->
{:next_state, :pausing, state}
{:stop, reason, state} ->
{:stop, reason, state}
end
end
@doc false
def handle_event(event, state_name, state_data) do
{:stop, {:bad_event, state_name, event}, state_data}
end
@doc false
def handle_sync_event(:stop, _from, _state_name, state) do
{:stop, :normal, :ok, state}
end
@doc false
def handle_info({:lock, :ok, lock}, :waiting_lock, %{lock: {false, lock}} = state_data) do
{:next_state, :prepare, %{state_data | lock: {true, lock}}, 0}
end
@doc false
def handle_info(:resume, :pausing, state) do
{:next_state, :consuming, state, 0}
end
@doc false
def terminate(_reason, _state_name, %{handler: handler,
handler_data: data} = state_data) do
close_connection(state_data)
release_lock(state_data)
if data, do: handler.terminate(data)
:ok
end
@doc false
def code_change(_old, state_name, state_data, _extra) do
{:ok, state_name, state_data}
end
# ===================================================================
# Internal functions
# ===================================================================
defp close_connection(%{conn: nil}), do: :ok
defp close_connection(%{conn: pid} = state) do
if Process.alive?(pid), do: conn_mod(state).close(pid)
end
defp release_lock(%{lock: {_, nil}}), do: :ok
defp release_lock(%{lock: {_, lock}, lock_cfg: {mod, _}}) do
mod.release(lock)
end
defp fetch_messages(%{fetching: true} = state), do: state
defp fetch_messages(%{topic: topic,
partition: partition,
hwm_offset: offset,
max_bytes: max_bytes,
conn: conn} = state) do
# Logger.debug fn -> "Consumer[#{group}:#{topic}:#{partition}] fetching messages: offset = #{offset}" end
request =
Map.take(state, [:max_wait_time, :min_bytes])
|> Map.put(:topics, [{topic, [{partition, offset, max_bytes}]}])
|> (&(struct(Fetch.Request, &1))).()
conn_mod(state).async_request(conn, request, {:fsm, self})
%{state | fetching: true}
end
defp handle_fetch_response(response, %{topic: topic,
partition: partition,
buffer: buffer,
hwm_offset: offset} = state) do
state = %{state | fetching: false}
case response do
{:ok, %{topics: [{^topic, [%{error: :no_error, messages: messages, hwm_offset: hwm_offset}]}]}} ->
messages =
messages
|> Enum.sort(fn (%{offset: off1}, %{offset: off2}) ->
off2 > off1
end)
|> Enum.filter(fn %{offset: msg_offset} ->
msg_offset >= offset
end)
if length(messages) != hwm_offset - offset do
Logger.warn "P:#{partition} Msg len: #{length(messages)}, #{offset}:#{hwm_offset}"
end
buffer = buffer ++ messages
hwm_offset = case List.last(buffer) do
nil -> offset
msg -> msg.offset + 1
end
{:ok, %{state | buffer: buffer, hwm_offset: hwm_offset}}
{:ok, %{topics: [{^topic, [%{error: :not_leader_for_partition = reason}]}]}} ->
Logger.error "Failed to fetch new messages: #{inspect reason}, topic: #{topic}, partition: #{partition}, offset: #{offset}"
{:error, :not_leader_for_partition, state}
{:ok, %{topics: [{^topic, [%{error: :offset_out_of_range = reason}]}]}} ->
Logger.error "Failed to fetch new messages: #{inspect reason}, topic: #{topic}, partition: #{partition}, offset: #{offset}"
# {:ok, state}
{:error, :offset_out_of_range, state}
{:error, reason} ->
Logger.error "Failed to fetch new messages: #{inspect reason}, topic: #{topic}, partition: #{partition}, offset: #{offset}"
{:ok, state}
end
|> case do
{:ok, %{buffer: []} = state} ->
{:next_state, :consuming, state, 1000}
{:ok, state} ->
{:next_state, :consuming, state, 0}
{:error, :offset_out_of_range, state} ->
state = offset_reset(state)
{:next_state, :consuming, state, 0}
{:error, reason, state} ->
{:stop, reason, state}
end
end
defp consume(%{pre_fetch_size: pre_fetch_size} = state) do
case do_consume(pre_fetch_size, state) do
{:continue, %{buffer: buffer} = state} ->
buffer_length = length(buffer)
cond do
buffer_length == 0->
{:next_state, :waiting_messages, fetch_messages(state)}
buffer_length <= pre_fetch_size ->
{:next_state, :consuming, fetch_messages(state), 0}
true ->
{:next_state, :consuming, state, 0}
end
{:pause, timeout, state} ->
:erlang.send_after(timeout, self, :resume)
{:next_state, :pausing, state}
end
end
defp do_consume(0, state), do: {:continue, state}
defp do_consume(_, %{buffer: []} = state), do: {:continue, state}
defp do_consume(c, %{buffer: [%{offset: offset} = first|rest],
coordinator: coordinator,
topic: topic,
partition: partition,
handler: handler,
handler_data: data} = state) do
message = %{first | topic: topic, partition: partition}
case handler.consume(message, data) do
{:ok, data} ->
OffsetManager.commit(coordinator, partition, offset + 1)
do_consume(c - 1, %{state | buffer: rest, handler_data: data})
{:nocommit, data} ->
do_consume(c - 1, %{state | buffer: rest, handler_data: data})
{:pause, timeout} ->
{:pause, timeout, state}
end
end
defp offset_reset(%{coordinator: coordinator, partition: partition, conn: conn} = state) do
{:ok, {offset, _}} = OffsetManager.reset(coordinator, partition, conn)
%{state | hwm_offset: offset}
end
defp conn_mod(%State{} = state, mod), do: %State{state | connection_mod: mod}
defp conn_mod(%State{connection_mod: mod}), do: mod
end