lib/loader.ex

defmodule Loader do
  @moduledoc "README.md"
             |> File.read!()
             |> String.split("<!-- MDOC !-->")
             |> Enum.fetch!(1)

  @external_resource "README.md"
  defmodule WorkResponse do
    @moduledoc """
    Internal data structure used to represent the results of executing a `WorkSpec`

    ## Properties

      - `:data` -  whatever important data is returned by the work

      - `:response_time` - **must be an integer number, in microseconds**, which is the "client-side" view of how long the work took. I recommend using `System.monotonic_time/0` or `:timer.tc/1`
    """
    defstruct [:data, :kind, :response_time]

    @type t :: %__MODULE__{
            data: any(),
            kind: :ok | :error,
            response_time: integer()
          }
  end

  defmodule WorkSpec do
    @moduledoc """
    A specification for some "work" to do, to generate load.
    """
    # TODO: should a user be able to specify a task that does not take a count? could we batch them ourselves?
    # is it a good design to force the user to tell us how to execute their work in
    # batches?
    # maybe they can give us either `arity-0 func`, `arity-1 func`, or `{arity-0, arity-1}` and we
    # handle batching as best we can?
    # TODO: should a `reason` be attached to the `is_success?` callback? so that a user can do something like `{false, "too slow"}`?
    defstruct [:task, :is_success?]

    @type t :: %__MODULE__{
            task: (() -> term()) | mfa(),
            is_success?: (Loader.WorkResponse.t() -> boolean())
          }
  end

  @doc """
  Execute tasks based on the `work_spec`, scheduled based on the parameters in the `load_profile`
  """
  @spec execute_profile(Loader.LoadProfile.t(), Loader.WorkSpec.t()) ::
          DynamicSupervisor.on_start_child()
  def execute_profile(load_profile, work_spec) do
    DynamicSupervisor.start_child(
      Loader.DynamicSupervisor,
      Loader.ScheduledLoader.child_spec(load_profile: load_profile, work_spec: work_spec)
    )
  end

  @doc """
  Execute several `LoadProfile`s simultaneously, each with their supplied `WorkSpec`
  """
  @spec execute_profiles([{Loader.LoadProfile.t(), Loader.WorkSpec.t()}]) :: [
          DynamicSupervisor.on_start_child()
        ]
  def execute_profiles(profile_spec_pairs) do
    Enum.map(profile_spec_pairs, fn {profile, spec} -> execute_profile(profile, spec) end)
  end
end