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:
-
Implement
start_link/1acceptingactor_id -
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
:gamecapability -
Chat nodes with
:chatcapability -
Payment nodes with
:paymentcapability
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
- Learn about Configuration to customize hash strategies and sharding
- Explore Clustering to distribute processes across multiple nodes
- See Implementing Processes for complex stateful processes
- Understand Sharding internals and distribution