lib/gear_application/gear_application.ex

# Copyright(c) 2015-2023 ACCESS CO., LTD. All rights reserved.

use Croma

defmodule Antikythera.GearApplication do
  @moduledoc """
  Template for gear application module.

  Invoking `use Antikythera.GearApplication` generates bunch of code, including (but not limited to)

  - `Application` callbacks
  - `Logger` module for the gear
  - accessors to gear config

  All gear implementations must have exactly one module that `use`s this module.

  For antikythera maintainers:
  Note that, when modifying the macros defined in `GearApplication` and `GearApplication.*`,
  we need to re-compile all gears in order to deploy the changes.
  """

  alias Antikythera.{GearName, Conn}
  alias Antikythera.ExecutorPool.Id, as: EPoolId
  alias AntikytheraCore.{MetricsUploader, ExecutorPool}
  alias AntikytheraCore.Alert.Manager, as: AlertManager
  alias AntikytheraCore.{GearManager, GearModule}
  alias AntikytheraCore.Config.Gear, as: GearConfig
  alias AntikytheraCore.GearLog.Writer

  @type child_spec :: :supervisor.child_spec() | {module, term} | module

  @doc false
  defun start(gear_name :: v[GearName.t()], children :: [child_spec]) :: {:ok, pid} do
    GearConfig.ensure_loaded(gear_name)
    all_children = predefined_children(gear_name) ++ children

    opts = [
      strategy: :one_for_one,
      name: GearModule.root_supervisor_unsafe(gear_name)
    ]

    {:ok, pid} = Supervisor.start_link(all_children, opts)
    ExecutorPool.start_per_gear_executor_pool(gear_name)
    GearManager.gear_started(gear_name)
    {:ok, pid}
  end

  defunp predefined_children(gear_name :: v[GearName.t()]) :: [child_spec] do
    # In many cases the process name atoms below are already generated (as module names) at compile-time in `__using__/1` macro.
    # However we don't assume that all these atoms actually exist and thus use unsafe functions,
    # in order to handle gears' beam files compiled with an older version of antikythera
    # where `__using__/1` didn't have all of the module generations.
    # Dynamic atom generations here are acceptable since the number of processes is limited.
    # Note: Required gear config should be read within `start_link/n` callbacks of each modules,
    # so that latest gear config is always used on restart.
    [
      {Writer, [gear_name, GearModule.logger_unsafe(gear_name)]},
      {MetricsUploader, [gear_name, GearModule.metrics_uploader_unsafe(gear_name)]},
      {AlertManager, [gear_name, GearModule.alert_manager_unsafe(gear_name)]}
    ]
  end

  @doc false
  defun stop(gear_name :: v[GearName.t()]) :: :ok do
    GearManager.gear_stopped(gear_name)
    ExecutorPool.kill_executor_pool({:gear, gear_name})
  end

  @callback children() :: [child_spec]
  @callback executor_pool_for_web_request(Conn.t()) :: EPoolId.t()

  defmacro __using__(_) do
    quote do
      use Application
      @behaviour Antikythera.GearApplication

      @gear_name Mix.Project.config()[:app]

      def start(_type, _args) do
        Antikythera.GearApplication.start(@gear_name, children())
      end

      def stop(_state) do
        Antikythera.GearApplication.stop(@gear_name)
      end

      use Antikythera.GearApplication.ConfigGetter
      use Antikythera.GearApplication.ErrorHandler
      use Antikythera.GearApplication.Logger
      use Antikythera.GearApplication.G2g
      use Antikythera.GearApplication.MetricsUploader
      use Antikythera.GearApplication.AlertManager
    end
  end
end