Health, speed and rate-limit state for SearXNG instances, learned from the
searches this node makes. One pool runs under the :perplexed application;
tests and hosts can start more with :name.
Ranking
Each instance keeps exponentially weighted moving averages (EWMA) of its response time and failure rate, and is scored
latency_ewma_ms * (1 + failure_penalty * failure_ewma) * jitterlowest first. An instance not measured yet scores its prior: the
searx.space median when it was seed/2ed, else :prior_ms. :jitter
multiplies each score by a random factor in 1 +/- jitter, so the
fastest few share the load instead of one taking every search.
:min_interval_ms ranks an instance used that recently after every
rested one, least recently used first.
Circuit breaker
Each instance is a small state machine:
:closed --refusal--> :open --cooldown timer--> :half_open
^ ^ |
| +--------trial fails-------+
+------------------------trial answers---------+A refusal (HTTP 429 or 503, a redirect from SearXNG's limiter, 403/418, a
bot challenge, a timeout, a transport error) opens the circuit. The
cooldown is the server's own Retry-After / RateLimit-Reset when it
sent one, otherwise :cooldown_ms doubled per consecutive refusal, capped
at :max_cooldown_ms either way. When it ends a timer moves the instance
to :half_open, where it hands out a single trial lease: an answer closes
the circuit, a refusal opens it again for twice as long. An open instance
is never checked out. An empty or off-topic answer is a soft failure: the
instance is reachable, so the circuit closes, but its failure average
rises, and :soft_strikes (default 3) in a row open the circuit like a
refusal: an instance that keeps answering off-topic is skipped for a
while however fast it is.
Leases
checkout/3 picks and reserves instances in one step inside the server,
so concurrent searches spread out instead of racing onto the same
instance. The pool monitors the caller; if it dies, its leases are
released. record/4 reports the outcome and returns the lease.
Rankings and stats are read straight from ETS, so inspection never waits
on the process. Instances idle longer than :forget_after_ms are swept
out.
Every state change emits [:perplexed, :searxng, :circuit] with metadata
instance, from, to and cooldown_ms.
Summary
Functions
Reserves up to count of candidates, best first, skipping open
circuits and half-open ones whose trial is already out. Returns the
leases to pass to record/4.
Returns a specification to start this module under a supervisor.
The instances in candidates that checkout/3 would hand out, best
first, without reserving them. Takes checkout/3's options.
Reports how a leased search went and releases the lease. outcome is
{:ok, hits}, {:error, reason}, or {:error, reason, retry_after_ms}
when the instance said how long to wait.
Forgets everything the pool learned and cancels its timers.
Sets prior response times (ms) for instances not measured yet, such as
searx.space medians from Perplexed.Search.SearXNG.Directory.
Every instance the pool knows, with its stats, best first.
Starts a pool.
The stats for instance (fresh ones when it has none yet).
Types
@type circuit() :: :closed | :open | :half_open
@type outcome() :: {:ok, non_neg_integer()} | {:error, atom()} | {:error, atom(), non_neg_integer() | nil}
@type stats() :: %{ circuit: circuit(), latency_ms: float() | nil, failure_rate: float(), successes: non_neg_integer(), failures: non_neg_integer(), consecutive_failures: non_neg_integer(), consecutive_soft: non_neg_integer(), cooldown_until: integer() | nil, in_flight: non_neg_integer(), last_outcome: atom() | nil, last_used_at: integer() | nil, prior_ms: number() | nil, seeded_at: integer() | nil }
Functions
@spec checkout([String.t()], non_neg_integer(), keyword()) :: [lease()]
Reserves up to count of candidates, best first, skipping open
circuits and half-open ones whose trial is already out. Returns the
leases to pass to record/4.
Options: :pool, :jitter (0 to 1, default 0), :min_interval_ms
(default 0), :prior_ms (default 2500), :failure_penalty
(default 3.0) and :random (a zero-arity function
returning a float in [0, 1), for tests).
Returns a specification to start this module under a supervisor.
See Supervisor.
The instances in candidates that checkout/3 would hand out, best
first, without reserving them. Takes checkout/3's options.
@spec record(lease(), outcome(), non_neg_integer(), keyword()) :: :ok
Reports how a leased search went and releases the lease. outcome is
{:ok, hits}, {:error, reason}, or {:error, reason, retry_after_ms}
when the instance said how long to wait.
@spec reset(keyword()) :: :ok
Forgets everything the pool learned and cancels its timers.
Sets prior response times (ms) for instances not measured yet, such as
searx.space medians from Perplexed.Search.SearXNG.Directory.
Every instance the pool knows, with its stats, best first.
@spec start_link(keyword()) :: GenServer.on_start()
Starts a pool.
Options: :name (default Perplexed.Search.SearXNG.Pool), :cooldown_ms
(default 60000), :max_cooldown_ms (default 1800000),
:forget_after_ms (default 21600000) and :sweep_every_ms
(default 600000) and :soft_strikes (default 3).
@spec stats(String.t(), GenServer.name()) :: stats()
The stats for instance (fresh ones when it has none yet).