Search code examples
elixirerlang-supervisor

Spawning a supervised process to report a heartbeat periodically


I'm trying to spawn a process that will post an HTTP request every five seconds to report it's heartbeat to a server. The code I have is:

defmodule MyModule.Heartbeat do
  def start_link do
    spawn_link(fn ->
      :timer.apply_interval(:timer.seconds(5), __MODULE__, :beat, [])
    end)
  end

  defp beat do
    HTTPoison.post "https://myserver/heartbeat
  end
end

defmodule MyModule.Supervisor do
  use Supervisor

  def start_link do
    Supervisor.start_link(__MODULE__, :ok)
  end

  def init(:ok) do
     children = [
       worker(MyModule.Heartbeat, [])
     ]

    supervise(children, strategy: :one_for_one)
  end
end

However, when I try to start the app, it exits with the following error:

[info] Application my_module exited: MyModule.start(:normal, []) returned an error: shutdown: failed to start child: MyModule.Heartbeat
    ** (EXIT) #PID<0.535.0>

All I need is some process that will run as part of the supervision tree and send it's request in the specified interval. It doesn't need to be able to receive any messages itself, and I'm not too bothered about the particular implementation.

Can anyone suggest what I've done wrong here that's preventing this process from starting, and if there might be a better way to achieve this?


Solution

  • There were 3 mistakes in your code:

    1. :timer.apply_interval/4 returns immediately, and thus the anonymous function passed to spawn_link in MyModule.Heartbeat.start_link/0 terminates soon after executing that line, causing :timer to remove the interval from its queue, and not calling MyModule.Heartbeat.beat/0 ever.

      You can solve this by adding a :timer.sleep(:infinity) as the last expression, which makes your process sleep forever.

    2. MyModule.Heartbeat.beat/0 needs to be a public function so that the anonymous function passed to spawn_link can call it.

    3. MyModule.Heartbeat.start_link/0 needs to return {:ok, pid} on success. spawn_link returns just a pid.

    Final code after these 3 changes:

    defmodule MyModule.Heartbeat do
      def start_link do
        pid = spawn_link(fn ->
          :timer.apply_interval(:timer.seconds(5), __MODULE__, :beat, [])
          :timer.sleep(:infinity)
        end)
        {:ok, pid}
      end
    
      def beat do
        IO.puts "beat"
      end
    end
    
    defmodule MyModule.Supervisor do
      use Supervisor
    
      def start_link do
        Supervisor.start_link(__MODULE__, :ok)
      end
    
      def init(:ok) do
         children = [
           worker(MyModule.Heartbeat, [])
         ]
    
        supervise(children, strategy: :one_for_one)
      end
    end
    

    Output:

    iex(1)> MyModule.Supervisor.start_link
    {:ok, #PID<0.84.0>}
    iex(2)> beat
    beat
    beat
    beat
    

    I'm not sure if you even need all this setup as an exception raised by the function passed to :timer.apply_interval does not affect the calling process. The following script keeps attempting to run the function even though it always raises an exception:

    defmodule Beat do
      def beat do
        raise "hey"
      end
    end
    
    :timer.apply_interval(:timer.seconds(1), Beat, :beat, [])
    :timer.sleep(:infinity)
    

    Output:

    $ elixir a.exs
    
    16:00:43.207 [error] Process #PID<0.53.0> raised an exception
    ** (RuntimeError) hey
        a.exs:3: Beat.beat/0
    
    16:00:44.193 [error] Process #PID<0.54.0> raised an exception
    ** (RuntimeError) hey
        a.exs:3: Beat.beat/0
    
    16:00:45.193 [error] Process #PID<0.55.0> raised an exception
    ** (RuntimeError) hey
        a.exs:3: Beat.beat/0