Trying to write a stateful worker process

I am trying to understand how GenServer and Supervisor work by writing a project wherein a process will maintain its previous state when it is restarted.

I have a mix (sup) project in which I have three primary processes - Supervisor, Stack, Stash . There are push and pop operations on the stack. If a pop operation is performed on the stack, the process will die and then restarted by the supervisor again. Before dying, the stack will store the empty list in the stash so that it will be picked up when the stack process is restarted.

supervisor.ex

defmodule Spvsr.More.Supervisor do
  use Supervisor

  def start_link(initial) do
    result = {:ok, sup} = Supervisor.start_link(__MODULE__, initial, name:  __MODULE__)

    start_workers(sup, initial)
    result
  end

  def start_workers(sup, initial) do
    Supervisor.start_child(sup, worker(Spvsr.More.Stash, [initial]))
    Supervisor.start_child(sup, worker(Spvsr.More.Stack, [Spvsr.More.Stash.get_value]))
  end

  def init(_) do
    supervise [], strategy: :one_for_one
  end
end

stash.ex

defmodule Spvsr.More.Stash do
  use GenServer
  require Logger

  def start_link(list) do
    {:ok, _pid} = GenServer.start_link(__MODULE__, list, name: __MODULE__)
  end

  def save_value(value) do
    IO.puts("setting value #{value}")
    GenServer.cast __MODULE__, {:save_value, value}
  end

  def get_value do
    IO.puts("get value")
    GenServer.call __MODULE__, :get_value
  end

  def handle_cast({:save_value, value}, _current_value) do
    {:noreply, value}
  end

  def handle_call(:get_value, _from, current_value) do
    {:reply, current_value, current_value}
  end

  def init(args) do
    {:ok, args}
  end
end

stack.ex

defmodule Spvsr.More.Stack do
  use GenServer

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

  def init(args) do
    {:ok, args}
  end

  def shutdown(msg \\ "No msg received") do
    GenServer.cast(__MODULE__, {:push, {:bye, msg}})
  end

  def push(n) do
    GenServer.cast(__MODULE__, {:push, n})
  end

  def pop do
    GenServer.call(__MODULE__, :pop)
  end

  def get do
    GenServer.call(__MODULE__, :get)
  end

  def handle_call(:pop, _from, [head | tail]), do: {:reply, head, tail}
  def handle_call(:pop, _from, []) do
    Spvsr.More.Stash.save_value [] # saving the value before the process dies
    {:stop, "Stack is empty", []}
  end
  def handle_call(:get, _from, list), do: {:reply, list, list}


  def handle_cast({:push, {:bye, msg}}, list), do: {:stop, msg, list}
  def handle_cast({:push, n}, list), do: {:noreply, [n | list]}

  def terminate(reason, state) do
    IO.puts("I m terminating #{IO.inspect reason} #{IO.inspect state} ")
    :ok
  end
end

application.ex

defmodule Spvsr.Application do
  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  def start(_type, _args) do
    # List all child processes to be supervised
    children = [
      # Starts a worker by calling: Spvsr.Worker.start_link(arg)
      # {Spvsr.Worker, arg},
      {Spvsr.More.Supervisor, [1, 2, 3, 4]}
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Spvsr.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Now on iex I am running the following commands

> Spvsr.More.Stack.get # [1, 2, 3, 4]
> Spvsr.More.Stack.pop # 1
> Spvsr.More.Stack.pop # 2
> Spvsr.More.Stack.pop # 3
> Spvsr.More.Stack.pop # 4
> Spvsr.More.Stack.pop
setting value
"Stack is empty"
[]
I m terminating Stack is empty
** (exit) exited in: GenServer.call(Spvsr.More.Stack, :pop, 5000)
    ** (EXIT) "Stack is empty"
    (elixir) lib/gen_server.ex:834: GenServer.call/3
iex(16)>
19:08:46.322 [error] GenServer Spvsr.More.Stack terminating
** (stop) "Stack is empty"
Last message (from #PID<0.115.0>): :pop
State: []
Client #PID<0.115.0> is alive
    (stdlib) gen.erl:169: :gen.do_call/4
    (elixir) lib/gen_server.ex:831: GenServer.call/3
    (stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
    (elixir) src/elixir.erl:233: :elixir.eval_forms/4
    (iex) lib/iex/evaluator.ex:250: IEx.Evaluator.handle_eval/5
    (iex) lib/iex/evaluator.ex:230: IEx.Evaluator.do_eval/3
    (iex) lib/iex/evaluator.ex:208: IEx.Evaluator.eval/3
    (iex) lib/iex/evaluator.ex:94: IEx.Evaluator.loop/1

> Spvsr.More.Stash.get_value []
> Spvsr.More.Stack.get # [1, 2, 3, 4]

Now, when the stack process restarts, it should take the value from the stash process, which is []. Instead, it contains the value [1, 2 ,3, 4]. Can someone please help me understand what am I doing wrong?

1 Like

When you call start_workers/2, you are cementing the value for the Spvsr.More.Stack.get_value() into the child spec. When Supervisor restarts a child, it uses the same child spec. You probably want a function reference so you can get the new value when the child gets started.

Try this:

In supervisor.ex:

Supervisor.start_child(sup, worker(Spvsr.More.Stack, [fn -> Spvsr.More.Stash.get_value() end]))

In stack.ex:

def start_link(fun) when is_function(fun, 0) do
  GenServer.start_link(__MODULE__, fun.(), name: __MODULE__)
end
5 Likes

Thanks for explaining it!

And yes, this works. I know one more thing about supervisors now. :slight_smile: