Skip to main content

lib/elixir_server_core/application.ex

defmodule ElixirServerCore.Application do
  @moduledoc """
  Main application supervisor. Configurable via `Application.get_env(:servcore, ...)`.

  ## Configuration options (single pipeline, backward compatible)

      config :servcore,
        router: MyApp.Router,           # default: Core.HTTP.Router
        port: 4000,                     # default: env PORT or 4000
        ip: {0, 0, 0, 0},              # default: {0,0,0,0}
        worker: Core.Workers.Worker,  # default: Core.Workers.Worker
        worker_pool_size: 8,            # default: CPU cores
        job_store: Core.JobStore.SQLite, # default: Core.JobStore.Memory
        job_store_opts: [database: "priv/jobs.db"],
        start_http: true,               # default: true
        start_workers: true             # default: true

  ## Configuration options (multiple pipelines)

      config :servcore,
        router: MyApp.Router,
        port: 4000,
        start_http: true,
        pipelines: [
          [
            queue: Core.Workers.JobQueue,
            queue_name: Core.Workers.JobQueue,
            pool: Core.Workers.WorkerPool,
            worker: Core.Workers.Worker,
            pool_size: 4,
            job_store: Core.JobStore.SQLite,
            job_store_opts: [database: "priv/jobs.db"]
          ],
          [
            queue: Core.Workers.JobQueue,
            queue_name: MyApp.MediaQueue,
            pool: Core.Workers.WorkerPool,
            pool_name: MyApp.MediaPool,
            worker: MyApp.MediaWorker,
            pool_size: 2,
            job_store: Core.JobStore.SQLite,
            job_store_opts: [database: "priv/media_jobs.db"]
          ]
        ]

  Set `start_http: false` to manage the HTTP server yourself (e.g. in a Phoenix app).
  """
  use Application
  require Logger

  @impl true
  def start(_type, _args) do
    start_http? = Application.get_env(:servcore, :start_http, true)

    children = build_pipeline_children()

    children =
      if start_http? do
        port = get_port()
        router = get_router()
        ip = get_ip()

        http_spec =
          {Bandit,
           plug: router, scheme: :http, port: port, ip: ip, http_2_options: [enabled: true]}

        [http_spec | children]
      else
        children
      end

    if start_http? do
      Logger.info("Starting ServCore on port #{get_port()}")
      Logger.info("http://localhost:#{get_port()}")
    end

    opts = [strategy: :one_for_one, name: ElixirServerCore.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # --- Pipeline builder ---

  defp build_pipeline_children do
    case Application.get_env(:servcore, :pipelines) do
      nil ->
        build_single_pipeline()

      pipelines when is_list(pipelines) ->
        Enum.flat_map(pipelines, &build_pipeline/1)
    end
  end

  defp build_single_pipeline do
    worker = get_worker()
    pool_size = get_pool_size()
    store = get_job_store()
    store_opts = get_job_store_opts()
    max_age_days = Application.get_env(:servcore, :max_age_days, 7)
    start_workers? = Application.get_env(:servcore, :start_workers, true)

    queue_spec =
      {Core.Workers.JobQueue,
       name: Core.Workers.JobQueue,
       pool: Core.Workers.WorkerPool,
       store: store,
       store_opts: store_opts,
       max_age_days: max_age_days}

    pool_spec =
      {Core.Workers.WorkerPool,
       name: Core.Workers.WorkerPool,
       worker: worker,
       size: pool_size,
       queue: Core.Workers.JobQueue}

    if start_workers? do
      [pool_spec, queue_spec]
    else
      [queue_spec]
    end
  end

  defp build_pipeline(pipeline) when is_list(pipeline) do
    queue_module = Keyword.get(pipeline, :queue, Core.Workers.JobQueue)
    queue_name = Keyword.get(pipeline, :queue_name, queue_module)
    pool_module = Keyword.get(pipeline, :pool, Core.Workers.WorkerPool)
    pool_name = Keyword.get(pipeline, :pool_name, pool_module)
    worker = Keyword.get(pipeline, :worker, Core.Workers.Worker)
    pool_size = Keyword.get(pipeline, :pool_size, System.schedulers_online())
    store = Keyword.get(pipeline, :job_store, Core.JobStore.Memory)
    store_opts = Keyword.get(pipeline, :job_store_opts, [])
    max_age_days = Keyword.get(pipeline, :max_age_days, 7)
    start_workers? = Keyword.get(pipeline, :start_workers, true)

    queue_spec =
      {queue_module,
       name: queue_name,
       pool: pool_name,
       store: store,
       store_opts: store_opts,
       max_age_days: max_age_days}

    pool_spec =
      {pool_module,
       name: pool_name,
       worker: worker,
       size: pool_size,
       queue: queue_name}

    if start_workers? do
      [pool_spec, queue_spec]
    else
      [queue_spec]
    end
  end

  # --- Config helpers ---

  defp get_port do
    System.get_env("PORT", "4000") |> String.to_integer()
  end

  defp get_router do
    Application.get_env(:servcore, :router, Core.HTTP.Router)
  end

  defp get_ip do
    Application.get_env(:servcore, :ip, {0, 0, 0, 0})
  end

  defp get_worker do
    Application.get_env(:servcore, :worker, Core.Workers.Worker)
  end

  defp get_pool_size do
    Application.get_env(:servcore, :worker_pool_size, System.schedulers_online())
  end

  defp get_job_store do
    Application.get_env(:servcore, :job_store, Core.JobStore.Memory)
  end

  defp get_job_store_opts do
    Application.get_env(:servcore, :job_store_opts, [])
  end
end