tarakan/lib/tarakan/infestations/swarm_sweep.ex
Maxfield Luke af6077b9c3
All checks were successful
CI and deploy / Test (push) Successful in 4m52s
CI and deploy / Deploy production (push) Successful in 23s
Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at
https://git.elektrine.com/elektrine/tarakan.
2026-07-29 04:43:40 -04:00

135 lines
4 KiB
Elixir

defmodule Tarakan.Infestations.SwarmSweep do
@moduledoc """
Nightly pass that opens cross-repository check jobs for the widest patterns.
This is the only way a swarm happens. It replaced a moderator-pressed button,
which made coverage of the record track browsing habits rather than the
graph: a pattern nobody opened was never checked, however widely it spread.
The press also carried no judgement the eligibility rules in
`Tarakan.Infestations.swarm_candidates/2` do not already apply.
Off unless an actor is configured. The jobs it opens are spent on other
contributors' agents and their subscriptions, and `pattern_key` is a
normalized *title* - contributors decide what groups into one infestation -
so nothing fans out until an operator names the account the platform speaks
as:
config :tarakan, :swarm_sweep,
actor_handle: "tarakan",
patterns_per_run: 25,
jobs_per_run: 40,
max_jobs_per_pattern: 8,
min_repos: 3
`max_jobs_per_pattern` is the bound that matters: a title generic enough to
span three hundred repositories still only spends eight of the run's budget.
"""
use Oban.Worker,
queue: :infestations,
max_attempts: 3,
unique: [period: 3600, states: :incomplete]
alias Tarakan.Accounts
alias Tarakan.Accounts.Scope
alias Tarakan.Infestations
alias Tarakan.Policy
require Logger
@defaults [
actor_handle: nil,
patterns_per_run: 25,
jobs_per_run: 40,
max_jobs_per_pattern: 8,
min_repos: 3,
days: 90
]
@impl Oban.Worker
def perform(%Oban.Job{}) do
config = config()
case actor(config[:actor_handle]) do
{:ok, scope} -> sweep(scope, config)
{:error, reason} -> skip(reason, config[:actor_handle])
end
end
@doc "Effective settings, application config merged over the defaults."
def config do
Keyword.merge(@defaults, Application.get_env(:tarakan, :swarm_sweep, []))
end
defp sweep(scope, config) do
patterns =
Infestations.list_infestations(
min_repos: config[:min_repos],
days: config[:days],
limit: config[:patterns_per_run]
)
result =
Enum.reduce(patterns, %{budget: config[:jobs_per_run], opened: 0, patterns: 0, held: 0}, fn
pattern, acc ->
sweep_pattern(scope, pattern, acc, config)
end)
# A silently truncated sweep reads like full coverage. Say what was left.
Logger.info(
"swarm sweep: opened #{result.opened} job(s) across #{result.patterns} pattern(s); " <>
"#{result.held} pattern(s) still cooling; #{result.budget} of " <>
"#{config[:jobs_per_run]} job budget unspent"
)
:ok
end
defp sweep_pattern(_scope, _pattern, %{budget: budget} = acc, _config) when budget <= 0, do: acc
defp sweep_pattern(scope, pattern, acc, config) do
limit = min(config[:max_jobs_per_pattern], acc.budget)
case Infestations.swarm_check_jobs(scope, pattern.pattern_key, limit: limit) do
{:ok, %{opened: 0}} ->
acc
{:ok, %{opened: opened}} ->
%{
acc
| budget: acc.budget - opened,
opened: acc.opened + opened,
patterns: acc.patterns + 1
}
{:error, {:cooldown, _seconds}} ->
%{acc | held: acc.held + 1}
{:error, reason} ->
Logger.warning("swarm sweep: #{pattern.pattern_key} failed: #{inspect(reason)}")
acc
end
end
defp actor(handle) when is_binary(handle) and handle != "" do
case Accounts.get_account_by_handle(handle) do
nil ->
{:error, :actor_not_found}
account ->
scope = Scope.for_account(account)
if Policy.moderator?(scope), do: {:ok, scope}, else: {:error, :actor_not_moderator}
end
end
defp actor(_handle), do: {:error, :not_configured}
# Never an Oban failure: a sweep with no actor is a deployment that has not
# opted in, not a job worth retrying.
defp skip(:not_configured, _handle), do: :ok
defp skip(reason, handle) do
Logger.warning("swarm sweep skipped (#{reason}): actor_handle=#{inspect(handle)}")
:ok
end
end