Files
comp4958/lectures/06/notes.ex
T
2026-10-20 10:10:30 -07:00

161 lines
6.4 KiB
Elixir

# Lecture 06 notes: raw sockets, then splitting a client/server across nodes
# This file is not meant to be compiled/run as a whole - paste lines into
# iex (or a shell, where noted) one at a time.
### ---------------------------------------------------------------------- ###
### Part 1: echo/ - plain TCP, no distributed Elixir involved yet
### ---------------------------------------------------------------------- ###
# load the module definitions into an iex session (run from lectures/06/echo/)
# iex echo.ex
# Echo.Active / Echo.Passive / Echo.Hybrid all block the calling process
# forever inside accept/loop, so start them in a spawned process if you
# want your iex shell back:
spawn(fn -> Echo.Active.start(6666) end)
spawn(fn -> Echo.Passive.start(6667) end)
spawn(fn -> Echo.Hybrid.start(6668) end)
# the GenServer version returns immediately because init/1 just schedules
# :accept via send(self(), :accept) and returns - no spawn needed
Echo.start(6666)
# -> {:ok, #PID<...>}
# quick manual client, no Elixir needed - good for sanity-checking a server
# nc localhost 6666
# (type a line, press enter, see it echoed back; Ctrl-D or Ctrl-C to quit)
# or use the provided Elixir client (run from a second terminal):
# elixir echo_client.ex 127.0.0.1 6666
# (defaults to 127.0.0.1:6666 if no args given)
# or the Java client (compiles/runs independently - proves the server
# doesn't care what language/process connects, it's just bytes on a socket)
# cd ../java && javac EchoClient.java && java EchoClient
### ---------------------------------------------------------------------- ###
### Part 2: distributed Elixir basics - the mechanism the counter/ examples
### rely on instead of hand-rolled sockets
### ---------------------------------------------------------------------- ###
# Start two connected BEAM nodes (same machine is fine for testing).
# --sname gives each node a short name; both MUST share a cookie or they
# refuse to connect to each other.
# terminal 1 (this will become the "server" node):
# iex --sname server --cookie lecture06
# terminal 2 (this will become the "client" node):
# iex --sname client --cookie lecture06
# from either node, connect them:
Node.connect(:"server@your-hostname")
# -> true
# find the exact name to use with: Node.self() (run that on the server node)
Node.list() # -> [:"client@your-hostname"] (from the server's side)
Node.self() # -> the current node's own full name
# :global is what replaces Registry here - Registry is local-to-a-node only,
# :global registers names cluster-wide once nodes are connected
:global.registered_names() # -> [] until something registers
:global.whereis_name(Counter.Worker) # -> PID or :undefined
### ---------------------------------------------------------------------- ###
### Part 3: counter/static - one globally-registered counter
### ---------------------------------------------------------------------- ###
# -- server node --
# cd counter/static/server
# mix deps.get # first time only
# iex --sname server --cookie lecture06 -S mix
# this boots Counter.Application, which starts Counter.Store (file-backed,
# "counter.db") and Counter.Worker (registered as {:global, Counter.Worker})
Counter.Worker.value() # works locally on the server node too
Counter.Worker.inc(5)
# -- client node --
# cd counter/static/client (NOT a mix project - just loose .ex files)
# iex --sname client --cookie lecture06
Node.connect(:"server@your-hostname")
c "worker.ex" # compiles/loads Counter.Worker (the thin stub)
# these now run against the REAL process living on the server node -
# no socket code written by us, distributed Erlang routes the GenServer
# call/cast over the network
Counter.Worker.inc()
Counter.Worker.value()
# -> whatever the server node's Counter.Worker currently holds
# don't call Counter.Worker.start_link/1 from the client - the server's
# Application already started (and globally registered) the one that
# matters; starting another just races/duplicates the name
### ---------------------------------------------------------------------- ###
### Part 4: counter/dynamic - many named counters, started on demand,
### reachable from a remote node
### ---------------------------------------------------------------------- ###
# -- server node --
# cd counter/dynamic/server
# mix deps.get
# iex --sname server --cookie lecture06 -S mix
# boots: Registry (local, unused for cross-node lookup), an ETS table for
# crash-durable state, and Counter.DynamicSupervisor registered as
# {:global, Counter.DynamicSupervisor}
# -- client node --
# cd counter/dynamic/client
# iex --sname client --cookie lecture06
Node.connect(:"server@your-hostname")
c "dynamic_supervisor.ex"
c "worker.ex"
# ask the REMOTE supervisor (on the server node) to spawn a new worker -
# the child process is started under the server's supervision tree, not
# the client's
Counter.DynamicSupervisor.start_worker("c1")
# -> {:ok, #PID<...>} (a server-node PID)
Counter.Worker.inc("c1")
Counter.Worker.inc("c1", 5)
Counter.Worker.value("c1")
# -> 6
# start as many independent counters as you like, all living on the
# server node, all addressable from the client by name:
Enum.each(1..5, fn n -> Counter.DynamicSupervisor.start_worker("c#{n}") end)
Enum.each(1..5, fn n -> Counter.Worker.inc("c#{n}", n) end)
Enum.map(1..5, fn n -> Counter.Worker.value("c#{n}") end)
# -> [1, 2, 3, 4, 5]
# kill a worker on the server node and watch it come back with its value
# intact (DynamicSupervisor restarts it, ETS on the server supplies the
# last known value in init/1):
# on the SERVER node:
Process.exit(:global.whereis_name("c1"), :kill)
# back on the CLIENT node:
Counter.Worker.value("c1")
# -> still 1 (reloaded from the server's ETS table in init/1, same pattern
# as lecture 05's local DynamicSupervisor/Registry/ETS combo - just now the
# "local" side is a different machine)
### ---------------------------------------------------------------------- ###
### Misc / troubleshooting
### ---------------------------------------------------------------------- ###
# cookie mismatch or wrong hostname are the two most common reasons
# Node.connect/1 silently returns false - check `iex --sname` output for
# the exact full node name, and make sure both nodes used the same --cookie
# run a mix app's tests / compile without a full iex session:
# mix test
# mix compile
# mix run --no-halt # boots the Application and just hangs there running