Strange (possibly silly) error in Genserver call

Hello all,

There might be something very stupid in this, but I have spent enough time and can’t figure out what am I doing wrong.

defmodule SendServer do
  use GenServer
  alias Sender

  def init(args) do
    IO.puts("Received arguments: #{inspect(args)}")
    max_retries = Keyword.get(args, :max_retries, 5)
    state = %{emails: [], max_retries: max_retries}
    {:ok, state}
  end

  def handle_cast({:send, email}, state) do
    Sender.send_email(email)
    IO.inspect(%{email: email, status: "sent", retries: 0})
    emails = [%{email: email, status: "sent", retries: 0}] ++ state.emails
    IO.inspect(state)
    {:noreply, state}
  end

  def handle_call(:get_state, _from, state) do
    {:reply, state, state}
  end
end

^^^ This is the genserver calling function below.

defmodule Sender do
  
  def send_email(email) do
    Process.sleep(3000)
    IO.puts("Email to #{email} sent")
    {:ok, "email_sent"}
  end

  
end

when I in iex -S mix , command and output are pasted.

iex(5)> {:ok, pid} = GenServer.start(SendServer, [max_retries: 1])
Received arguments: [max_retries: 1]
{:ok, #PID<0.184.0>}
iex(6)> GenServer.cast(pid, {:send, "hello@email.com"})
:ok
Email to hello@email.com sent
%{email: "hello@email.com", retries: 0, status: "sent"}
%{emails: [], max_retries: 1}
iex(7)> GenServer.call(pid, :get_state)
%{emails: [], max_retries: 1}

Why does my emails array NOT have the email I just appended via handle_cast({:send, email}, state) function?

The state should have been updated in genserver, but it is not. To me, it appears that prepending to the list is not working as expected.

This error was posted on the forum earlier. My bad.

I think in your handle_cast that state was not modified. I’m not sure if it’s correct but the returning state should be updated in order for the get_state call to retrieve the updated state (with email).

  def handle_cast({:send, email}, state) do
    Sender.send_email(email)
    IO.inspect(%{email: email, status: "sent", retries: 0})
    emails = [%{email: email, status: "sent", retries: 0}] ++ state.emails
    {:noreply, emails}
  end
2 Likes

The only thing that you are missing (I think) is that the state is not the emails list, but a map with a emails key.

So you shouldn’t set emails as the new state but update the state map’s emails list, and return that map?

2 Likes

As mentionned by other… the state is not modified. And the output state is equal to the input state.

You need to figure out how to update it cleanly…

{:reply, %{state | emails: [%{...} | state.emails]}}

But this is not going to work… You need to check if the email key hay already be seen, to increase retries.

And check that the new retries is lower than max_retries.

You don’t have this logic…

Instead of a list of emails, I would use a map with email as key, and use Map.update.

4 Likes

True. that did solve the problem!

Thank you!