Listings Grotz/Elixir

Listing 1: Für die Sammlung der Ergebnisse ohne weitere Funktionalität reicht ein Agent
# Datei: lib/filesize/results.ex
defmodule Filesize.Results do
    use Agent
    @me __MODULE__

    def start_link(_init_arg), do: Agent.start_link(fn -> [] end, name: @me)

    def add(new_result), do: Agent.update(@me, fn existing_results -> [new_result | existing_results] end)

    def get_all_sorted() do
      Agent.get(@me, fn existing_results ->
        Enum.sort_by(
          existing_results,
          &elem(&1, 0), # erstes Element aus Tupel dient als Sortierschlüssel
          &>=/2 # Vergleichsfunktion für absteigende Sortierung
        )
      end)
    end
end

-------

Listing 2: Der GenServer verwaltet die Warteschlange noch zu bearbeitender Ordner
# Datei: lib/filesize/queue.ex
defmodule Filesize.Queue do
    use GenServer
    @me __MODULE__

    def start_link(init_arg), do: GenServer.start_link(__MODULE__, init_arg, name: @me)
    def init(_init_arg), do: {:ok, []}

    # CLIENT
    def add_directory(directory) do
      GenServer.call(@me, {:add, directory})
      Filesize.WorkerManager.start_workers()
    end

    def get_next_directory(), do: GenServer.call(@me, {:get_next_directory})
    def directory_done(directory), do: GenServer.call(@me, {:directory_done, directory})

# SERVER
    def handle_call({:add, directory}, _from, state) do
      {:reply, nil, [directory | state]}
    end

    def handle_call({:get_next_directory}, _from, state) do
      case state do
        [] -> {:reply, nil, state}
        [next_dir | rest] -> {:reply, next_dir, rest}
      end
    end

    def handle_call({:directory_done, directory}, _from, state) do
      {:reply, :ok, List.delete(state, directory)}
    end
end

-------

Listing 3: Entbehrliche Worker-Prozesse erledigen die eigentliche Arbeit im Dateisystem
# Datei: lib/filesize/worker.ex
defmodule Filesize.Worker do
    use GenServer, restart: :transient

    def start_link(init_arg), do: GenServer.start_link(__MODULE__, init_arg)

    def init(_init_arg) do
      Filesize.WorkerManager.started()
      Process.send_after(self(), :traverse_one_directory, 0)
      {:ok, nil}
    end

    def terminate(_reason, _state), do: Filesize.WorkerManager.done(self())

    def handle_info(:traverse_one_directory, _from) do
      work_dir = Filesize.Queue.get_next_directory()
      traverse(work_dir)
      Filesize.Queue.directory_done(work_dir)

      {:noreply, nil}
    end

    # kein Ordner mehr zu bearbeiten -> Worker ist fertig und lässt sich beenden
    defp traverse(nil), do: Filesize.WorkerManager.done(self())

    defp traverse(directory) do
      File.ls!(directory)
      |> Enum.map(fn path ->
        absolute_path = Path.join(directory, path)

        if File.dir?(absolute_path) do
          Filesize.Queue.add_directory(absolute_path)
        else
          file_info = File.stat!(absolute_path)
          Filesize.Results.add({file_info.size, absolute_path})
        end
      end)

      # nächsten Ordner aus der Warteschlange holen
      send(self(), :traverse_one_directory)
    end
end

-------

Listing 4: WorkerManager ist für die richtige Anzahl an aktiven Worker-Prozessen verantwortlich
# Datei: lib/filesize/worker_manager.ex
defmodule Filesize.WorkerManager do
    use GenServer
    @me __MODULE__

    def start_link(max_workers), do: GenServer.start_link(__MODULE__, max_workers, name: @me)
    def init(max_workers), do: {:ok, %{max_workers: max_workers, current_workers: 0}}

    def start_workers(), do: GenServer.call(@me, :start_workers)
    def done(pid), do: GenServer.call(@me, {:stop_worker, pid})
    def started(), do: GenServer.cast(@me, :worker_started)

    def handle_cast(:worker_started, state) do
      {:noreply, %{state | current_workers: state.current_workers + 1}}
    end

    def handle_call(:start_workers, _from, state) do
      if state.current_workers < state.max_workers do
        state.current_workers..(state.max_workers - 1)
        |> Enum.each(fn _ -> Filesize.WorkerSupervisor.add_worker() end)
      end

      {:reply, nil, state}
    end

    def handle_call({:stop_worker, pid}, _from, state) do
      Filesize.WorkerSupervisor.stop_worker(pid)
      {:reply, nil, %{state | current_workers: state.current_workers - 1}}
    end
end

-------

Listing 5: StateSupervisor propagiert den Fehlerfall sofort nach oben
# Datei: lib/filesize/state_supervisor.ex
defmodule Filesize.StateSupervisor do
    use Supervisor
    def start_link(init_arg), do: Supervisor.start_link(__MODULE__, init_arg)

    def init(_init_arg) do
      children = [Filesize.Results, Filesize.Queue]
      Supervisor.init(children, strategy: :one_for_all, max_restarts: 0)
    end
end

-------

Listing 6: Die Gesamtanwendung verbindet alle Äste miteinander
# Datei: lib/filesize/application.ex
defmodule Filesize.Application do
    use Application

    def start(_type, _args) do
      children = [
        Filesize.StateSupervisor,
        {Filesize.WorkerManager, 4},
        Filesize.WorkerSupervisor
      ]

      opts = [strategy: :rest_for_one, name: Filesize.Supervisor]
      Supervisor.start_link(children, opts)
    end
end

-------

Listing 7: Die Worker sind austauschbar
# Datei: lib/filesize/worker_supervisor.ex
defmodule Filesize.WorkerSupervisor do
    use DynamicSupervisor
    @me __MODULE__

    def start_link(init_arg), do: DynamicSupervisor.start_link(__MODULE__, init_arg, name: @me)
    def init(_init_arg), do: DynamicSupervisor.init(strategy: :one_for_one)

    def add_worker(), do: {:ok, _pid} = DynamicSupervisor.start_child(@me, Filesize.Worker)
    def stop_worker(pid), do: DynamicSupervisor.terminate_child(@me, pid)
end