Powered by AppSignal & Oban Pro

Quickstart

quickstart.livemd

Quickstart

Mix.install([
  {:mesh, "~> 0.1"},
  {:libcluster, "~> 3.3"} # optional
])

Introduction

Mesh is a distributed virtual process system for Elixir that provides:

  • Location transparency - processes can live anywhere in the cluster
  • Automatic sharding - consistent hashing distributes processes across nodes
  • Capability-based routing - route processes to nodes based on capabilities
  • Simple GenServer protocol - no special behaviors required

Setup

Add Mesh to your supervision tree:

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      # Start Mesh
      Mesh.Supervisor
    ]

    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

For this quickstart, we’ll start Mesh manually:

{:ok, _pid} = Mesh.Supervisor.start_link([])
IO.puts("✓ Mesh started")

Define a Process

Create a simple GenServer that will act as your virtual process. The only requirements are:

  1. Implement start_link/1 accepting actor_id
  2. Handle {:actor_call, payload} messages
defmodule GameActor do
  use GenServer
  require Logger

  def start_link(actor_id) do
    GenServer.start_link(__MODULE__, actor_id)
  end

  def init(actor_id) do
    Logger.info("GameActor #{actor_id} started")
    {:ok, %{id: actor_id, score: 0, level: 1}}
  end

  def handle_call({:actor_call, %{action: "increment_score"}}, _from, state) do
    new_score = state.score + 1
    new_state = %{state | score: new_score}
    {:reply, {:ok, new_score}, new_state}
  end

  def handle_call({:actor_call, %{action: "level_up"}}, _from, state) do
    new_level = state.level + 1
    new_state = %{state | level: new_level}
    {:reply, {:ok, new_level}, new_state}
  end

  def handle_call({:actor_call, %{action: "get_stats"}}, _from, state) do
    {:reply, {:ok, %{score: state.score, level: state.level}}, state}
  end
end

Register Capabilities

Capabilities define what types of processes this node can handle. By registering :game, we’re telling Mesh that this node is responsible for handling game-related processes.

In a multi-node cluster, you might have:

  • Game nodes with :game capability
  • Chat nodes with :chat capability
  • Payment nodes with :payment capability

Mesh uses these capabilities to route process requests to the appropriate nodes.

Mesh.register_capabilities([:game])
IO.puts("✓ Capabilities registered: [:game]")

# Give it a moment to sync across nodes
Process.sleep(100)

Invoke Processes

Now you can invoke processes! Mesh will automatically:

  • Create the process on first invocation
  • Route to the correct node based on consistent hashing
  • Reuse the same process instance for subsequent calls with the same actor_id
# First call - process will be created
{:ok, pid, score} = Mesh.call(%Mesh.Request{
  module: GameActor,
  id: "player_123",
  payload: %{action: "increment_score"},
  capability: :game
})
IO.puts("Score: #{score}, PID: #{inspect(pid)}")

Subsequent calls with the same actor_id will reuse the same process:

# Same process instance
{:ok, ^pid, score} = Mesh.call(%Mesh.Request{
  module: GameActor,
  id: "player_123",
  payload: %{action: "increment_score"},
  capability: :game
})
IO.puts("Score: #{score}, PID: #{inspect(pid)} (same PID!)")

Try different actions:

{:ok, _pid, level} = Mesh.call(%Mesh.Request{
  module: GameActor,
  id: "player_123",
  payload: %{action: "level_up"},
  capability: :game
})
IO.puts("Level: #{level}")

{:ok, _pid, stats} = Mesh.call(%Mesh.Request{
  module: GameActor,
  id: "player_123",
  payload: %{action: "get_stats"},
  capability: :game
})
IO.inspect(stats, label: "Stats")

Multiple Processes

Each unique actor_id creates a separate process instance:

# Create multiple players
for player_id <- 1..5 do
  {:ok, _pid, score} = 
    Mesh.call(%Mesh.Request{
      module: GameActor,
      id: "player_#{player_id}",
      payload: %{action: "increment_score"},
      capability: :game
    })
  IO.puts("Player #{player_id}: #{score}")
end

Sharding

Mesh uses consistent hashing to determine which node owns each process:

# Check which shard an actor belongs to
shard = Mesh.shard_for("player_123")
IO.puts("player_123 is on shard #{shard}")

# Check which node owns a shard
{:ok, owner_node} = Mesh.owner_node(shard, :game)
IO.puts("Shard #{shard} is owned by: #{owner_node}")

NOTE: The default hash strategy (EventualConsistency) uses eventual consistency for process placement. Shards are used purely for routing decisions - they do not provide state guarantees or transactions. Each process manages its own state independently. During network partitions or topology changes, the same process ID may temporarily exist on multiple nodes until the system converges. You can implement custom hash strategies with different consistency guarantees - see Configuration.

Next Steps

Additional Links