Elixir Supervisors — How do you name a Supervised Task
Asked Answered
K

1

8

I'm really struggling with Elixir supervisors and figuring out how to name them so that I can use them. Basically, I'm just trying to start a supervised Task which I can send messages to.

So I have the following:

defmodule Run.Command do
  def start_link do
    Task.start_link(fn ->
      receive do
        {:run, cmd} -> System.cmd(cmd, [])
      end
    end)
  end
end

with the project entry point as:

defmodule Run do
  use Application

  # See http://elixir-lang.org/docs/stable/elixir/Application.html
  # for more information on OTP Applications
  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    children = [
      # Define workers and child supervisors to be supervised
      worker(Run.Command, [])
    ]

    # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Run.Command]
    Supervisor.start_link(children, opts)
  end
end

At this point, I don't even feel confident that I'm using the right thing (Task specifically). Basically, all I want is to spawn a process or task or GenServer or whatever is right when the application starts that I can send messages to which will in essence do a System.cmd(cmd, opts). I want this task or process to be supervised. When I send it a {:run, cmd, opts} message such as {:run, "mv", ["/file/to/move", "/move/to/here"]} I want it to spawn a new task or process to execute that command. For my use, I don't even need to ever get back the response from the task, I just need it to execute. Any guidance on where to go would be helpful. I've read through the getting started guide but honestly it left me more confused because when I try to do what is done it never turns out as it does in the application.

Thanks for your patience.

Katiakatie answered 14/7, 2015 at 1:23 Comment(0)
I
8

I would just use a GenServer, set up like the following:

defmodule Run do
  use Application

  def start(_, _) do
    import Supervisor.Spec, warn: false

    children = [worker(Run.Command, [])]
    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

defmodule Run.Command do
  use GenServer

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

  def run(cmd, opts) when is_list(opts), do: GenServer.call(__MODULE__, {:run, cmd, opts})
  def run(cmd, _), do: GenServer.call(__MODULE__, {:run, cmd, []})

  def handle_call({:run, cmd, opts}, _from, state) do
    {:reply, System.cmd(cmd, opts), state}
  end
  def handle_call(request, from, state), do: super(request, from, state)
end

You can then send the running process a command to execute like so:

# If you want the result
{contents, _} = Run.Command.run("cat", ["path/to/some/file"])
# If not, just ignore it
Run.Command.run("cp", ["path/to/source", "path/to/destination"])

Basically we're creating a "singleton" process (only one process can be registered with a given name, and we're registering the Run.Command process with the name of the module, so any consecutive calls to start_link while the process is running will fail. However, this makes it easy to set up an API (the run function) which can transparently execute the command in the other process without the calling process having to know anything about it. I used call vs. cast here, but it's a trivial change if you will never care about the result and don't want the calling process to block.

This is a better pattern for something long-running. For one-off things, Task is a lot simpler and easier to use, but I prefer to use GenServer for global processes like this personally.

Imre answered 14/7, 2015 at 4:13 Comment(5)
That's a perfect solution. The reason why you can't name a Task is because if you want to send it messages... you don't want to use a Task anymore.Geralyngeraniaceous
Thank you for explaining this in such detail. This was very helpful for me @bitwalker. Thank you for your time, assistance, and patience.Katiakatie
@Imre When trying to use this I get a GenServer.call timeout. Shouldn't the supervisor prevent this type of thing from happening? (exit) exited in: GenServer.call(Run.Command, {:run, "ls", ["."]}, 5000) ** (EXIT) time out (elixir) lib/gen_server.ex:356: GenServer.call/3Katiakatie
Ah, never mind. Cast is better for asynchronous situations where call is better for when a response is needed due to synchronous. Sorry!Katiakatie
@Katiakatie The supervisor doesn't prevent timeouts, it just makes sure the process is restarted after a failure, so you can either set the timeout to a larger value when using call if you are making calls to long running commands, or use cast :)Imre

© 2022 - 2024 — McMap. All rights reserved.