Current section

Files

Jump to
kaffe lib kaffe consumer_group worker worker_manager.ex
Raw

lib/kaffe/consumer_group/worker/worker_manager.ex

defmodule Kaffe.WorkerManager do
@moduledoc """
Manage topic/partition-to-worker assignments. Subscribers get workers
from here.
This process manages the workers, while delegating to
`Kaffe.WorkerSupervisor` to start each worker under supervision.
Workers are allocated based on a configured strategy
(`worker_allocation_strategy` in the `consumer` config): either one
worker per topic/partition, or one worker for a partition across
topics.
The first strategy is useful for higher throughput and increased
flexibility. The second is useful when mutliple input topics may have
the same messages (identified by key) and those messages must be
processsed sequentially.
The table of workers is stored in an ETS table, `:kaffe_workers`.
"""
use GenServer
require Logger
## ==========================================================================
## Public API
## ==========================================================================
@doc false
def child_spec(opts), do: super(opts)
@doc false
def start_link([subscriber_name, config]) do
GenServer.start_link(__MODULE__, [self(), subscriber_name, config], name: name(subscriber_name))
end
def worker_for(pid, topic, partition) do
GenServer.call(pid, {:worker_for, topic, partition})
end
## ==========================================================================
## Callbacks
## ==========================================================================
@impl GenServer
def init([supervisor_pid, subscriber_name, config]) do
Logger.info("event#starting=#{__MODULE__} subscriber_name=#{subscriber_name} supervisor=#{inspect(supervisor_pid)}")
worker_table = :ets.new(:kaffe_workers, [:set, :protected])
{:ok,
%{
supervisor_pid: supervisor_pid,
subscriber_name: subscriber_name,
worker_table: worker_table,
config: config
}}
end
@impl GenServer
def handle_call({:worker_for, topic, partition}, _from, state) do
Logger.debug("Allocating worker: #{topic} / #{partition}")
worker_pid = allocate_worker(topic, partition, state)
{:reply, worker_pid, state}
end
## ==========================================================================
## Helpers
## ==========================================================================
defp allocate_worker(topic, partition, %{worker_table: worker_table} = state) do
worker_name = worker_name(topic, partition, state.config.worker_allocation_strategy)
case :ets.lookup(worker_table, worker_name) do
[{^worker_name, worker_pid}] -> worker_pid
[] -> start_worker(worker_name, state)
end
end
defp start_worker(worker_name, state) do
config = state.config
Logger.debug("Creating worker: #{worker_name}")
worker_supervisor().start_worker(
state.supervisor_pid,
config.message_handler,
state.subscriber_name,
worker_name
)
|> capture_worker(worker_name, state)
end
defp capture_worker({:ok, pid}, worker_name, %{worker_table: worker_table}) do
true = :ets.insert(worker_table, {worker_name, pid})
pid
end
# This should probably be defp, but would technically be a breaking change.
@doc false
def worker_name(topic, partition, worker_allocation_strategy) do
case worker_allocation_strategy do
:worker_per_partition -> :"worker_#{partition}"
:worker_per_topic_partition -> :"worker_#{topic}_#{partition}"
end
end
defp worker_supervisor do
Application.get_env(:kaffe, :worker_supervisor_mod, Kaffe.WorkerSupervisor)
end
defp name(subscriber_name) do
:"#{__MODULE__}.#{subscriber_name}"
end
end