Genserver: Difference between revisions
(Created page with "mix new App --sup <b>app/lib/app.ex</b> <source> defmodule App.Service do use GenServer def start_link(state) do GenServer.start_link(__MODULE__, state, name: __MODULE__) end def init(state) do {:ok, state} end def get_state(pid) do GenServer.call(pid, :get_state) end def set_state(pid,state) do GenServer.call(pid, {:set_state, state}) end def handle_call(:get_state, _from, state) do {:reply, state, state} end de...") |
No edit summary |
||
| Line 133: | Line 133: | ||
end | end | ||
</source> | </source> | ||
__________________________________________________________ | |||
= Understanding Elixir OTP: GenServer and Supervisors = | |||
This tutorial explains the provided code, which implements a classic Elixir pattern: a stateful worker (<code>GenServer</code>) monitored by a <code>Supervisor</code>. | |||
== 1. The Big Picture == | |||
The goal of this application is to maintain a list of items (state) in memory. | |||
* '''The Service:''' Holds the state. | |||
* '''The Supervisor:''' Watches the Service. If the Service crashes (or is killed), the Supervisor restarts it immediately. | |||
* '''The Application:''' The entry point that tells the VM what to start when you run <code>iex -S mix</code>. | |||
== 2. The Worker: App.Service == | |||
Located in <code>app/lib/app.ex</code>, this module is a '''GenServer''' (Generic Server). It splits logic into two parts: the '''Client API''' (helper functions you call) and the '''Server Callbacks''' (logic running inside the process). | |||
=== The Code Breakdown === | |||
<syntaxhighlight lang="elixir"> | |||
defmodule App.Service do | |||
use GenServer | |||
# 1. Start the process | |||
def start_link(state) do | |||
# __MODULE__ is "App.Service". We register the process under this name | |||
# so we don't have to track the PID manually. | |||
GenServer.start_link(__MODULE__, state, name: __MODULE__) | |||
end | |||
# 2. Initialize State | |||
def init(state) do | |||
{:ok, state} | |||
end | |||
# --- Client API --- | |||
def get_state(pid) do | |||
GenServer.call(pid, :get_state) | |||
end | |||
def set_state(pid, state) do | |||
GenServer.call(pid, {:set_state, state}) | |||
end | |||
# --- Server Callbacks --- | |||
# Handling get_state | |||
def handle_call(:get_state, _from, state) do | |||
# Return: {:reply, response_to_client, internal_state} | |||
{:reply, state, state} | |||
end | |||
# Handling set_state | |||
def handle_call({:set_state, new_item}, _from, current_list) do | |||
# Logic: Prepend the new_item to the current_list | |||
new_state = [new_item | current_list] | |||
# Note: You are returning 'current_list' (the old state) to the caller, | |||
# but saving 'new_state' internally. | |||
{:reply, current_list, new_state} | |||
end | |||
end | |||
</syntaxhighlight> | |||
=== Key Takeaway === | |||
The <code>handle_call</code> for <code>:set_state</code> has a specific behavior: | |||
# It receives a new item. | |||
# It replies with the '''old''' list. | |||
# It updates its internal state to the '''new''' list (with the item prepended). | |||
== 3. The Entry Point: App.Application == | |||
In <code>application.exs</code>, you defined the application startup logic. This is triggered because <code>mix.exs</code> has <code>mod: {App.Application, []}</code>. | |||
<syntaxhighlight lang="elixir"> | |||
defmodule App.Application do | |||
use Application | |||
def start(_type, _args) do | |||
children = [ | |||
# This tells the Supervisor to start App.Service with an empty list [] as initial state | |||
{App.Service, []} | |||
] | |||
opts = [strategy: :one_for_one, name: App.Supervisor] | |||
Supervisor.start_link(children, opts) | |||
end | |||
end | |||
</syntaxhighlight> | |||
'''Note regarding <code>App.Supervisor</code>''': | |||
In your <code>app.ex</code> file, you manually defined a module named <code>App.Supervisor</code>. However, your <code>App.Application</code> '''is ignored that module''' and starting a supervisor directly using <code>Supervisor.start_link/2</code>. The <code>App.Service</code> is currently being supervised directly by the Application root supervisor. | |||
== 4. Interactive Tutorial: Testing Fault Tolerance == | |||
Now, let's look at the commented-out script at the bottom of your file. This demonstrates the power of OTP. | |||
Open your terminal in the project folder and run: | |||
iex -S mix | |||
=== Step 1: Verify the Service is running === | |||
Because of <code>App.Application</code>, the service starts automatically. | |||
<syntaxhighlight lang="elixir"> | |||
# Check if the process exists by looking up its name | |||
pid = Process.whereis(App.Service) | |||
# Output: #PID<0.152.0> (The ID will vary) | |||
</syntaxhighlight> | |||
=== Step 2: Manipulate State === | |||
Let's use the API functions. | |||
<syntaxhighlight lang="elixir"> | |||
# Get initial state (defined as [] in App.Application) | |||
App.Service.get_state(pid) | |||
# Output: [] | |||
# Add an item | |||
App.Service.set_state(pid, "we are the world") | |||
# Output: [] <-- Rembember, handle_call returns the OLD state | |||
# Add another | |||
App.Service.set_state(pid, "hurray") | |||
# Output: ["we are the world"] | |||
# Check current state | |||
App.Service.get_state(pid) | |||
# Output: ["hurray", "we are the world"] | |||
</syntaxhighlight> | |||
=== Step 3: The "Let it Crash" Test === | |||
This is the most important part of the tutorial. We will kill the process and watch the Supervisor bring it back to life. | |||
<syntaxhighlight lang="elixir"> | |||
# 1. Get the current PID | |||
pid = Process.whereis(App.Service) | |||
# 2. Kill the process forcefully | |||
Process.exit(pid, :kill) | |||
# 3. Check if it's alive | |||
new_pid = Process.whereis(App.Service) | |||
# 4. Compare | |||
pid == new_pid | |||
# Output: false | |||
</syntaxhighlight> | |||
'''What happened?''' | |||
# You killed the process. | |||
# The Supervisor noticed the child died. | |||
# The Supervisor restarted <code>App.Service.start_link([])</code> immediately. | |||
# The <code>new_pid</code> is different because it is a brand new process. | |||
# '''Important:''' The state reset to <code>[]</code> because the process memory was wiped when it died. (To keep state across restarts, you would need a database or ETS table). | |||
== Summary == | |||
# '''<code>mix.exs</code>''' defines the entry point (<code>App.Application</code>). | |||
# '''<code>App.Application</code>''' starts a Supervisor. | |||
# '''The Supervisor''' starts <code>App.Service</code>. | |||
# '''<code>App.Service</code>''' holds state in a loop. | |||
# If <code>App.Service</code> dies, the Supervisor restarts it fresh. | |||
Latest revision as of 02:36, 28 November 2025
mix new App --sup
app/lib/app.ex
defmodule App.Service do
use GenServer
def start_link(state) do
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def init(state) do
{:ok, state}
end
def get_state(pid) do
GenServer.call(pid, :get_state)
end
def set_state(pid,state) do
GenServer.call(pid, {:set_state, state})
end
def handle_call(:get_state, _from, state) do
{:reply, state, state}
end
def handle_call({:set_state, new_state}, _from, state)do
{:reply,state,[new_state | state]}
end
end
defmodule App.Supervisor do
use Supervisor
def start do
Supervisor.start_link(__MODULE__, [])
end
def init(_) do
children = [
{App.Service,[]}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
# App.Supervisor.start() # # IO.inspect x # pid = Process.whereis(App.Service) # # The follwing is nil # IO.inspect pid # Process.exit(pid, :kill) # IO.inspect "______________" # pid = Process.whereis(App.Service) # # IO.inspect "_____" # IO.inspect Process.whereis(App.Service) # App.Service.get_state(pid) # App.Service.set_state(pid, "we are the world") # App.Service.set_state(pid, "hurrrray") # IO.inspect App.Service.get_state(pid)
mix.exs
defmodule App.MixProject do
use Mix.Project
def project do
[
app: :app,
version: "0.1.0",
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger],
mod: {App.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
application.exs
defmodule App.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: App.Worker.start_link(arg)
{App.Service, []}
]
opts = [strategy: :one_for_one, name: App.Supervisor]
Supervisor.start_link(children, opts)
end
end
__________________________________________________________
Understanding Elixir OTP: GenServer and Supervisors
This tutorial explains the provided code, which implements a classic Elixir pattern: a stateful worker (GenServer) monitored by a Supervisor.
1. The Big Picture
The goal of this application is to maintain a list of items (state) in memory.
- The Service: Holds the state.
- The Supervisor: Watches the Service. If the Service crashes (or is killed), the Supervisor restarts it immediately.
- The Application: The entry point that tells the VM what to start when you run
iex -S mix.
2. The Worker: App.Service
Located in app/lib/app.ex, this module is a GenServer (Generic Server). It splits logic into two parts: the Client API (helper functions you call) and the Server Callbacks (logic running inside the process).
The Code Breakdown
defmodule App.Service do
use GenServer
# 1. Start the process
def start_link(state) do
# __MODULE__ is "App.Service". We register the process under this name
# so we don't have to track the PID manually.
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
# 2. Initialize State
def init(state) do
{:ok, state}
end
# --- Client API ---
def get_state(pid) do
GenServer.call(pid, :get_state)
end
def set_state(pid, state) do
GenServer.call(pid, {:set_state, state})
end
# --- Server Callbacks ---
# Handling get_state
def handle_call(:get_state, _from, state) do
# Return: {:reply, response_to_client, internal_state}
{:reply, state, state}
end
# Handling set_state
def handle_call({:set_state, new_item}, _from, current_list) do
# Logic: Prepend the new_item to the current_list
new_state = [new_item | current_list]
# Note: You are returning 'current_list' (the old state) to the caller,
# but saving 'new_state' internally.
{:reply, current_list, new_state}
end
endKey Takeaway
The handle_call for :set_state has a specific behavior:
- It receives a new item.
- It replies with the old list.
- It updates its internal state to the new list (with the item prepended).
3. The Entry Point: App.Application
In application.exs, you defined the application startup logic. This is triggered because mix.exs has mod: {App.Application, []}.
defmodule App.Application do
use Application
def start(_type, _args) do
children = [
# This tells the Supervisor to start App.Service with an empty list [] as initial state
{App.Service, []}
]
opts = [strategy: :one_for_one, name: App.Supervisor]
Supervisor.start_link(children, opts)
end
endNote regarding App.Supervisor:
In your app.ex file, you manually defined a module named App.Supervisor. However, your App.Application is ignored that module and starting a supervisor directly using Supervisor.start_link/2. The App.Service is currently being supervised directly by the Application root supervisor.
4. Interactive Tutorial: Testing Fault Tolerance
Now, let's look at the commented-out script at the bottom of your file. This demonstrates the power of OTP.
Open your terminal in the project folder and run:
iex -S mix
Step 1: Verify the Service is running
Because of App.Application, the service starts automatically.
# Check if the process exists by looking up its name pid = Process.whereis(App.Service) # Output: #PID<0.152.0> (The ID will vary)
Step 2: Manipulate State
Let's use the API functions.
# Get initial state (defined as [] in App.Application) App.Service.get_state(pid) # Output: [] # Add an item App.Service.set_state(pid, "we are the world") # Output: [] <-- Rembember, handle_call returns the OLD state # Add another App.Service.set_state(pid, "hurray") # Output: ["we are the world"] # Check current state App.Service.get_state(pid) # Output: ["hurray", "we are the world"]
Step 3: The "Let it Crash" Test
This is the most important part of the tutorial. We will kill the process and watch the Supervisor bring it back to life.
# 1. Get the current PID pid = Process.whereis(App.Service) # 2. Kill the process forcefully Process.exit(pid, :kill) # 3. Check if it's alive new_pid = Process.whereis(App.Service) # 4. Compare pid == new_pid # Output: false
What happened?
- You killed the process.
- The Supervisor noticed the child died.
- The Supervisor restarted
App.Service.start_link([])immediately. - The
new_pidis different because it is a brand new process. - Important: The state reset to
[]because the process memory was wiped when it died. (To keep state across restarts, you would need a database or ETS table).
Summary
mix.exsdefines the entry point (App.Application).App.Applicationstarts a Supervisor.- The Supervisor starts
App.Service. App.Serviceholds state in a loop.- If
App.Servicedies, the Supervisor restarts it fresh.