diff --git a/lectures/06/counter/dynamic/client/dynamic_supervisor.ex b/lectures/06/counter/dynamic/client/dynamic_supervisor.ex new file mode 100644 index 0000000..92b6eb7 --- /dev/null +++ b/lectures/06/counter/dynamic/client/dynamic_supervisor.ex @@ -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 diff --git a/lectures/06/counter/dynamic/client/worker.ex b/lectures/06/counter/dynamic/client/worker.ex new file mode 100644 index 0000000..bf44f35 --- /dev/null +++ b/lectures/06/counter/dynamic/client/worker.ex @@ -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 diff --git a/lectures/06/counter/dynamic/server/.formatter.exs b/lectures/06/counter/dynamic/server/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/lectures/06/counter/dynamic/server/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/lectures/06/counter/dynamic/server/.gitignore b/lectures/06/counter/dynamic/server/.gitignore new file mode 100644 index 0000000..f578f6f --- /dev/null +++ b/lectures/06/counter/dynamic/server/.gitignore @@ -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 + diff --git a/lectures/06/counter/dynamic/server/README.md b/lectures/06/counter/dynamic/server/README.md new file mode 100644 index 0000000..a34fc98 --- /dev/null +++ b/lectures/06/counter/dynamic/server/README.md @@ -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 . + diff --git a/lectures/06/counter/dynamic/server/lib/counter.ex b/lectures/06/counter/dynamic/server/lib/counter.ex new file mode 100644 index 0000000..2e364fe --- /dev/null +++ b/lectures/06/counter/dynamic/server/lib/counter.ex @@ -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 diff --git a/lectures/06/counter/dynamic/server/lib/counter/application.ex b/lectures/06/counter/dynamic/server/lib/counter/application.ex new file mode 100644 index 0000000..f43244c --- /dev/null +++ b/lectures/06/counter/dynamic/server/lib/counter/application.ex @@ -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 diff --git a/lectures/06/counter/dynamic/server/lib/counter/dynamic_supervisor.ex b/lectures/06/counter/dynamic/server/lib/counter/dynamic_supervisor.ex new file mode 100644 index 0000000..ae60bd5 --- /dev/null +++ b/lectures/06/counter/dynamic/server/lib/counter/dynamic_supervisor.ex @@ -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 diff --git a/lectures/06/counter/dynamic/server/lib/counter/worker.ex b/lectures/06/counter/dynamic/server/lib/counter/worker.ex new file mode 100644 index 0000000..cfeeddb --- /dev/null +++ b/lectures/06/counter/dynamic/server/lib/counter/worker.ex @@ -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 diff --git a/lectures/06/counter/dynamic/server/mix.exs b/lectures/06/counter/dynamic/server/mix.exs new file mode 100644 index 0000000..88e13c1 --- /dev/null +++ b/lectures/06/counter/dynamic/server/mix.exs @@ -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 diff --git a/lectures/06/counter/dynamic/server/test/counter_test.exs b/lectures/06/counter/dynamic/server/test/counter_test.exs new file mode 100644 index 0000000..99884d6 --- /dev/null +++ b/lectures/06/counter/dynamic/server/test/counter_test.exs @@ -0,0 +1,8 @@ +defmodule CounterTest do + use ExUnit.Case + doctest Counter + + test "greets the world" do + assert Counter.hello() == :world + end +end diff --git a/lectures/06/counter/dynamic/server/test/test_helper.exs b/lectures/06/counter/dynamic/server/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/lectures/06/counter/dynamic/server/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/lectures/06/counter/static/.formatter.exs b/lectures/06/counter/static/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/lectures/06/counter/static/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/lectures/06/counter/static/.gitignore b/lectures/06/counter/static/.gitignore new file mode 100644 index 0000000..3cf10e9 --- /dev/null +++ b/lectures/06/counter/static/.gitignore @@ -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 + diff --git a/lectures/06/counter/static/client/worker.ex b/lectures/06/counter/static/client/worker.ex new file mode 100644 index 0000000..50dfb3c --- /dev/null +++ b/lectures/06/counter/static/client/worker.ex @@ -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 + diff --git a/lectures/06/counter/static/server/README.md b/lectures/06/counter/static/server/README.md new file mode 100644 index 0000000..f15a4ad --- /dev/null +++ b/lectures/06/counter/static/server/README.md @@ -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 . + diff --git a/lectures/06/counter/static/server/counter.db b/lectures/06/counter/static/server/counter.db new file mode 100644 index 0000000..908866a --- /dev/null +++ b/lectures/06/counter/static/server/counter.db @@ -0,0 +1 @@ +ƒa \ No newline at end of file diff --git a/lectures/06/counter/static/server/lib/counter.ex b/lectures/06/counter/static/server/lib/counter.ex new file mode 100644 index 0000000..2e364fe --- /dev/null +++ b/lectures/06/counter/static/server/lib/counter.ex @@ -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 diff --git a/lectures/06/counter/static/server/lib/counter/application.ex b/lectures/06/counter/static/server/lib/counter/application.ex new file mode 100644 index 0000000..bb16218 --- /dev/null +++ b/lectures/06/counter/static/server/lib/counter/application.ex @@ -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 diff --git a/lectures/06/counter/static/server/lib/counter/store.ex b/lectures/06/counter/static/server/lib/counter/store.ex new file mode 100644 index 0000000..8055c70 --- /dev/null +++ b/lectures/06/counter/static/server/lib/counter/store.ex @@ -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 + + + diff --git a/lectures/06/counter/static/server/lib/counter/worker.ex b/lectures/06/counter/static/server/lib/counter/worker.ex new file mode 100644 index 0000000..db0e29a --- /dev/null +++ b/lectures/06/counter/static/server/lib/counter/worker.ex @@ -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 + diff --git a/lectures/06/counter/static/server/mix.exs b/lectures/06/counter/static/server/mix.exs new file mode 100644 index 0000000..6bba5ee --- /dev/null +++ b/lectures/06/counter/static/server/mix.exs @@ -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 diff --git a/lectures/06/counter/static/server/test/counter_test.exs b/lectures/06/counter/static/server/test/counter_test.exs new file mode 100644 index 0000000..99884d6 --- /dev/null +++ b/lectures/06/counter/static/server/test/counter_test.exs @@ -0,0 +1,8 @@ +defmodule CounterTest do + use ExUnit.Case + doctest Counter + + test "greets the world" do + assert Counter.hello() == :world + end +end diff --git a/lectures/06/counter/static/server/test/test_helper.exs b/lectures/06/counter/static/server/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/lectures/06/counter/static/server/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/lectures/06/echo/echo.ex b/lectures/06/echo/echo.ex new file mode 100644 index 0000000..00ed438 --- /dev/null +++ b/lectures/06/echo/echo.ex @@ -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 + diff --git a/lectures/06/echo/echo_client.ex b/lectures/06/echo/echo_client.ex new file mode 100644 index 0000000..c247ad8 --- /dev/null +++ b/lectures/06/echo/echo_client.ex @@ -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 diff --git a/lectures/06/java/EchoClient.java b/lectures/06/java/EchoClient.java new file mode 100644 index 0000000..9513f21 --- /dev/null +++ b/lectures/06/java/EchoClient.java @@ -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); + } + } + } +} diff --git a/lectures/06/notes.ex b/lectures/06/notes.ex new file mode 100644 index 0000000..bb078df --- /dev/null +++ b/lectures/06/notes.ex @@ -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