2
votes

First off, I'm very new to Elixir so this may be a misguided question

I have a function that launches two processes.

The first process uses the erlang :fs library to watch a directory for file changes and then sends a message to the second process.

The second process waits for a message that a file in a directory has changed and when it gets the message, runs a function that regenerates an HTML report (it's an invoice).

It looks like this:

def run_report_daemon(line_item_dir) do

    if Process.whereis(:html_daemon) == nil do 
      spawn(HTMLInvoiceDaemon, :run_daemon, [line_item_dir])
      |> Process.register(:html_daemon)
    end

    if Process.whereis(:file_watcher) == nil do
      :fs.start_link(:file_watcher, Path.absname(line_item_dir))
    end
    Process.sleep(1000)
    run_report_daemon(line_item_dir)
end

This "works" but what bothers me is the "sleep" and the recursive call.

My question: Is there a better way to keep the process containing the function that spawns my processes alive. Without the recursive call and the sleep it just dies and takes the other processes with it. With the recursive call and no sleep, it consumes a huge chunk of processor resources because it keeps looping very quickly.

The if blocks are necessary because otherwise it would repeatedly spawn and register the processes.

I thought of using Supervisor, but then I'm not sure how to launch the :fs process under a supervisor, and even in that case I need to keep the starting process alive.

I suspect my problem is because of a fundamental misunderstanding of "the right way to do things" in Elixir.

(note: I can probably do the whole thing without spawning processes like this, but it's a learning exercise)

2
Where exactly are you sending the messages to HTMLInvoiceDaemon? Can you post some more code?Dogbert
The :fs library includes a function that allows another process to subscribe to receive messages from the :fs process. In HTMLInvoiceDaemon, I subscribe to messages from :file_watcher, and then act on those messages.Veen
Is line_item_dir arbitrary or do you know these at start-time? Also, how many of them are there? I had attempted to answer but then realized that the input there would be tough to get if it's not known when the application starts up.Dave Lugg
It's known — a single directory passed in as an option from the command line..Veen

2 Answers

1
votes

I'd probably do something like this

Notice that if either of those crash, the supervisor will restart it and we'll end up in the correct state, where there is one FSWatch process and one InvoiceDaemon process, and the InvoiceDaemon process will be listening to the FSWatch process.

The strategy is rest_for_one which will restart the InvoiceDaemon process if the FSWatch process dies. This will prevent an InvoiceDaemon process from being subscribed to a FSWatch that no longer exists. If the InvoiceDaemon crashes then there's no need to restart the FSWatch process because when InvoiceDaemon comes back up it will re-subscribe and then run forever.

defmodule Thing do
  use Supervisor
  # Wraps the :fs.start_link
  # thing in a process and goes to sleep
  defmodule FSWatch do
    def start_link(target_dir) do
      watcher = spawn_link(fn ->
        IO.puts "Watching #{target_dir}"
        :fs.start_link(:file_watcher, target_dir)
        :timer.sleep(:infinity)
      end)

      {:ok, watcher}
    end
  end

  # Subscribe to the file watcher and then listen 
  # forever
  defmodule InvoiceDaemon do
    def start_link do
      daemon = spawn_link(fn ->
        :fs.subscribe(:file_watcher, "/path")
        run()
      end)

      {:ok, daemon}
    end

    defp run do
      receive do
        {_, {:fs, file_event}, path} ->
          IO.puts "Got a file event #{inspect path}"
          run()
        e ->
          IO.puts "unexpected event #{inspect e}"
          run()
      end
    end
  end

  def start_link do
    target_dir = "/foo/bar"

    children = [
      worker(FSWatch, [target_dir]),
      worker(InvoiceDaemon, [])
    ]
    IO.inspect self
    Supervisor.start_link(children, strategy: :rest_for_one)
  end
end

Thing.start_link
:timer.sleep(:infinity)
0
votes

If you are not going to use supervisor, which is correct way, you can use a link/monitor for those processes.

Instead of sleep/recursive call, you link/monitor to those processes. Then you wait for a down message, which is sent to you when either of them dies.

The good thing is that you are blocking for a receive, so no resource consumption.

Actually, this is the base of a supervisor, and I would recommend that one if possible.

For more information, see docs on Process, spawn_link, and spawn_monitor functions.