# Book 3 — Advanced OTP & Application Design

```elixir
Mix.install([
  {:kino, "~> 0.14"}
])
```

## Setup

Companion to `book3-advanced-otp.html`. You'll use Task for parallel fan-out, build the Registry + DynamicSupervisor process-per-entity pattern, and make an ETS cache that survives crashes.

## 1. Task — parallelism in one line

```elixir
# Simulate slow external calls (e.g., HTTP requests):
fake_fetch = fn id ->
  Process.sleep(Enum.random(200..500))
  %{id: id, temp_c: Enum.random(-10..35)}
end

# Sequential: ~3.5s for 10 calls
{micros_seq, _} = :timer.tc(fn -> Enum.map(1..10, fake_fetch) end)

# Parallel with Task.async_stream: ~0.5s (bounded to 10 concurrent)
{micros_par, results} =
  :timer.tc(fn ->
    1..10
    |> Task.async_stream(fake_fetch, max_concurrency: 10)
    |> Enum.map(fn {:ok, r} -> r end)
  end)

%{
  sequential_ms: div(micros_seq, 1000),
  parallel_ms: div(micros_par, 1000),
  results: length(results)
}
```

```elixir
# Task.async links to the caller — a crashing task crashes YOU (fail-fast).
# Run this to see it (the notebook cell will error):
task = Task.async(fn -> raise "task exploded" end)
Task.await(task)
```

```elixir
# For fire-and-forget work that shouldn't take you down: Task.Supervisor.
{:ok, task_sup} = Task.Supervisor.start_link()

Task.Supervisor.start_child(task_sup, fn ->
  raise "this crash is isolated and merely logged"
end)

Process.sleep(100)
"still alive — the task died under its own supervisor, not linked to us"
```

### Exercise 3.1

Using `Task.async_stream` with `max_concurrency: 3` and `timeout: 400`, map `fake_fetch` over `1..9`, but pass `on_timeout: :kill_task` and count how many results are `{:ok, _}` vs `{:exit, :timeout}`. (Some calls sleep up to 500ms, so some will time out.)

```elixir
# your code here
```

<details>
<summary>💡 Solution</summary>

```elixir
1..9
|> Task.async_stream(fake_fetch, max_concurrency: 3, timeout: 400, on_timeout: :kill_task)
|> Enum.frequencies_by(fn
  {:ok, _} -> :ok
  {:exit, :timeout} -> :timeout
end)
```

</details>

## 2. Registry + DynamicSupervisor: process-per-entity

The most important architecture pattern in Elixir. One supervised GenServer per user session, addressable by user id:

```elixir
defmodule Session do
  use GenServer

  # via-tuple: name the process by an arbitrary term in the Registry
  defp via(user_id), do: {:via, Registry, {SessionRegistry, {:session, user_id}}}

  def start_link(user_id) do
    GenServer.start_link(__MODULE__, user_id, name: via(user_id))
  end

  def add_to_cart(user_id, item), do: GenServer.call(via(user_id), {:add, item})
  def cart(user_id), do: GenServer.call(via(user_id), :cart)
  def whoami(user_id), do: GenServer.call(via(user_id), :whoami)

  @impl true
  def init(user_id) do
    IO.puts("session for user #{user_id} starting: #{inspect(self())}")
    {:ok, %{user_id: user_id, cart: []}}
  end

  @impl true
  def handle_call({:add, item}, _from, state) do
    {:reply, :ok, update_in(state.cart, &[item | &1])}
  end

  def handle_call(:cart, _from, state), do: {:reply, Enum.reverse(state.cart), state}
  def handle_call(:whoami, _from, state), do: {:reply, {state.user_id, self()}, state}
end

# The two infrastructure pieces (normally in your Application tree):
{:ok, _} = Registry.start_link(keys: :unique, name: SessionRegistry)
{:ok, session_sup} = DynamicSupervisor.start_link(strategy: :one_for_one)

# The "find or start" helper:
defmodule Sessions do
  def ensure_started(sup, user_id) do
    case Registry.lookup(SessionRegistry, {:session, user_id}) do
      [{pid, _}] -> {:ok, pid}
      [] -> DynamicSupervisor.start_child(sup, {Session, user_id})
    end
  end
end

Sessions.ensure_started(session_sup, 1)
Sessions.ensure_started(session_sup, 2)
Sessions.ensure_started(session_sup, 1)  # already running — same pid, no duplicate

Session.add_to_cart(1, "keyboard")
Session.add_to_cart(1, "mouse")
Session.add_to_cart(2, "monitor")

{Session.cart(1), Session.cart(2)}
```

