politeness

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 2, 2026 License: MIT Imports: 2 Imported by: 0

Documentation

Overview

Package politeness enforces the rules that keep a crawl from hammering a host: the per-host and per-IP token buckets, the crawl-delay derived from robots.txt and adaptive backoff, and the gate that decides when a host's next fetch is eligible. It reads and writes the politeness fields of meguri.HostRecord and is consulted by the frontier scheduler before every dispatch.

This is part of the M3 milestone (politeness, DNS, robots, conditional GET). The package is a placeholder until then.

Package politeness is meguri's rate brain: the policy that decides how fast a single host may be crawled and keeps two independent throttles in step, one per host group and one per resolved IP. It owns no network and no state beyond the shared per-IP table; the frontier reads its functions to compute a host's next-eligible instant and to fold one fetch outcome into the host's adaptive interval (doc 07). The split is deliberate: meguri owns the politeness policy, ami owns the fetch mechanism.

The model is two one-token buckets reduced to timestamps. A bucket has no burst: a fetch may start only at or after its next-eligible instant, and the instant advances by one interval measured start-to-start. A host is dispatched only when BOTH its host bucket and its IP bucket permit, and dispatching spends both. The per-IP bucket is what stops meguri from hammering one machine through a hundred vhosts that share an address.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Clamp

func Clamp(d, lo, hi time.Duration) time.Duration

Clamp bounds d into [lo, hi]. lo wins if the band is inverted.

Types

type Config

type Config struct {
	Default time.Duration // baseline interval for a host with no signal yet
	Floor   time.Duration // hard minimum; never fetch one host faster than this
	Ceiling time.Duration // hard maximum; a host pinned here is flagged dead
	IPFloor time.Duration // per-IP minimum when many hosts share one address

	Backoff     float64       // multiplicative widen on 429/5xx, >= 1
	SoftWiden   float64       // multiplicative widen on sharply rising latency, >= 1
	Narrow      time.Duration // additive narrow toward the floor on a healthy fetch
	LatencyRise float64       // latency ratio that counts as "rising", e.g. 1.5
}

Config is the politeness policy: the interval band a host's rate must stay in and the AIMD reaction constants that turn fetch outcomes into a crawl rate. The defaults come from doc 07: a one-second baseline, a 250ms floor that is the physical wall meguri refuses to cross (D19), and a ceiling past which a host is treated as dead.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns the doc 07 baseline policy.

func (Config) Adapt

func (c Config) Adapt(adaptive time.Duration, s Signal) time.Duration

Adapt is the AIMD step: it maps the current adaptive interval and one fetch signal to the next interval, before clamping. The reactions, in order:

  • a 429 or 5xx multiplies the interval by Backoff and honors Retry-After as a floor, the multiplicative increase that backs a struggling host off fast.
  • sharply rising latency (Latency over PrevLatency*LatencyRise) softly widens by SoftWiden, easing off before the host starts erroring.
  • an otherwise healthy fetch narrows the interval by Narrow, the additive decrease that creeps a recovered host back toward the floor slowly.

The result is unclamped; the caller applies HostInterval to bound it into the host's legal band.

func (Config) AtCeiling

func (c Config) AtCeiling(clamped time.Duration) bool

AtCeiling reports whether the clamped interval has been pinned to the ceiling, the condition that, sustained over a fetch run, marks a host dead.

func (Config) HostInterval

func (c Config) HostInterval(adaptive, crawlDelay time.Duration) time.Duration

HostInterval clamps a host's adaptive interval into the legal band. The lower bound is the larger of the global floor and the host's published crawl-delay (from robots or config), so a site asking to be crawled slower is always honored and meguri never dips under its own physical floor. The upper bound is the ceiling.

type IPTable

type IPTable struct {
	// contains filtered or unexported fields
}

IPTable is the shared per-IP politeness state. Many host groups can resolve to one address (a CDN, shared hosting), and hammering that machine through a hundred vhosts is the rudeness the per-host bucket alone cannot catch. The table reduces each IP to a single next-eligible instant, advanced every time a host on that IP dispatches, and stays bounded by the in-flight IP working set because idle buckets are evicted.

It is safe for concurrent use: the engine dispatches many hosts at once and several of them may share an address.

func NewIPTable

func NewIPTable(floor time.Duration) *IPTable

NewIPTable returns an empty table whose per-IP interval never drops below floor, regardless of how fast an individual host on that IP is allowed to go.

func (*IPTable) EligibleAt

func (t *IPTable) EligibleAt(ip [16]byte) int64

EligibleAt returns the unix-second instant ip may next be fetched, or 0 when the IP is unknown or unresolved (the zero address).

func (*IPTable) Evict

func (t *IPTable) Evict(cutoff int64) int

Evict drops IP buckets untouched at or before cutoff, keeping the table sized to the active IP working set rather than every address ever resolved. It returns the number evicted.

func (*IPTable) Len

func (t *IPTable) Len() int

Len reports the number of resident IP buckets, the table's memory footprint.

func (*IPTable) Spend

func (t *IPTable) Spend(ip [16]byte, now int64, interval time.Duration)

Spend records a dispatch to ip at unix-second now using the host's interval, pushing the IP's next-eligible forward by at least the per-IP floor. A fetch is only dispatched when the IP already permits, so now is at or past the old next-eligible and the advance is monotonic. It is a no-op for the zero IP.

type Signal

type Signal struct {
	Status      int           // HTTP status, 0 for a transport error
	RetryAfter  time.Duration // Retry-After header value, 0 if none
	Latency     time.Duration // this fetch's round-trip time
	PrevLatency time.Duration // the host's smoothed latency before this fetch
}

Signal is the part of a fetch Outcome the rate controller reads.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL