# Book 2 — OTP Core: GenServer & Supervisors

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

## Setup

Read `book2-otp-core.html` first (or side by side). Here you'll build real GenServers, put them under real supervisors, crash them, and watch the tree heal — with live visualizations.

## 1. Your first GenServer

The `Counter` from Book 1, rebuilt on the behaviour. Compare the amount of machinery you *don't* have to write:

```elixir
defmodule Counter do
  use GenServer

  ## Client API — these run in the CALLER's process
  def start_link(initial \\ 0, opts \\ []) do
    GenServer.start_link(__MODULE__, initial, opts)
  end

  def increment(pid), do: GenServer.cast(pid, :increment)
  def value(pid), do: GenServer.call(pid, :value)

  ## Server callbacks — these run INSIDE the GenServer process
  @impl true
  def init(initial), do: {:ok, initial}

  @impl true
  def handle_cast(:increment, state), do: {:noreply, state + 1}

  @impl true
  def handle_call(:value, _from, state), do: {:reply, state, state}
end

{:ok, counter} = Counter.start_link(0)

Counter.increment(counter)
Counter.increment(counter)
Counter.increment(counter)
Counter.value(counter)
```

```elixir
# Proof that client API runs in the caller: self() here...
IO.inspect(self(), label: "notebook process")
IO.inspect(counter, label: "counter process")

# ...and GenServer.call is just a wrapped send + receive under the hood.
GenServer.call(counter, :value)
```

## 2. A realistic GenServer: a TTL cache

State can be any value. Here it's a map, plus `handle_info/2` for a timer message — the third callback type, for messages that aren't calls or casts:

```elixir
defmodule TTLCache do
  use GenServer

  @cleanup_every 2_000

  def start_link(opts \\ []), do: GenServer.start_link(__MODULE__, :ok, opts)
  def put(pid, key, value, ttl_ms), do: GenServer.call(pid, {:put, key, value, ttl_ms})
  def get(pid, key), do: GenServer.call(pid, {:get, key})

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

  @impl true
  def handle_call({:put, key, value, ttl_ms}, _from, state) do
    expires_at = System.monotonic_time(:millisecond) + ttl_ms
    {:reply, :ok, Map.put(state, key, {value, expires_at})}
  end

  def handle_call({:get, key}, _from, state) do
    now = System.monotonic_time(:millisecond)

    case Map.get(state, key) do
      {value, expires_at} when expires_at > now -> {:reply, {:ok, value}, state}
      _ -> {:reply, :miss, state}
    end
  end

  @impl true
  def handle_info(:cleanup, state) do
    now = System.monotonic_time(:millisecond)
    alive = Map.filter(state, fn {_k, {_v, exp}} -> exp > now end)
    schedule_cleanup()
    {:noreply, alive}
  end

  defp schedule_cleanup, do: Process.send_after(self(), :cleanup, @cleanup_every)
end

{:ok, cache} = TTLCache.start_link()
TTLCache.put(cache, :greeting, "hello", 1_500)
TTLCache.get(cache, :greeting)
```

```elixir
# Wait past the TTL and it's gone:
Process.sleep(1_600)
TTLCache.get(cache, :greeting)
```

**Note the pattern:** `Process.send_after(self(), ...)` + `handle_info` is the idiomatic GenServer timer. No setInterval — the timer is just a message.

### Exercise 2.1

Add a `delete(pid, key)` function + callback to `TTLCache` (copy the module into the cell below and extend it). Bonus: add `stats(pid)` returning `%{size: n}`.

```elixir
# your extended TTLCache here
```

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

Add to the client API:

```elixir
def delete(pid, key), do: GenServer.call(pid, {:delete, key})
def stats(pid), do: GenServer.call(pid, :stats)
```

And the callbacks:

```elixir
def handle_call({:delete, key}, _from, state), do: {:reply, :ok, Map.delete(state, key)}
def handle_call(:stats, _from, state), do: {:reply, %{size: map_size(state)}, state}
```

</details>

## 3. call vs cast, demonstrated

```elixir
defmodule SlowServer do
  use GenServer
  def start_link(_), do: GenServer.start_link(__MODULE__, :ok)

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

  @impl true
  def handle_cast(:work, n) do
    Process.sleep(100)   # simulate 100ms of work
    {:noreply, n + 1}
  end

  @impl true
  def handle_call(:count, _from, n), do: {:reply, n, n}
end

{:ok, slow} = SlowServer.start_link([])

# Fire 50 casts — returns instantly:
{micros, _} = :timer.tc(fn ->
  for _ <- 1..50, do: GenServer.cast(slow, :work)
end)

IO.puts("50 casts sent in #{micros / 1000} ms (fire-and-forget)")

# But the mailbox is now a backlog:
Process.info(slow, :message_queue_len)
```

```elixir
# A call now has to WAIT behind all that queued work (one message at a time!).
# 50 casts x 100ms each ≈ 5s of queue — so this call takes ~5s. The default call
# timeout is 5s; we pass 10_000 to be safe:
{micros, count} = :timer.tc(fn -> GenServer.call(slow, :count, 10_000) end)
"call waited #{Float.round(micros / 1_000_000, 2)}s behind the cast backlog; processed count: #{count}"
```

That's the whole call-vs-cast story in one experiment: casts pile up invisibly; calls make producers feel the queue (back-pressure).

## 4. Supervisors

```elixir
# A crashy worker to supervise:
defmodule Flaky do
  use GenServer

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

  def crash(name), do: GenServer.cast(name, :crash)

  @impl true
  def init(name) do
    IO.puts("Flaky #{name} starting as #{inspect(self())}")
    {:ok, name}
  end

  @impl true
  def handle_cast(:crash, name) do
    raise "#{name} exploded!"
    {:noreply, name}
  end
end

children = [
  Supervisor.child_spec({Flaky, :worker_a}, id: :a),
  Supervisor.child_spec({Flaky, :worker_b}, id: :b),
  Supervisor.child_spec({Flaky, :worker_c}, id: :c)
]

{:ok, sup} = Supervisor.start_link(children, strategy: :one_for_one)
Supervisor.which_children(sup)
```

```elixir
# 🌳 Visualize the live supervision tree (Kino renders it as a diagram):
Kino.Process.render_sup_tree(sup)
```

```elixir
# Now crash worker_b and watch it come back with a NEW pid:
before = Supervisor.which_children(sup)
Flaky.crash(:worker_b)
Process.sleep(100)

after_crash = Supervisor.which_children(sup)

IO.inspect(before, label: "before")
IO.inspect(after_crash, label: "after ")
"worker_b has a new PID — restarted by the supervisor with fresh state"
```

```elixir
# Restart intensity: crash it more than 3 times in 5 seconds and the SUPERVISOR
# gives up and dies too (escalation). We monitor the supervisor to observe it:
ref = Process.monitor(sup)

for _ <- 1..4 do
  try do
    Flaky.crash(:worker_b)
  catch
    :exit, _ -> :ok
  end

  Process.sleep(50)
end

receive do
  {:DOWN, ^ref, :process, ^sup, reason} ->
    "supervisor itself died: #{inspect(reason)} — failure escalated up the tree"
after
  2_000 -> "supervisor survived (crashes were spread out enough)"
end
```

## 5. Strategies compared

```elixir
# Same three workers under :rest_for_one — crash B, and B + C restart, A survives.
children = [
  Supervisor.child_spec({Flaky, :rfo_a}, id: :a),
  Supervisor.child_spec({Flaky, :rfo_b}, id: :b),
  Supervisor.child_spec({Flaky, :rfo_c}, id: :c)
]

{:ok, sup2} = Supervisor.start_link(children, strategy: :rest_for_one)

pids_before = for {id, pid, _, _} <- Supervisor.which_children(sup2), do: {id, pid}

Flaky.crash(:rfo_b)
Process.sleep(100)

pids_after = for {id, pid, _, _} <- Supervisor.which_children(sup2), do: {id, pid}

Enum.zip(pids_before, pids_after)
|> Enum.map(fn {{id, p1}, {_, p2}} ->
  {id, if(p1 == p2, do: "survived ✅", else: "restarted 🔄")}
end)
```

### Exercise 2.2

Predict, then verify: with `strategy: :one_for_all`, crash `:worker_c` — which workers get new PIDs? Build it in the cell below using the pattern above.

```elixir
# your experiment here
```

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

All three restart. With `:one_for_all`, any child's death causes the supervisor to terminate **every** child and restart them all — use it when children can't function without each other.

</details>

## 6. Mini-project: supervised bank account

Put it together: a `Bank` GenServer holding balances, under a supervisor, with a deliberate bug — then observe what restart does to state.

```elixir
defmodule Bank do
  use GenServer

  def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  def deposit(who, amount), do: GenServer.call(__MODULE__, {:deposit, who, amount})
  def balance(who), do: GenServer.call(__MODULE__, {:balance, who})

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

  @impl true
  def handle_call({:deposit, who, amount}, _from, state) do
    # BUG: negative deposits crash us (on purpose — "let it crash")
    if amount < 0, do: raise("negative deposit!")
    state = Map.update(state, who, amount, &(&1 + amount))
    {:reply, {:ok, state[who]}, state}
  end

  def handle_call({:balance, who}, _from, state) do
    {:reply, Map.get(state, who, 0), state}
  end
end

{:ok, bank_sup} = Supervisor.start_link([Bank], strategy: :one_for_one)

Bank.deposit("ada", 100)
Bank.deposit("ada", 50)
Bank.balance("ada")
```

```elixir
# Crash it (the caller gets an exit — we catch it to keep the notebook running):
try do
  Bank.deposit("ada", -999)
catch
  :exit, _ -> "our call crashed the Bank"
end
```

```elixir
Process.sleep(100)
# The supervisor restarted Bank... with EMPTY state:
Bank.balance("ada")
```

Ada's 150 is **gone** — `init/1` ran fresh. This is the honest cost of "let it crash", and Book 3's tools (ETS tables that survive restarts, external stores) are how real systems handle it.

## Wrap-up

You can now build the two components that make up ~90% of every Elixir system. In **Book 3** (`book3-advanced-otp.livemd`): the rest of the toolbox — Task, Agent, Registry, DynamicSupervisor, ETS — and how a Mix application assembles the whole tree at boot.
