Packages

SuperWorker is a powerful Elixir library for working with supervisors and background jobs. It provides a much simpler approach than traditional supervisors. This library is currently under development and is unstable, so it is not recommended for production use.

Current section

Files

Jump to
super_worker lib supervisor chain messaging messaging.ex
Raw

lib/supervisor/chain/messaging/messaging.ex

defmodule SuperWorker.Supervisor.Chain.Messaging do
@moduledoc """
Handles message passing and data flow within a worker chain.
This module is responsible for sending data to the correct worker(s)
based on the chain's configuration (e.g., send type, order) and
invoking the finished callback when data completes its journey
through the chain.
"""
require Logger
alias SuperWorker.Supervisor.{Chain, Db, Message, ErrorHandler}
# ============================================================================
# Public API
# ============================================================================
@doc """
Injects new data into the beginning of the chain.
"""
@spec new_data(Chain.t(), Message.t()) :: :ok
def new_data(chain = %Chain{}, msg = %Message{}) do
Logger.debug("Chain.Messaging: Injecting new data into chain #{inspect(chain.id)}")
send_next(chain, 1, msg)
end
@doc """
Sends data to the next worker(s) in the chain based on the current order.
"""
@spec send_next(Chain.t(), non_neg_integer(), Message.t()) ::
{:ok, atom()} | {:error, atom()}
def send_next(chain = %Chain{}, order, msg = %Message{}) do
with {:ok, {worker_id, pid}} <-
Db.get_chain_order(chain.table, chain.id, order) do
Logger.debug(
"Chain.Messaging: Chain #{inspect(chain.id)}, order #{order}, found next worker: #{inspect(worker_id)}. Sending message."
)
send(pid, {:new_data, msg})
{:ok, :sent_to_one}
else
{:error, :not_found} ->
handle_finished_callback(chain, msg)
end
end
# ============================================================================
# Private Helpers
# ============================================================================
defp route_to_multiple_workers(chain, workers, msg) do
case chain.send_type do
:broadcast ->
Enum.each(workers, fn {pid, worker_id} ->
Logger.debug("Chain.Messaging: Broadcasting to worker #{inspect(worker_id)}")
send(pid, {:new_data, msg})
end)
{:ok, :sent_broadcast}
:random ->
{pid, worker_id} = Enum.random(workers)
Logger.debug("Chain.Messaging: Sending randomly to worker #{inspect(worker_id)}")
send(pid, {:new_data, msg})
{:ok, :sent_random}
:partition ->
index = :erlang.phash2(msg.data, length(workers))
{pid, worker_id} = Enum.at(workers, index)
Logger.debug(
"Chain.Messaging: Sending via partition to worker #{inspect(worker_id)} at index #{index}"
)
send(pid, {:new_data, msg})
{:ok, :sent_partition}
:round_robin ->
[{_, {:multi_workers, worker_id, _}} | _] = workers
index = get_next_round_robin_order(chain, worker_id, length(workers))
{pid, _} = Enum.at(workers, index)
Logger.debug("Chain.Messaging: Sending round-robin to worker at index #{index}")
send(pid, {:new_data, msg})
{:ok, :sent_round_robin}
end
end
defp get_next_round_robin_order(chain, worker_id, max_order) do
key = {:round_robin, {:chain, chain.id}, worker_id}
# {pos_to_update, increment, threshold, set_on_threshold}
# This will increment field 2. If it reaches max_order, it resets to 0.
# The initial value is {key, 0}.
# The return value is the value *before* the update.
:ets.update_counter(chain.data_table, key, {2, 1, max_order, 0}, {key, 0})
end
defp handle_finished_callback(chain, msg) do
case chain.finished_callback do
nil ->
Logger.debug(
"Chain.Messaging: No finished_callback defined for chain #{inspect(chain.id)}"
)
{:ok, :no_callback}
{:fun, fun} when is_function(fun, 1) ->
try do
fun.(msg.data)
{:ok, :callback_executed}
catch
kind, reason ->
ErrorHandler.log_error(__MODULE__, "Finished callback failed",
chain_id: chain.id,
kind: kind,
reason: reason
)
{:error, :callback_failed}
end
{m, f, a} when is_atom(m) and is_atom(f) and is_list(a) ->
try do
apply(m, f, [msg.data | a])
{:ok, :callback_executed}
catch
kind, reason ->
ErrorHandler.log_error(__MODULE__, "Finished callback failed",
chain_id: chain.id,
module: m,
function: f,
kind: kind,
reason: reason
)
{:error, :callback_failed}
end
invalid_callback ->
ErrorHandler.log_error(__MODULE__, "Invalid finished_callback definition",
chain_id: chain.id,
callback: invalid_callback
)
{:error, :invalid_callback}
end
end
end