Skip to content

jimurrito/Beethoven

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

97 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Beethoven

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.

How to use

Within mix.exs, add the below to deps/0.

defp deps do
    [
        ...
        {:beethoven, "~> 0.0"}
        ...
    ]
end

Add :beethoven to :extra_applications for your application/0 config within mix.exs.

def application do
[
    mod: {..., []},
    extra_applications: [:logger, :runtime_tools, :beethoven]
]
end

Add dependencies via mix in a shell session.

$> mix Deps.get

Then 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 to 127.0.0.0 if not defined above.
  • :cluster_net_mask -> Subnet mask for the target cluster network.
  • :listener_port -> Port used by Beethoven.Locator and Beethoven.BeaconServer for 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.

Role definition

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 the start_link/2 function 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.

Features

DistrServer

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.

Example

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

end

See documentation for Beethoven.DistrServer for more information.

Allocator

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

Example

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()
end

Allocator.Agent

A 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.

Example

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.0

Any issues?

Please run your app with the environmental variable MIX_ENV set to dev, and create an issue on the Github repo for Beethoven.