smaximov

smaximov

Supervised GenServer process disappeared and wasn't restarted

Greetings!

Tonight we had an incident in production when a somewhat important GenServer process seemingly disappeared and had not been restarted by a supervisor. This is the first incident in 2.5 years since this worker had been introduced.

The GenServer in question has basically the following structure:

defmodule MyWorker do
  use GenServer, restart: :transient

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

  @impl GenServer
  def init(opts) do
    Process.send_after(self(), :do_work, 500)

    {:ok, opts}
  end

  @impl GenServer
  def handle_info(:do_work, state) do
    do_some_work(state)
    Process.send_after(self(), :do_work, 500)

    {:noreply, state}
  end

  defp do_some_work(state) do
     # query DB
     # send HTTP request
     # that's all - no OTP messaging, exits, etc.
  end
end

This GenServer is started as part of a Supervisor (let’s call it MySupervisor). MySupervisor uses the :one_for_one strategy, all other init options are default.

When I connected to the remote console, I found out that the process is not alive - Process.whereis(MyWorker) returned nil, Supervisor.which_children(MySupervisor) returned the following entry for MyWorker:

{MyWorker, :undefined, :worker, [MyWorker]}

As far as I understand, this means MyWorker’s child spec was known to MySupervisor, but the worker process itself was not running. I restarted MyWorker with Supervisor.restart_child/2 and started figuring out what went wrong.

The obvious culprit is restart: :transient - as MyWorker was intended to work indefinitely and to never successfully terminate, it should have been :permanent. If we assume MyWorker terminated normally, it explains why it wasn’t restarted.

The problem is figuring out why MyWorker died, because it neither stops itself, nor it is explicitly stopped by some other app code. I have 2 hypotheses left:

  • some developer connected to the remote console and stopped the worker by hand (very unlikely);
  • the worker terminated with an error and was not restarted due to a bug in Supervisor (extremely unlikely).

Am I missing anything else?

Most Liked

mudasobwa

mudasobwa

Creator of Cure

There are two things to take into consideration as well:

Restart strategies, specifically :max_restarts - the maximum number of restarts allowed in a time frame. Defaults to 3 and :max_seconds - the time frame in which :max_restarts applies. Defaults to 5. If something went south and there were 4 attempts to restart it within 5 seconds interval (defaults,) all failing, the GenServer would end up not restarted. This is likely your case, if e. g. the DB connection has dropped for 3 seconds. The culprit would be 500ms interval in init/1, which makes 4 consequtive insuccessful attempts to execute do_some_work/1 to fit into 5 secs and shutdown the process forever.

② The mailbox overflow. This technically might be your case, but extremely unlikely, just saying for the complete picture.

LostKobrakai

LostKobrakai

The app will terminate if the root pid exits, and immediatelly on the first exit, no restarts. And if the app is :permanent it’ll make the whole vm stop.

lud

lud

Yes the supervisor will exit if it has to restart a child too many times. And it will be restarted by its own supervisor.

You can have a chain of Sup → Sup → Sup → Sup → Worker and all elements of the chain will behave like this.

A good way to think about this is that the code describing the supervisor tree (each supervisor children, and the children description in each child supervisor, and so on) describes “how the app state should be at runtime”. And the system will try its best to maintain that state, or otherwise fail and shutdown totally.

Of course that description can be altered at runtime by calling start_child, delete_child to add/remove parts of the tree, and of course having :transient, :temporary or :significant children.

But there is no way it will skip some parts of the tree unless you tell it to. Max-restarts and al’ will only have a temporary impact on the state of the system, but the app will always get back to the desired state.

So if my understanding is correct, if MyWorker was linked to another process, and that process exited with a :shutdown (or {:shutdown, value}), then it would indeed cause MyWorker to terminate normally as well. Unfortunately, MyWorker isn’t linked to any other process.

If the child spec for MyWorker has restart: :transient then yes! (:shutdown is not “normally” but the supervisor will not restart it indeed).

Any progress on finding the cause?

  • Another possibility is that the child spec is overriden somewhere and :transient is replaced with :temporary ; but I guess this is unlikely.
  • Are there any calls to Supervisor.terminate_child or GenServer.stop in your codebase?
  • Are you using :significant with :any_significant or :all_significant?
lud

lud

Hi :slight_smile: Welcome back!

Is there exit(:normal) or exit({:shutdown, some_value}) somewhere in your code that could have been called by that process?

Or maybe it was linked to another process that exits with shutdown? I don’t remember how GenServer behaves in that case.

smaximov

smaximov

Thanks for the reply, I think you nailed the two most possible cases that I haven’t though of!

No, there’re no explicit exits that can be called from this process (unless I missed something). I also doubt that Ecto and HTTPoison have such exits in their codebases that could leak into user’s code.

As for how GenServer behaves in this case, I think this part of Process.link/1 docs is relevant:

When two processes are linked, each one receives exit signals from the other (see also exit/2). Let’s assume pid1 and pid2 are linked. If pid2 exits with a reason other than :normal (which is also the exit reason used when a process finishes its job) and pid1 is not trapping exits (see flag/2), then pid1 will exit with the same reason as pid2 and in turn emit an exit signal to all its other linked processes. The behavior when pid1 is trapping exits is described in exit/2.

So if my understanding is correct, if MyWorker was linked to another process, and that process exited with a :shutdown (or {:shutdown, value}), then it would indeed cause MyWorker to terminate normally as well. Unfortunately, MyWorker isn’t linked to any other process.

Where Next?

Popular in Questions Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Other popular topics Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

We're in Beta

About us Mission Statement