A Decentralized failover and peer-to-peer node finder for Elixir. Allows Elixir nodes to find each other automatically. Once connected, they can coordinate to delegate roles and tasks between the nodes in the cluster. Written using only the Elixir and Erlang standard library.
Within mix.exs, add the below to deps/0.
defp deps do
[
...
{:beethoven, "~> 0.0"}
...
]
endAdd :beethoven to :extra_applications for your application/0 config within mix.exs.
def application do
[
mod: {..., []},
extra_applications: [:logger, :runtime_tools, :beethoven]
]
endAdd dependencies via mix in a shell session.
$> mix Deps.getThen within config.exs, or your preferred config file, input the below config.
# Configuration for the Beethoven stack
config :beethoven,
use_az_net: false,
cluster_net: "127.0.0.0",
cluster_net_mask: "29",
listener_port: 3000,
common_random_backoff: 150..300,
roles: [
# {<AtomName>, <Module>, <Initial Args>, <InstanceCount>}
{:test, Beethoven.TestRole, [arg1: "arg1"], 2},
{:testd, Beethoven.TestdRole, [arg1: "arg1"], 2}
]This configuration uses the 2 test servers, Beethoven.TestRole and Beethoven.TestdRole.
Lets break down the configuration as a whole:
:use_az_net-> Defines whether we should monitor Azure IMDS for changes. Used for Spot VMSS.:cluster_net-> Network name for the target cluster. Defaults to127.0.0.0if not defined above.:cluster_net_mask-> Subnet mask for the target cluster network.:listener_port-> Port used byBeethoven.LocatorandBeethoven.BeaconServerfor node discovery.:common_random_backoff-> Random backoff used by a few services in Beethoven. Generally this should not be changed.:roles-> List of roles that Beethoven should monitor. Defaults to 'no roles' if left undefined.
Defining a role for Beethoven to track uses this following schema:
# {<AtomName>, <Module>, <Initial Args>, <InstanceCount>}
{:test, Beethoven.TestRole, [arg1: "arg1"], 2}AtomName-> Friendly name for the Role within the Beethoven Cluster. Should be unique among the other defined roles.Module-> An OTP compliant server that will be used to spawn the role on creation.Initial Args-> Arguments that will be fed into the role on creation. Arguments will be input to thestart_link/2function for the provided OTP compliant module.InstanceCount-> The number of instances needed for this role across the cluster.
Notes on
InstanceCount
- Nodes in the cluster will only ever run one single instance of a role.
- If you need this role on all nodes in the cluster, consider just adding it to your applications supervisor tree directly. You will receive no benefit from the Role assignment.
- The number is maintained as nodes join and leave the cluster.
Now, once you start your Mix Application, your role should spawn once Beethoven.RoleServer has initialized.
Distr(ibuted)Server is a modified GenServer that allows for seamless integration with a dedicated Mnesia table.
This was specially built for operation in a Beethoven environment.
The idea is that the brain of the GenServer can be set with Mnesia and not the GenServer's internal state. This allows for the compute and state of the GenServer to be distributed across the Beethoven cluster.
defmodule Test do
use DistrServer, subscribe?: true
# Standard OTP entry point for starting the PID.
def start_link(init_args) do
DistrServer.start_link(__MODULE__, init_args, name: __MODULE__)
end
# The configuration for the DistrServer's Mnesia table.
@impl true
def config() do
%{
tableName: TestTracker,
columns: [:col1, :col2, :last_change],
indexes: [],
dataType: :set,
copyType: :multi
}
end
# This is ran when the table is newly created.
@impl true
def create_action(_tableConfig) do
...
:ok
end
# Similar to GenServer.init/1.
@impl true
def entry_point(_init_args) do
...
{:ok, :ok} # all init/1 returns are supported.
end
endSee documentation for Beethoven.DistrServer for more information.
A signal aggregator to determine the busyness of a given Beethoven node. By default, CPU, RAM, and Beethoven roles hosted are signaled for busyness.
These signals can be created using the signal() macro provided in Allocator.Agent
def test_func() do
# Returns the least-busy node in the beethoven cluster.
# Wrapper for `Beethoven.Allocator.allocate()`
nodeName = Beethoven.allocate()
# Returns sorted list of all active Beethoven roles.
# List is sorted from least-to-most busy.
nodeList = Beethoven.Allocator.allocation_list()
endA behavior that provides a macro for just-in-time telemetry signal creation. Signals defined a :name, :weight, and :type value. These values combine together to create 0-1 arity functions to streamline sending signals to Allocator.
defmodule Signals do
use Beethoven.Allocator.Agent
signal(name: :ram, weight: 10.0, type: :percent)
signal(name: :http_requests, weight: 5.0, type: :count)
end
# Generates
Signals.percent_ram(data) # Data is now the value of the metric in `:ets`
Signals.increment_http_requests_count() # +1.0
Signals.decrement_http_requests_count() # -1.0Please run your app with the environmental variable MIX_ENV set to dev, and create an issue on the Github repo for Beethoven.