Asynchronously executing functions in supervised processes

Ian Source

I'm trying to monitor several APIs at the same time. Currently I'm starting my application, which then causes my Supervisor to start and that in turn starts the monitoring processes. My feeble brain has managed to get to the point where I am spawning all 4 of child processes.

My APIMon.Monitor module is a mess and I'm not really sure how to fix it. What I want here is for each of these to start as children of the Supervisor, and then indefinitely run in their own scan() loops while all outputting information to the console independently.

I've been playing with this for a while and trying to go through the Task docs and I can't seem to find a way to make this work where I'm not blocking the console entirely in iex -S mix, which I assume means I'm blocking entirely. I want to be able to execute commands in the console while these Tasks are running or sleeping.

Any help would be greatly appreciated.

APIMon.ex

defmodule APIMon do
  @API [
     %{id: "A", host: '192.168.1.5', port: 3001},
     %{id: "B", host: '192.168.1.7', port: 3001},
     %{id: "C", host: '192.168.1.8', port: 3001},
     %{id: "D", host: '192.168.1.9', port: 3001}
  ]

  def start(_type, _args) do
    APIMon.Supervisor.start_link(@API)
  end
end

APIMon.Supervisor

defmodule APIMon.Supervisor do
  use Supervisor

  def start_link(args) do
    Supervisor.start_link(__MODULE__, args, name: __MODULE__)
  end

  def init(args) do
    children =
      args
      |> Enum.reduce([], fn child, acc ->
        acc =
          acc ++ [Supervisor.child_spec({APIMon.Monitor, child}, id: "Worker_#{child.id}")]
      end)

    Supervisor.init(children, strategy: :one_for_one)
  end
end

APIMon.Monitor

defmodule APIMon.Monitor do
  use GenServer

  def start_link(arg) do
    id = arg.id
    GenServer.start_link(__MODULE__, arg, name: :"#{__MODULE__} [#{id}]")
  end

  ## Callbacks

  def init(arg) do
    pid = Kernel.inspect(self())

    IO.puts "Adding API monitor #{arg.id} to the stack as #{pid}"

    scan()

    {:ok, arg}
  end

  def scan() do
    task = Task.async(fn ->
       #do individual API health checks here
     end)

    Task.await(task)
    :timer.sleep(2500)

    scan()
  end
end
elixir

Answers

answered 8 months ago PaweĊ‚ Dawczak #1

The problem with your APIMon.Monitor is it doesn't return from init - you're getting into infinite loop calling scan(). You could get around this by sending message to itself.

Sounds a bit cryptic, so let me show you an example:

defmodule APIMon.Monitor do
  use GenServer

  def start_link(arg) do
    id = arg.id
    GenServer.start_link(__MODULE__, arg, name: :"#{__MODULE__} [#{id}]")
  end

  ## Callbacks

  def init(arg) do
    pid = Kernel.inspect(self())

    IO.puts "Adding API monitor #{arg.id} to the stack as #{pid}"

    Process.send_after(self(), :scan, 2500)

    {:ok, arg}
  end

  def handle_info(:scan, state) do
    #do individual API health checks here

    Process.send_after(self(), :scan, 2500)

    {:noreply, state}
  end
end

Other than that I don't think your code is a mess - this is really nicely structured Elixir/OTP code!

Hope that helps!

comments powered by Disqus