```elixir
# Visualize the dynamic tree:
Kino.Process.render_sup_tree(session_sup)
```

```elixir
# Kill user 1's session — user 2 is untouched, and the supervisor restarts user 1
# (with empty cart — fresh init!). Registry cleans up + re-registers automatically.
{_, pid_before} = Session.whoami(1)
Process.exit(pid_before, :kill)
Process.sleep(100)

{_, pid_after} = Session.whoami(1)

%{
  user1_restarted: pid_before != pid_after,
  user1_cart_after_crash: Session.cart(1),
  user2_cart_untouched: Session.cart(2)
}
```

### Exercise 3.2

Spawn sessions for users `1..50` (use `Enum.each` + `Sessions.ensure_started`), then use `Registry.count(SessionRegistry)` and `DynamicSupervisor.count_children(session_sup)` to verify. Bonus: use `Registry.select` or `Registry.lookup` to fetch user 25's pid directly.

```elixir
# your code here
```

<details>
<summary>💡 Solution</summary>

```elixir
Enum.each(1..50, &Sessions.ensure_started(session_sup, &1))

%{
  registry: Registry.count(SessionRegistry),
  supervisor: DynamicSupervisor.count_children(session_sup),
  user25: Registry.lookup(SessionRegistry, {:session, 25})
}
```

</details>

## 3. ETS — shared memory done right

```elixir
# A public named table any process can read/write:
:ets.new(:cache, [:set, :public, :named_table, read_concurrency: true])

:ets.insert(:cache, {:user_42, %{name: "Ada"}})
:ets.insert(:cache, {:user_43, %{name: "Grace"}})

:ets.lookup(:cache, :user_42)
```

```elixir
# How much faster than a GenServer? Benchmark 100k reads of each:
defmodule Holder do
  use GenServer
  def start_link(v), do: GenServer.start_link(__MODULE__, v, name: __MODULE__)
  def get, do: GenServer.call(__MODULE__, :get)
  @impl true
  def init(v), do: {:ok, v}
  @impl true
  def handle_call(:get, _from, v), do: {:reply, v, v}
end

{:ok, _} = Holder.start_link(%{name: "Ada"})

{gs_micros, _} = :timer.tc(fn -> for _ <- 1..100_000, do: Holder.get() end)
{ets_micros, _} = :timer.tc(fn -> for _ <- 1..100_000, do: :ets.lookup(:cache, :user_42) end)

%{
  genserver_reads_ms: div(gs_micros, 1000),
  ets_reads_ms: div(ets_micros, 1000),
  speedup: Float.round(gs_micros / ets_micros, 1)
}
```

```elixir
# Atomic counters — the rate-limiter primitive:
:ets.new(:hits, [:set, :public, :named_table])

# 1000 processes all incrementing concurrently — no locks, no races:
1..1000
|> Task.async_stream(fn _ -> :ets.update_counter(:hits, :page, 1, {:page, 0}) end)
|> Stream.run()

:ets.lookup(:hits, :page)
```

