Current section
Files
Jump to
Current section
Files
lib/crucible/pipeline/runner.ex
defmodule Crucible.Pipeline.Runner do
@moduledoc """
Executes experiment pipelines stage-by-stage.
"""
alias Crucible.{Context, Registry}
alias Crucible.IR.{Experiment, StageDef}
alias CrucibleFramework.Persistence
@doc """
Runs an experiment, optionally persisting run state.
"""
@spec run(Experiment.t(), keyword()) :: {:ok, Context.t()} | {:error, {atom(), term()}}
def run(%Experiment{} = experiment, opts \\ []) do
run_id = Keyword.get(opts, :run_id, Ecto.UUID.generate())
persist? = Keyword.get(opts, :persist, true)
{run_record, ctx} =
case persist? do
true ->
case Persistence.start_run(experiment, metadata: %{run_id: run_id}) do
{:ok, run} -> {run, build_context(experiment, run_id, opts) |> put_run(run)}
{:error, _} -> {nil, build_context(experiment, run_id, opts)}
end
false ->
{nil, build_context(experiment, run_id, opts)}
end
result =
Enum.reduce_while(experiment.pipeline, {:ok, ctx}, fn %StageDef{} = stage_def,
{:ok, ctx_acc} ->
case resolve_stage(stage_def) do
{:ok, mod} ->
log_stage(stage_def.name)
case mod.run(ctx_acc, stage_def.options) do
{:ok, new_ctx} -> {:cont, {:ok, new_ctx}}
{:error, reason} -> {:halt, {:error, {stage_def.name, reason}, ctx_acc}}
end
{:error, reason} ->
{:halt, {:error, {stage_def.name, reason}, ctx_acc}}
end
end)
finalize(result, run_record)
end
defp build_context(experiment, run_id, opts) do
%Context{
experiment_id: experiment.id,
run_id: run_id,
experiment: experiment,
assigns: Keyword.get(opts, :assigns, %{})
}
end
defp resolve_stage(%StageDef{module: nil, name: name}) do
Registry.stage_module(name)
end
defp resolve_stage(%StageDef{module: mod}), do: {:ok, mod}
defp log_stage(name) do
IO.puts("→ Running stage #{name}")
end
defp finalize({:ok, ctx}, nil), do: {:ok, ctx}
defp finalize({:ok, ctx}, run_record) do
Persistence.finish_run(run_record, "completed", %{
metrics: ctx.metrics,
outputs: ctx.outputs,
metadata: Map.put(run_record.metadata, "assigns", ctx.assigns)
})
{:ok, ctx}
end
defp finalize({:error, {stage, reason}, ctx}, nil), do: {:error, {stage, reason, ctx}}
defp finalize({:error, {stage, reason}, ctx}, run_record) do
Persistence.finish_run(run_record, "failed", %{
metrics: ctx.metrics,
metadata: Map.merge(run_record.metadata || %{}, %{"failure" => {stage, inspect(reason)}})
})
{:error, {stage, reason, ctx}}
end
defp put_run(ctx, run_record) do
%Context{ctx | assigns: Map.put(ctx.assigns, :run_record, run_record)}
end
end