defmodule Loader do @moduledoc "README.md" |> File.read!() |> String.split("") |> Enum.fetch!(1) use Supervisor @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 `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 """ Start an instance of `Loader` ## Options * `:name` - The name of your Loader instance. This field is required. """ def start_link(opts) do name = opts[:name] || raise(ArgumentError, "must supply a name") config = %{ dynamic_supervisor_name: dynamic_supervisor_name(name), execution_store_name: execution_store_name(name), task_supervisors_name: task_supervisors_name(name) } Supervisor.start_link(__MODULE__, config, name: :"#{name}.Supervisor") end def child_spec(opts) do %{ id: opts[:name] || raise(ArgumentError, "must supply a name"), start: {__MODULE__, :start_link, [opts]} } end @impl Supervisor def init(config) do children = [ {Loader.ExecutionStore, name: config.execution_store_name}, {PartitionSupervisor, child_spec: Task.Supervisor, name: config.task_supervisors_name}, {DynamicSupervisor, name: config.dynamic_supervisor_name} ] Supervisor.init(children, strategy: :one_for_one) end @doc """ Execute tasks defined by the `work_spec`, scheduled based on the `load_profile`. When provided with a list, all profiles will be executed concurrently. See `Loader.LoadProfile` for more information on how to define a profile. """ @spec execute({Loader.LoadProfile.t(), Loader.WorkSpec.t()}, atom()) :: DynamicSupervisor.on_start_child() def execute({load_profile, work_spec}, instance_name) do DynamicSupervisor.start_child( dynamic_supervisor_name(instance_name), Loader.ScheduledLoader.child_spec(load_profile: load_profile, work_spec: work_spec, instance_name: instance_name) ) end @spec execute([{Loader.LoadProfile.t(), Loader.WorkSpec.t()}], atom()) :: [ DynamicSupervisor.on_start_child() ] def execute(profile_spec_pairs, instance_name) do Enum.map(profile_spec_pairs, fn {profile, spec} -> execute({profile, spec}, instance_name) end) end @doc false def execution_store_name(instance_name), do: :"#{instance_name}.ExecutionStore" @doc false def task_supervisors_name(instance_name), do: :"#{instance_name}.TaskSupervisors" @doc false def dynamic_supervisor_name(instance_name), do: :"#{instance_name}.DynamicSupervisor" end