Genserver: Difference between revisions

From ElixirBlocks
Jump to: navigation, search
(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
end

Key Takeaway

The handle_call for :set_state has a specific behavior:

  1. It receives a new item.
  2. It replies with the old list.
  3. 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
end

Note 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?

  1. You killed the process.
  2. The Supervisor noticed the child died.
  3. The Supervisor restarted App.Service.start_link([]) immediately.
  4. The new_pid is different because it is a brand new process.
  5. 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

  1. mix.exs defines the entry point (App.Application).
  2. App.Application starts a Supervisor.
  3. The Supervisor starts App.Service.
  4. App.Service holds state in a loop.
  5. If App.Service dies, the Supervisor restarts it fresh.