Distributed Tutorial — Client Node

Copy Markdown View Source

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

Step 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)}")