lecture 06

This commit is contained in:
SowinskiBraeden committed 2026-10-20 10:10:30 -07:00
1 parent 8777cee0d0
commit d729786437
28 files changed
+825

No files matched your search

@@ -0,0 +1,13 @@
defmodule Counter.DynamicSupervisor do
@name {:global, __MODULE__}
def start_link(_) do
DynamicSupervisor.start_link(__MODULE__, nil, name: @name)
end
def start_worker(name) do
DynamicSupervisor.start_child(@name, %{
id: Counter.Worker,
start: {Counter.Worker, :start_link, [name]}})
end
end
@@ -0,0 +1,18 @@
defmodule Counter.Worker do
# client
def start_link(name) do
GenServer.start_link(__MODULE__, name, name: via(name))
end
def value(name) do
GenServer.call(via(name), :value)
end
def inc(name, amt \\ 1) do
GenServer.cast(via(name), {:inc, amt})
end
defp via(name) do
{:global, name}
end
end
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
@@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Temporary files, for example, from tests.
/tmp/
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
dynamic_counters-*.tar
@@ -0,0 +1,21 @@
# Counter
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `dynamic_counters` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:dynamic_counters, "~> 0.1.0"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/dynamic_counters>.
@@ -0,0 +1,18 @@
defmodule Counter do
@moduledoc """
Documentation for `Counter`.
"""
@doc """
Hello world.
## Examples
iex> Counter.hello()
:world
"""
def hello do
:world
end
end
@@ -0,0 +1,26 @@
defmodule Counter.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
@registry Counter.Registry
@table Counter.Table
use Application
@impl true
def start(_type, _args) do
children = [
{Registry, [name: @registry, keys: :unique]},
# Starts a worker by calling: Counter.Worker.start_link(arg)
# {DynamicSupervisor, name: Counter.DynamicSupervisor, strategy: :one_for_one}
Counter.DynamicSupervisor
]
:ets.new(@table, [:named_table, :public])
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Counter.Supervisor]
Supervisor.start_link(children, opts)
end
end
@@ -0,0 +1,17 @@
defmodule Counter.DynamicSupervisor do
use DynamicSupervisor
@name {:global, __MODULE__}
def start_link(_) do
DynamicSupervisor.start_link(__MODULE__, nil, name: @name)
end
def start_worker(name) do
DynamicSupervisor.start_child(@name, {Counter.Worker, name})
end
@impl true
def init(_) do
DynamicSupervisor.init(strategy: :one_for_one)
end
end
@@ -0,0 +1,47 @@
defmodule Counter.Worker do
use GenServer
@table Counter.Table
# client
def start_link(name) do
GenServer.start_link(__MODULE__, name, name: via(name))
end
def value(name) do
GenServer.call(via(name), :value)
end
def inc(name, amt \\ 1) do
GenServer.cast(via(name), {:inc, amt})
end
defp via(name) do
{:global, name}
end
# server
@impl true
def init(arg) do
name = {__MODULE__, arg}
value = case :ets.lookup(@table, name) do
[] -> 0
[{_, v}] -> v
end
{:ok, {name, value}}
end
@impl true
def handle_call(:value, _from, {_, value} = state) do
{:reply, value, state}
end
@impl true
def handle_cast({:inc, amt}, {name, value}) do
{:noreply, {name, value + amt}}
end
@impl true
def terminate(_reason, state) do
:ets.insert(@table, state)
end
end
@@ -0,0 +1,29 @@
defmodule Counter.MixProject do
use Mix.Project
def project do
[
app: :dynamic_counters,
version: "0.1.0",
elixir: "~> 1.20",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger],
mod: {Counter.Application, []}
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end
@@ -0,0 +1,8 @@
defmodule CounterTest do
use ExUnit.Case
doctest Counter
test "greets the world" do
assert Counter.hello() == :world
end
end
@@ -0,0 +1 @@
ExUnit.start()
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
+24
View File
@@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Temporary files, for example, from tests.
/tmp/
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
counter-*.tar
@@ -0,0 +1,17 @@
defmodule Counter.Worker do
@name {:global, __MODULE__}
# client
def start_link(n \\ 0) do
GenServer.start_link(__MODULE__, n, name: @name)
end
def inc(amt \\ 1) do
GenServer.cast(@name, {:inc, amt})
end
def value() do
GenServer.call(@name, :value)
end
end
@@ -0,0 +1,21 @@
# Counter
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `counter` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:counter, "~> 0.1.0"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/counter>.
@@ -0,0 +1 @@
ƒa
@@ -0,0 +1,18 @@
defmodule Counter do
@moduledoc """
Documentation for `Counter`.
"""
@doc """
Hello world.
## Examples
iex> Counter.hello()
:world
"""
def hello do
:world
end
end
@@ -0,0 +1,21 @@
defmodule Counter.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
# Starts a worker by calling: Counter.Worker.start_link(arg)
{Counter.Store, "counter.db"},
{Counter.Worker, 0}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Counter.Supervisor]
Supervisor.start_link(children, opts)
end
end
@@ -0,0 +1,41 @@
defmodule Counter.Store do
use GenServer
# client
def start_link(file) do
GenServer.start_link(__MODULE__, file, name: __MODULE__)
end
def put(value) do
GenServer.cast(__MODULE__, {:put, value})
end
def get() do
GenServer.call(__MODULE__, :get)
end
# server
@impl true
def init(file) do
{:ok, file}
end
@impl true
def handle_call(:get, _from, file) do
reply =
case File.read(file) do
{:ok, content} -> :erlang.binary_to_term(content)
_ -> nil
end
{:reply, reply, file}
end
@impl true
def handle_cast({:put, value}, file) do
File.write(file, :erlang.term_to_binary(value))
{:noreply, file}
end
end
@@ -0,0 +1,47 @@
defmodule Counter.Worker do
use GenServer
@store Counter.Store
@name {:global, __MODULE__}
# client
def start_link(n \\ 0) do
GenServer.start_link(__MODULE__, n, name: @name)
end
def inc(amt \\ 1) do
GenServer.cast(@name, {:inc, amt})
end
def value() do
GenServer.call(@name, :value)
end
# server
@impl true
def init(arg) do
{:ok, @store.get() || arg}
end
@impl true
def handle_call(:value, _from, state) do
{:reply, state, state}
end
@impl true
def handle_cast({:inc, amt}, state) do
{:noreply, state + amt}
end
# for demonstration only
@impl true
def handle_info(:reset, _state) do
{:noreply, 0}
end
@impl true
def terminate(_reason, state) do
@store.put(state)
end
end
+29
View File
@@ -0,0 +1,29 @@
defmodule Counter.MixProject do
use Mix.Project
def project do
[
app: :counter,
version: "0.1.0",
elixir: "~> 1.20",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger],
mod: {Counter.Application, []}
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end
@@ -0,0 +1,8 @@
defmodule CounterTest do
use ExUnit.Case
doctest Counter
test "greets the world" do
assert Counter.hello() == :world
end
end
@@ -0,0 +1 @@
ExUnit.start()
+144
View File
@@ -0,0 +1,144 @@
defmodule Echo.Active do
def start(port \\ 6666) do
opts = [:binary, active: true, packet: :line, reuseaddr: true]
{:ok, listen} = :gen_tcp.listen(port, opts)
accept(listen)
end
def accept(listen) do
{:ok, socket} = :gen_tcp.accept(listen) # data delivered as messages in the mailbox of the
# process that calls accept
spawn(fn -> accept(listen) end)
loop(socket)
end
defp loop(socket) do
receive do
{:tcp, ^socket, data} ->
:gen_tcp.send(socket, data)
loop(socket)
{:tcp_closed, ^socket} ->
:gen_tcp.close(socket)
{:tcp_error, ^socket, _reason} ->
:gen_tcp.close(socket)
end
end
end
defmodule Echo.Passive do
def start(port \\ 6666) do
opts = [:binary, active: false, packet: :line, reuseaddr: true] # note: active: false
{:ok, listen} = :gen_tcp.listen(port, opts)
accept(listen)
end
def accept(listen) do
{:ok, socket} = :gen_tcp.accept(listen) # data delivered as messages in the mailbox of the
# process that calls accept
spawn(fn -> accept(listen) end)
loop(socket)
end
def loop(socket) do
case :gen_tcp.recv(socket, 0) do
{:ok, data} ->
:gen_tcp.send(socket, data)
loop(socket)
{:error, _reason} ->
:gen_tcp.close(socket)
end
end
end
defmodule Echo.Hybrid do
def start(port \\ 6666) do
opts = [:binary, active: :once, packet: :line, reuseaddr: true]
{:ok, listen} = :gen_tcp.listen(port, opts)
accept(listen)
end
def accept(listen) do
{:ok, socket} = :gen_tcp.accept(listen) # data delivered as messages in the mailbox of the
# process that calls accept (the controlling process)
spawn(fn -> accept(listen) end)
loop(socket)
end
defp loop(socket) do
receive do
{:tcp, ^socket, data} ->
:inet.setopts(socket, active: :once)
:gen_tcp.send(socket, data)
loop(socket)
{:tcp_closed, ^socket} ->
:gen_tcp.close(socket)
{:tcp_error, ^socket, _reason} ->
:gen_tcp.close(socket)
end
end
end
defmodule Echo do
use GenServer
require Logger
def start(port \\ 6666) do
GenServer.start(__MODULE__, port)
end
@impl true
def init(port) do
opts = [:binary, active: true, packet: :line, reuseaddr: true]
{:ok, listen} = :gen_tcp.listen(port, opts)
send(self(), :accept)
{:ok, listen}
end
@impl true
def handle_info(:accept, listen) do
{:ok, socket} = :gen_tcp.accept(listen)
log(socket, "accepted connection")
{:ok, pid} = Echo.Worker.start(socket)
:gen_tcp.controlling_process(socket, pid)
send(self(), :accept)
{:noreply, listen}
end
defp log(socket, msg) do
Logger.info("#{inspect self()}: #{msg} #{inspect socket}")
end
end
defmodule Echo.Worker do
use GenServer
require Logger
def start(socket) do
GenServer.start(__MODULE__, socket)
end
@impl true
def init(socket) do
log(socket, "started worker")
{:ok, socket}
end
@impl true
def handle_info({:tcp, socket, data}, socket) do
:inet.setopts(socket, active: :once)
:gen_tcp.send(socket, data)
{:noreply, socket}
end
@impl true
def handle_info({:tcp_closed, socket}, socket) do
:gen_tcp.close(socket)
log(socket, "closed connection")
{:stop, :normal, socket}
end
defp log(socket, msg) do
Logger.info("#{inspect self()}: #{msg} #{inspect socket}")
end
end
+31
View File
@@ -0,0 +1,31 @@
defmodule Echo.Client do
def connect(host, port \\ 6666) do
opts = [active: false, packet: :line]
{:ok, socket} = :gen_tcp.connect(host, port, opts)
loop(socket)
end
defp loop(socket) do
case IO.gets("> ") do
:eof ->
:gen_tcp.close(socket)
line ->
:gen_tcp.send(socket, line)
case :gen_tcp.recv(socket, 0) do
{:ok, data} ->
IO.write(data)
loop(socket)
{:error, _} ->
:gen_tcp.close(socket)
end
end
end
end
with [host, port | _] <- System.argv(),
{port, _} <- Integer.parse(port)
do
Echo.Client.connect(String.to_atom(host), port)
else
_ -> Echo.Client.connect(:"127.0.0.1", 6666)
end
+32
View File
@@ -0,0 +1,32 @@
import java.io.*;
import java.net.*;
class EchoClient {
public static void main(String... args) throws IOException {
var host = "127.0.0.1";
var port = 6666;
try (
var s = new Socket(host, port);
var in =
new BufferedReader(new InputStreamReader(s.getInputStream()));
var out =
new PrintWriter(new OutputStreamWriter(s.getOutputStream()), true);
var stdin =
new BufferedReader(new InputStreamReader(System.in));
) {
String line, reply;
while (true) {
System.out.print("> ");
if ((line = stdin.readLine()) == null) {
break;
}
out.println(line);
if ((reply = in.readLine()) == null) {
break;
}
System.out.println(reply);
}
}
}
}
+160
View File
@@ -0,0 +1,160 @@
# 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