```elixir
# The crash-survival pattern: table owned by a stable parent, used by a crashy worker.
# (In real apps: give the table an :heir, or have the supervisor own it.)
defmodule CrashyCacheUser do
  use GenServer
  def start_link(_), do: GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
  def put(k, v), do: GenServer.call(__MODULE__, {:put, k, v})
  def crash, do: GenServer.cast(__MODULE__, :crash)

  @impl true
  def init(:ok), do: {:ok, nil}
  @impl true
  def handle_call({:put, k, v}, _from, s) do
    :ets.insert(:durable, {k, v})
    {:reply, :ok, s}
  end
  @impl true
  def handle_cast(:crash, s) do
    raise "boom"
    {:noreply, s}
  end
end

# Notebook process owns the table (stable); worker just uses it:
if :ets.whereis(:durable) == :undefined do
  :ets.new(:durable, [:set, :public, :named_table])
end

{:ok, _sup} = Supervisor.start_link([CrashyCacheUser], strategy: :one_for_one)

CrashyCacheUser.put(:precious, "survives crashes")
CrashyCacheUser.crash()
Process.sleep(100)

# Worker restarted with fresh (nil) state — but the DATA is still here:
:ets.lookup(:durable, :precious)
```

This is the answer to Book 2's bank losing Ada's balance: keep precious data in a store that outlives the worker.

## 4. The Application tree, assembled

In a real Mix project you'd declare everything we built by hand in `lib/my_app/application.ex`. Here's the equivalent, as one supervisor:

```elixir
defmodule DemoApp do
  def start do
    children = [
      {Registry, keys: :unique, name: DemoApp.Registry},
      {DynamicSupervisor, name: DemoApp.SessionSup, strategy: :one_for_one},
      {Task.Supervisor, name: DemoApp.TaskSup}
    ]

    Supervisor.start_link(children, strategy: :one_for_one, name: DemoApp.Supervisor)
  end
end

{:ok, app_sup} = DemoApp.start()
Kino.Process.render_sup_tree(app_sup)
```

Read the tree: infrastructure first (Registry), then things that depend on it. In a Phoenix app the same file lists `Repo`, `PubSub`, `Endpoint` — you'll recognize it immediately in Book 4.

### Exercise 3.3 (mini-project)

Build a **word-count service**: a `DynamicSupervisor`-spawned `Counter` GenServer per document id (via `DemoApp.Registry`), each exposing `add_text(doc_id, text)` (casts, splits words, accumulates counts in state) and `top(doc_id, n)` (call, returns the n most frequent words). Useful: `String.split/1`, `Enum.frequencies/1`, `Map.merge/3`, `Enum.sort_by/3`.

```elixir
# your code here
```

<details>
<summary>💡 Solution</summary>

```elixir
defmodule DocCounter do
  use GenServer

  defp via(id), do: {:via, Registry, {DemoApp.Registry, {:doc, id}}}

  def ensure(id) do
    case Registry.lookup(DemoApp.Registry, {:doc, id}) do
      [{pid, _}] -> {:ok, pid}
      [] -> DynamicSupervisor.start_child(DemoApp.SessionSup, %{
        id: {:doc, id}, start: {__MODULE__, :start_link, [id]}
      })
    end
  end

  def start_link(id), do: GenServer.start_link(__MODULE__, id, name: via(id))
  def add_text(id, text), do: GenServer.cast(via(id), {:add, text})
  def top(id, n), do: GenServer.call(via(id), {:top, n})

  @impl true
  def init(_id), do: {:ok, %{}}

  @impl true
  def handle_cast({:add, text}, counts) do
    new = text |> String.downcase() |> String.split() |> Enum.frequencies()
    {:noreply, Map.merge(counts, new, fn _k, a, b -> a + b end)}
  end

  @impl true
  def handle_call({:top, n}, _from, counts) do
    top = counts |> Enum.sort_by(fn {_w, c} -> -c end) |> Enum.take(n)
    {:reply, top, counts}
  end
end

DocCounter.ensure("readme")
DocCounter.add_text("readme", "the beam the otp the process")
DocCounter.add_text("readme", "process process supervision")
DocCounter.top("readme", 3)
```

</details>

## Wrap-up

You now hold the full OTP toolbox and the canonical patterns: parallel fan-out, process-per-entity, ETS-backed durability, declarative boot trees. **Book 4** shows that Phoenix is nothing more than these tools pointed at HTTP.
