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?
There were 3 mistakes in your code:
: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.
MyModule.Heartbeat.beat/0
needs to be a public function so that the
anonymous function passed to spawn_link
can call it.
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