Step 1: Connect to the Storage Node
storage_node = :"storage@127.0.0.1"
case Node.connect(storage_node) do
true ->
IO.puts("Connected to #{storage_node}")
IO.puts("Connected nodes: #{inspect(Node.list())}")
false ->
IO.puts("Failed to connect to #{storage_node}")
IO.puts("Make sure the Storage node IEx session is running")
endStep 2: Make Remote Calls
Call the storage service running on the other node.
import Malla, only: [call: 1]
# Store data on the remote node
call StorageService.put(:from_client, %{message: "Hello from client node!"})
call StorageService.put(:timestamp, DateTime.utc_now())
IO.puts("Data stored on remote node")
# Retrieve data from the remote node
{:ok, data} = call StorageService.get(:from_client)
IO.puts("Retrieved from remote: #{inspect(data)}")Step 3: Understanding Remote Calls
The call macro is syntactic sugar for Malla.remote/4:
# These are equivalent:
# call StorageService.get(:user_1)
# Malla.remote(StorageService, :get, [:user_1])
# Let's see where the service is actually running
nodes = Malla.Node.get_nodes(StorageService)
IO.puts("StorageService is available on: #{inspect(nodes)}")
# Make an explicit remote call with options
result =
Malla.remote(
StorageService,
:get,
[:user_1],
timeout: 5000
)
IO.puts("Remote call result: #{inspect(result)}")Part 4: New service
First, run the "Add a Log Service" section in the Storage notebook. Service will be discovered immediately by this node. Then run this:
import Malla, only: [call: 1]
# Use the LogService running on the storage node
call LogService.log("Hello from client node!")
call LogService.log("Testing distributed logging")
# Retrieve logs
logs = call LogService.get_recent_logs(10)
IO.puts("\nRecent logs:")
Enum.each(logs, &IO.puts/1)Part 5: Virtual Modules
When Malla discovers a remote service, it creates a virtual module — a local proxy that forwards calls to the remote node automatically. This means you can call remote services directly, without using the call macro:
# No import, no `call` macro — just a regular function call.
# Malla's virtual module routes it to the storage node transparently.
logs = LogService.get_recent_logs(10)
IO.puts("Logs via virtual module:")
Enum.each(logs, &IO.puts/1)See documentation about downsides of using this "virtual" call.
Summary
IO.puts("""
Congratulations! You've successfully:
1. Set up a distributed Erlang cluster with two nodes
2. Created a service on one node (StorageService)
3. Made remote calls from another node
4. Experienced automatic service discovery
5. Built bidirectional service communication
Key Concepts:
- global: true
Makes services discoverable across the cluster via :pg process groups
- Malla.call / Malla.remote
Transparently route function calls to the node running the service
- Service Discovery
Malla.Node automatically finds services every 10 seconds
Local node is preferred, remote nodes are randomized for load balancing
- Virtual Modules
When a remote service is detected, Malla creates a local proxy module
- Failover
If a node fails, calls automatically route to other available nodes
Next steps: try starting a third node, stopping the storage node
to see failover, or using Malla.Node.call_cb_all/3 to broadcast.
""")Troubleshooting
Connection Problems
IO.puts("This node: #{node()}")
IO.puts("Connected nodes: #{inspect(Node.list())}")
IO.puts("Cookie: #{Node.get_cookie()}")Service Discovery Issues
Process.sleep(11_000)
nodes_with_storage = Malla.Node.get_nodes(StorageService)
IO.puts("StorageService found on: #{inspect(nodes_with_storage)}")
members = :pg.get_members(Malla.Services2, StorageService)
IO.puts("Process group members: #{inspect(members)}")