Current section

Files

Jump to
broadway_kafka lib broadway_kafka acknowledger.ex
Raw

lib/broadway_kafka/acknowledger.ex

defmodule BroadwayKafka.Acknowledger do
@moduledoc false
@behaviour Broadway.Acknowledger
@type t :: %{key => value}
@type key :: {:brod.group_generation_id(), :brod.topic(), :brod.partition()}
@type value :: {pending_offsets, last_offset :: :brod.offset(), seen_offsets}
@type pending_offsets :: [:brod.offset()]
@type seen_offsets :: [:brod.offset()]
@spec new() :: t
def new(), do: %{}
@doc """
Add keys to the acknowledger.
"""
@spec add(t, [{:brod.group_generation_id(), :brod.topic(), :brod.partition(), :brod.offset()}]) ::
t
def add(acknowledgers, list) do
for entry <- list,
{generation_id, topic, partition, offset} = entry,
do: {{generation_id, topic, partition}, {:ordsets.new(), offset, :ordsets.new()}},
into: acknowledgers
end
@doc """
Whenever demand is pending, we will call:
for key <- Acknowledger.keys(acks) do
Process.send_after(self(), {:poll, key}, @timeout)
end
Then on :poll, one should call `last_offset/2`, see if the key is
still valid. If so, we should poll Kafka and generate messages.
The `ack_ref` of each message should be `{producer_pid, ack_key}`.
Once poll is done, `update_last_offset/4` must be called every
time messages are sent to the client.
"""
@spec keys(t) :: [key]
def keys(acknowledgers), do: Map.keys(acknowledgers)
@doc """
Returns the last offset for key.
Returns nil if the key does not exist.
"""
@spec last_offset(t, key) :: :brod.offset() | nil
def last_offset(acknowledgers, key) do
case acknowledgers do
%{^key => {_, offset, _}} -> offset
%{} -> nil
end
end
@doc """
Updates the last offset after every polling.
"""
@spec update_last_offset(t, key, :brod.offset(), seen_offsets) :: t
def update_last_offset(acknowledgers, key, last_offset, new_offsets) do
%{^key => {pending, _, seen}} = acknowledgers
%{acknowledgers | key => {pending ++ new_offsets, last_offset, seen}}
end
@doc """
Receives a list of offsets update the current key.
Returns `{drained?, new_offset, t}`. The `drained?`
value should be used a quick check during draining
before checking if all partitions are drained.
{drained?, new_offset, acknowledgers} =
Acknowledger.update_current_offset(state.acknowledgers, key, offsets)
if new_offset do
:brod_group_coordinator.ack(..., new_offset)
end
if drained? && state.draining && Acknowledger.all_drained?(acknowledgers) do
...
end
"""
@spec update_current_offset(t, key, [:brod.offset()]) ::
{drained? :: boolean, :brod.offset() | nil, t}
def update_current_offset(acknowledgers, key, offsets) when is_list(offsets) do
%{^key => {pending, last, seen}} = acknowledgers
{new_pending, new_seen} = update_offsets(offsets, pending, seen)
next = List.first(pending) || last
new_next = List.first(new_pending) || last
update = if new_next > next, do: new_next - 1, else: nil
value = {new_pending, last, new_seen}
{drained?(value), update, %{acknowledgers | key => value}}
end
# Discard older offsets
defp update_offsets([offset | offsets], [current | _] = pending, seen)
when offset < current,
do: update_offsets(offsets, pending, seen)
# Bump latest offset
defp update_offsets([offset | offsets], [offset | pending], seen),
do: update_offsets(offsets, pending, seen)
# Bump from seen
defp update_offsets(offsets, [current | pending], [current | seen]),
do: update_offsets(offsets, pending, seen)
# Merge any left over
defp update_offsets(offsets, pending, seen),
do: {pending, :ordsets.union(offsets, seen)}
@doc """
Returns if all keys drained.
"""
@spec all_drained?(t) :: boolean()
def all_drained?(map) do
Enum.all?(map, fn {_, v} -> drained?(v) end)
end
defp drained?({[], _, []}), do: true
defp drained?(_), do: false
@doc """
The ack callback. It simply sends messages to the annotated producer.
"""
def ack({producer_pid, key}, successful, failed) do
offsets =
Enum.map(successful ++ failed, fn %{acknowledger: {_, _, %{offset: offset}}} -> offset end)
send(producer_pid, {:ack, key, Enum.sort(offsets)})
end
end