defmodule Microwaveprop.Release do @moduledoc """ Release tasks that run in production without Mix installed. Long-running tasks are enqueued as Oban jobs so `eval` returns immediately. Check Oban Web at /admin/oban for progress. Usage via `bin/microwaveprop eval`: bin/microwaveprop eval "Microwaveprop.Release.migrate" bin/microwaveprop eval "Microwaveprop.Release.backtest_all" bin/microwaveprop eval "Microwaveprop.Release.backtest(\"naive_gradient\")" bin/microwaveprop eval "Microwaveprop.Release.climatology" bin/microwaveprop eval "Microwaveprop.Release.native_backfill(500)" bin/microwaveprop eval "Microwaveprop.Release.native_derive" bin/microwaveprop eval "Microwaveprop.Release.recalibrate" """ alias Microwaveprop.Workers.AdminTaskWorker alias Microwaveprop.Workers.HrrrNativeGridWorker @app :microwaveprop @spec migrate() :: [term()] def migrate do _ = load_app() for repo <- repos() do {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) end end @spec rollback(module(), integer()) :: {:ok, term(), term()} def rollback(repo, version) do _ = load_app() {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) end @spec backtest(String.t()) :: :ok def backtest(feature_name) when is_binary(feature_name) do _ = start_app() {:ok, job} = Oban.insert(AdminTaskWorker.new(%{task: "backtest", feature: feature_name})) IO.puts("Enqueued backtest for #{feature_name} (job #{job.id})") end @spec backtest_all() :: :ok def backtest_all do _ = start_app() {:ok, job} = Oban.insert(AdminTaskWorker.new(%{task: "backtest_all"})) IO.puts("Enqueued consolidated backtest (job #{job.id})") end @spec climatology(pos_integer()) :: :ok def climatology(min_samples \\ 3) do _ = start_app() {:ok, job} = Oban.insert(AdminTaskWorker.new(%{task: "climatology", min_samples: min_samples})) IO.puts("Enqueued climatology build (job #{job.id})") end @spec native_backfill(pos_integer()) :: :ok def native_backfill(limit \\ 500) do _ = start_app() %{rows: rows} = Microwaveprop.Repo.query!( """ SELECT EXTRACT(YEAR FROM qso_timestamp)::int AS year, EXTRACT(MONTH FROM qso_timestamp)::int AS month, EXTRACT(DAY FROM qso_timestamp)::int AS day, EXTRACT(HOUR FROM qso_timestamp)::int AS hour, COUNT(*) AS cnt FROM contacts WHERE pos1 IS NOT NULL AND qso_timestamp >= '2019-01-01' GROUP BY 1, 2, 3, 4 ORDER BY cnt DESC LIMIT $1 """, [limit], timeout: 60_000 ) IO.puts("Enqueuing #{length(rows)} native backfill jobs...") for [year, month, day, hour, cnt] <- rows do args = %{year: year, month: month, day: day, hour: hour} case Oban.insert(HrrrNativeGridWorker.new(args, unique: [period: :infinity])) do {:ok, _} -> IO.puts(" #{year}-#{month}-#{day} #{hour}Z (#{cnt} contacts)") {:error, _} -> IO.puts(" #{year}-#{month}-#{day} #{hour}Z (skipped)") end end IO.puts("Done.") end @spec recalibrate() :: :ok def recalibrate do _ = start_app() {:ok, job} = Oban.insert(AdminTaskWorker.new(%{task: "recalibrate"})) IO.puts("Enqueued recalibration (job #{job.id})") end # scorer_diff removed — the tool depended on per-cell factors in # propagation_scores, which are gone now that scores live as binary # files on disk. Left as a stub that tells the operator what # happened if they shell into a running release from muscle memory. @spec scorer_diff(String.t()) :: :ok def scorer_diff(_new_weights_json) do _ = start_app() IO.puts("scorer_diff is disabled: propagation_scores table + factors storage have been dropped.") end @spec native_derive(pos_integer()) :: :ok def native_derive(limit \\ 10_000) do _ = start_app() {:ok, job} = Oban.insert(AdminTaskWorker.new(%{task: "native_derive", limit: limit})) IO.puts("Enqueued native derive (job #{job.id})") end defp repos do Application.fetch_env!(@app, :ecto_repos) end defp load_app do {:ok, _} = Application.ensure_all_started(:ssl) Application.ensure_loaded(@app) end defp start_app do Application.ensure_all_started(@app) end end