Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
135 lines
4 KiB
Elixir
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
|