Packages

Mostly-transparent batching of remote messages in Erlang/Elixir cluster

Current section

Files

Jump to
batched_communication lib batched_communication.ex
Raw

lib/batched_communication.ex

use Croma
defmodule BatchedCommunication do
@moduledoc File.read!(Path.join([__DIR__, "..", "README.md"])) |> String.replace_prefix("# BatchedCommunication\n\n", "")
import Kernel, except: [send: 2]
alias BatchedCommunication.{Compression, Sender, Receiver}
@type dest :: pid | atom | {atom, node}
@type message :: any
@type reply :: any
@doc """
Sends an asynchronous message to the given destination process.
When you want to batch multiple messages to the same destination node,
you can use this function as a replacement for `GenServer.cast/2`.
"""
defun cast(dest :: dest, msg :: message) :: :ok do
send(dest, {:"$gen_cast", msg})
end
@doc """
Makes a synchronous request to the given destination process.
When you want to batch multiple messages to the same destination node,
you can use this function as a replacement for `GenServer.call/3`.
"""
defun call(dest :: dest, msg :: message, t :: timeout | {:clean_timeout, timeout} | {:dirty_timeout, timeout} \\ 5000) :: reply do
ref = Process.monitor(dest)
send(dest, {:"$gen_call", {self(), ref}, msg})
receive do
{^ref, reply} -> Process.demonitor(ref, [:flush]); reply
{:DOWN, ^ref, _, _, :noconnection} -> exit({{:nodedown, get_node(dest)}, {__MODULE__, :call, [dest, msg, t]}})
{:DOWN, ^ref, _, _, reason} -> exit({reason, {__MODULE__, :call, [dest, msg, t]}})
after
timeout(t) ->
Process.demonitor(ref, [:flush])
exit({:timeout, {__MODULE__, :call, [dest, msg, t]}})
end
end
defp get_node({_name, node}), do: node
defp get_node(p) when is_pid(p), do: node(p)
defp timeout({:dirty_timeout, t}), do: t
defp timeout({:clean_timeout, t}), do: t
defp timeout(t ), do: t
@doc """
Sends a reply to a client that has sent a synchronous request.
When you want to batch multiple messages to the same destination node,
you can use this function as a replacement for `GenServer.reply/2`.
"""
defun reply({pid, tag} :: {pid, reference}, reply :: reply) :: :ok do
send(pid, {tag, reply})
end
@doc """
Sends `message` to the destination process `dest` with a batching mechanism.
"""
defun send(dest :: dest, message :: message) :: :ok do
{p, n} = node_pair(dest)
case n == node() do
true -> send_local(p, message)
false -> Sender.enqueue(n, p, message)
end
:ok
end
@doc """
Sends the same `message` to multiple destination processes.
"""
defun broadcast(dests :: [dest], message :: message) :: :ok do
dests_per_node =
Enum.map(dests, &node_pair/1)
|> Enum.group_by(&elem(&1, 1), &elem(&1, 0))
{dests_in_this_node, dests_per_remote_node} = Map.pop(dests_per_node, Node.self(), [])
Enum.each(dests_in_this_node, fn d -> send_local(d, message) end)
Enum.each(dests_per_remote_node, fn {n, ds} -> Sender.enqueue(n, ds, message) end)
end
defp node_pair(p) when is_pid(p) , do: {p, node(p)}
defp node_pair(a) when is_atom(a), do: {a, Node.self()}
defp node_pair({_a, _n} = pair) , do: pair
defp send_local(p, message) do
try do
Kernel.send(p, message)
rescue
ArgumentError -> :ok # no process found for name (in this case `p` is an atom)
end
end
@type configurations :: %{
max_wait_time: pos_integer,
max_messages_per_batch: pos_integer,
compression: Compression.t,
}
@doc """
Gets the current configurations.
"""
defun get_configurations() :: configurations do
Sender.get_configurations()
end
@doc """
Sets maximum wait time (in milliseconds) before sending messages as a batch.
When a `BatchedCommunication.Sender` process receives a message for a particular destination node,
it starts a timer with the maximum wait time.
When the timer fires the accumulated messages are sent in one batch.
Defaults to `100` milliseconds.
"""
defun change_max_wait_time(time :: g[pos_integer]) :: :ok do
Sender.change_property_in_all_senders(:max_wait_time, time)
end
@doc """
Sets maximum number of messages to accumulate in one batch.
When a `BatchedCommunication.Sender` process has messages more than or equal to this maximum,
it immediately (i.e., without waiting for timer; see also `change_max_wait_time/1`) sends the messages in one batch.
Defaults to `100` messages.
"""
defun change_max_messages_per_batch(max :: g[pos_integer]) :: :ok do
Sender.change_property_in_all_senders(:max_messages_per_batch, max)
end
@doc """
Changes whether to compress each batch of messages or not.
Currently supported values are `:gzip` and `:raw` (no compression).
Defaults to `:gzip`.
"""
defun change_compression(compression :: Compression.t) :: :ok do
if not Compression.valid?(compression) do
raise ArgumentError, "invalid value for compression setting: #{inspect(compression)}"
end
Sender.change_property_in_all_senders(:compression, compression)
end
@doc """
Changes the process scheduling priority of senders and receivers.
By default processes run with `:normal` priority.
You can change the priority of all sender/receiver processes using this function.
See also [`:erlang.process_flag/2`](http://erlang.org/doc/man/erlang.html#process_flag_priority).
"""
defun change_process_priority(priority :: :high | :normal | :low) :: :ok do
:high -> change_property_in_all_senders_and_receivers(:priority, :high )
:normal -> change_property_in_all_senders_and_receivers(:priority, :normal)
:low -> change_property_in_all_senders_and_receivers(:priority, :low )
end
defp change_property_in_all_senders_and_receivers(prop, value) do
Sender.change_property_in_all_senders(prop, value)
Receiver.change_property_in_all_receivers(prop, value)
end
@type batch_stats :: {n_messages :: pos_integer, raw_bytes :: pos_integer, sent_bytes :: pos_integer}
@doc """
Collect statistics of batches sent from this node to the specified node during the specified duration (in milliseconds).
Each element of the returned list is a 3-tuple that consists of
- number of messages in a batch
- byte size of the batch before comprression
- byte size of the batch after compression (this is equal to the previous one if `:raw` compression option is used)
"""
defun collect_sending_stats(dest_node :: g[node], duration :: g[pos_integer]) :: [batch_stats] do
if dest_node == Node.self(), do: raise "target node must not be the current node"
Sender.collect_stats(dest_node, duration)
end
end