Documentation
¶
Overview ¶
Package usenet is Ether's Usenet layer: an NNTP client, per-provider connection pools with priority scheduling and circuit breakers, and a multi-provider fetcher that implements core.ArticleFetcher with primary-first ordering and backfill from block accounts.
Index ¶
- Constants
- Variables
- func ShareOf(capacity int, self string, weights map[string]int) int
- func Shares(capacity int, weights map[string]int) map[string]int
- func TestProvider(ctx context.Context, p config.UsenetProvider) error
- type BodyReader
- type Conn
- func (c *Conn) Body(ctx context.Context, messageID string) (*BodyReader, error)
- func (c *Conn) BodyDecode(ctx context.Context, messageID string, opt *yenc.DecodeOptions) (*yenc.Part, error)
- func (c *Conn) Broken() bool
- func (c *Conn) Close() error
- func (c *Conn) CurrentGroup() string
- func (c *Conn) Date(ctx context.Context) (time.Time, error)
- func (c *Conn) Group(ctx context.Context, name string) error
- func (c *Conn) Quit() error
- func (c *Conn) Stat(ctx context.Context, messageID string) (bool, error)
- func (c *Conn) StatMany(ctx context.Context, messageIDs []string) (map[string]bool, error)
- type DialOptions
- type Fetcher
- func (f *Fetcher) Body(ctx context.Context, messageID string, groups []string, prio core.Priority) ([]byte, error)
- func (f *Fetcher) BodyPart(ctx context.Context, messageID string, groups []string, prio core.Priority) ([]byte, core.ArticlePart, error)
- func (f *Fetcher) BodyPartBurst(ctx context.Context, id string, groups []string, prio core.Priority, ...) ([]byte, core.ArticlePart, error)
- func (f *Fetcher) CheckBody(ctx context.Context, messageID string, groups []string, prio core.Priority) error
- func (f *Fetcher) CheckBodyPart(ctx context.Context, messageID string, groups []string, prio core.Priority, ...) error
- func (f *Fetcher) Close() error
- func (f *Fetcher) MaxConnections() int
- func (f *Fetcher) SetConnectionShare(share func(configured int) int)
- func (f *Fetcher) Stat(ctx context.Context, messageID string) (bool, error)
- func (f *Fetcher) StatMany(ctx context.Context, messageIDs []string) (map[string]bool, error)
- func (f *Fetcher) StatStream(ctx context.Context, messageIDs []string, ...) error
- func (f *Fetcher) Stats() []ProviderStats
- func (f *Fetcher) TestProvider(ctx context.Context, p config.UsenetProvider) error
- func (f *Fetcher) Update(providers []config.UsenetProvider)
- func (f *Fetcher) UpdateConfig(u config.Usenet)
- func (f *Fetcher) Warm(ctx context.Context, perProvider int) error
- type Options
- type ProtocolError
- type ProviderError
- type ProviderStats
Constants ¶
const ( StateOK = "ok" StateBackoff = "backoff" // connection failures; retrying after a delay StateAuthFail = "auth_failed" // disabled; re-probed after a growing backoff or on a settings change StateClosed = "closed" )
Provider states reported in statistics.
const ( DefaultDialTimeout = 15 * time.Second DefaultCommandTimeout = 60 * time.Second DefaultIdleReadTimeout = 5 * time.Second )
Default timeouts for a single connection.
const ( DefaultConnections = 8 DefaultBodyPipelineDepth = 4 DefaultMinIdle = 2 DefaultPrefetchFraction = 0.25 DefaultIdleTimeout = 90 * time.Second DefaultKeepAlive = 30 * time.Second DefaultMissTTL = 2 * time.Minute DefaultMissCacheSize = 100_000 DefaultRetries = 2 DefaultBackoffMin = time.Second DefaultBackoffMax = 2 * time.Minute DefaultFailThreshold = 2 DefaultLimitCooloff = 5 * time.Minute DefaultAuthRetryMin = 15 * time.Minute DefaultAuthRetryMax = 6 * time.Hour )
Defaults for Options.
const ( ProblemConfig = "config" // missing or invalid settings ProblemDNS = "dns" // host name does not resolve ProblemUnreachable = "unreachable" // refused, no route, reset ProblemTimeout = "timeout" // no answer in time ProblemTLS = "tls" // handshake or certificate failure ProblemAuth = "auth" // credentials rejected ProblemProtocol = "protocol" // not an NNTP server, or unexpected replies )
Problem kinds reported by TestProvider.
MaxShareWeight bounds a node's weight in Shares.
Variables ¶
var ( // ErrNoSuchArticle is a 430 (or 423) reply: this provider does not have // the article. The fetcher turns "every provider said 430" into // core.ErrArticleMissing. ErrNoSuchArticle = errors.New("nntp: no such article") // ErrAuthFailed means the provider rejected the credentials (481/482). ErrAuthFailed = errors.New("nntp: authentication failed") // ErrAuthRequired is a 480 reply to a command. ErrAuthRequired = errors.New("nntp: authentication required") // refusing service, usually because of too many connections. ErrServiceUnavailable = errors.New("nntp: service unavailable") // ErrNoSuchGroup is a 411 reply to GROUP. ErrNoSuchGroup = errors.New("nntp: no such group") // ErrCorrupt means an article was received but did not decode as valid // yEnc (bad CRC, wrong size, missing headers). ErrCorrupt = core.ErrArticleCorrupt // ErrInvalidMessageID rejects message-ids that would break the protocol. ErrInvalidMessageID = errors.New("usenet: invalid message-id") // ErrNoProviders means no provider is configured and enabled. ErrNoProviders = fmt.Errorf("usenet: no usable providers: %w", core.ErrSourceUnavailable) // ErrProviderBackoff means a provider is temporarily skipped after // repeated connection failures. ErrProviderBackoff = fmt.Errorf("usenet: provider backing off after connection failures: %w", core.ErrSourceUnavailable) // ErrProviderDisabled means a provider is disabled after an // authentication failure, until a background login probe succeeds or // its settings change. ErrProviderDisabled = fmt.Errorf("usenet: provider disabled after authentication failure: %w", core.ErrSourceUnavailable) // ErrPoolClosed means the provider was removed or the fetcher closed. ErrPoolClosed = errors.New("usenet: connection pool closed") // ErrConnBroken means a connection was used after an I/O failure. ErrConnBroken = errors.New("nntp: connection broken") )
Errors reported by the NNTP client and the fetcher. They are wrapped with context; test with errors.Is.
Functions ¶
func ShareOf ¶
ShareOf is self's part of capacity under Shares; capacity itself when self is the only node or not a member.
func Shares ¶
Shares divides capacity (an account-wide connection limit) among live cluster nodes in proportion to their weights (0 or less counts as 1): node i gets floor(capacity*w_i/Σw), raised to at least 1. When raising pushes the total over capacity, the largest shares give one back each (ties: the higher node ID first) until the total fits, so the cluster never opens more than capacity connections while capacity ≥ nodes.
When capacity is smaller than the number of nodes every node still gets one connection: a node without connections could not stream at all, and a provider that refuses the extra logins is handled by the pool's server-limit adaptation. The result is deterministic for a membership list, so every node computes the same division independently.
func TestProvider ¶
func TestProvider(ctx context.Context, p config.UsenetProvider) error
TestProvider is the setup wizard's check with default options.
Types ¶
type BodyReader ¶
type BodyReader struct {
// contains filtered or unexported fields
}
BodyReader streams a multi-line response, undoing dot-stuffing. It implements yenc.LineReader (zero-copy lines straight from the connection buffer) and io.Reader (lines with their CRLF terminators).
func (*BodyReader) Close ¶
func (b *BodyReader) Close() error
Close drains the remainder of the body so the connection can be reused.
func (*BodyReader) Read ¶
func (b *BodyReader) Read(p []byte) (int, error)
Read implements io.Reader over the unstuffed body.
func (*BodyReader) ReadLine ¶
func (b *BodyReader) ReadLine() ([]byte, error)
ReadLine returns the next body line without its terminator. The slice is valid until the next call. It returns io.EOF after the terminating ".".
type Conn ¶
type Conn struct {
// PostingAllowed reports a 200 (vs 201) greeting.
PostingAllowed bool
// Greeting is the server's welcome text.
Greeting string
// contains filtered or unexported fields
}
Conn is one NNTP client connection. It is not safe for concurrent use; the pool hands each connection to one request at a time.
func Dial ¶
func Dial(ctx context.Context, o DialOptions) (*Conn, error)
Dial connects, reads the greeting and authenticates if credentials are given. Errors wrap ErrAuthFailed, ErrServiceUnavailable, a *tls error or a network error so callers can tell bad credentials from an unreachable server.
func (*Conn) Body ¶
Body issues BODY and returns a reader over the dot-unstuffed body. The reader must be closed (which drains any unread remainder) before the connection is used again.
func (*Conn) BodyDecode ¶
func (c *Conn) BodyDecode(ctx context.Context, messageID string, opt *yenc.DecodeOptions) (*yenc.Part, error)
BodyDecode fetches an article and yEnc-decodes it straight from the connection buffer. Decoding problems wrap ErrCorrupt; the connection stays usable after them because the body is drained to its terminator.
func (*Conn) CurrentGroup ¶
CurrentGroup is the last group selected with Group.
type DialOptions ¶
type DialOptions struct {
Host string
Port int // 0 = 563 with TLS, 119 without
TLS bool
Username string
Password string
// TLSConfig is the base TLS configuration (e.g. custom roots in tests).
// ServerName defaults to Host.
TLSConfig *tls.Config
// Timeout bounds dial, TLS handshake, greeting and authentication.
Timeout time.Duration
// CommandTimeout bounds each command including reading its response.
CommandTimeout time.Duration
// IdleReadTimeout bounds a stalled read; progress renews it. Negative disables.
IdleReadTimeout time.Duration
// DialContext overrides the network dialer.
DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
// BytesRead, when set, is incremented with every byte read off the wire.
BytesRead *atomic.Int64
// OnPhase receives durations for tcp, tls, greeting, and auth setup phases.
OnPhase func(string, time.Duration)
// OnStatus receives the raw server status line for trace diagnostics.
OnStatus func(string)
// OnCommand receives a protocol command before it is sent. AUTHINFO
// credentials are redacted so trace logging cannot expose them.
OnCommand func(string)
}
DialOptions describe how to reach and log in to one NNTP server.
type Fetcher ¶
type Fetcher struct {
// contains filtered or unexported fields
}
Fetcher implements core.ArticleFetcher over the owner's providers.
Ordering: non-backfill providers are tried in ascending Priority; providers sharing a Priority form a tier and share load (the least busy is tried first). Only when every non-backfill provider answered 430 are backfill (block) providers tried, in the same way. Transient errors are retried on fresh connections and then on the next provider.
func New ¶
func New(providers []config.UsenetProvider, opts Options) *Fetcher
New builds a fetcher for providers. Connections are dialled lazily, except each primary provider's MinIdle warm floor, which the pool janitors establish (and keep) in the background.
func (*Fetcher) Body ¶
func (f *Fetcher) Body(ctx context.Context, messageID string, groups []string, prio core.Priority) ([]byte, error)
Body returns the decoded payload of one article.
func (*Fetcher) BodyPart ¶
func (f *Fetcher) BodyPart(ctx context.Context, messageID string, groups []string, prio core.Priority) ([]byte, core.ArticlePart, error)
BodyPart returns the decoded payload and its yEnc part header. It returns an error wrapping core.ErrArticleMissing only when every provider able to answer said 430. Providers locked out after an authentication failure cannot answer and are not counted; providers in connection backoff are.
func (*Fetcher) BodyPartBurst ¶ added in v0.2.8
func (*Fetcher) CheckBody ¶
func (f *Fetcher) CheckBody(ctx context.Context, messageID string, groups []string, prio core.Priority) error
CheckBody verifies actual BODY availability without downloading a full article. See CheckBodyPart.
func (*Fetcher) CheckBodyPart ¶
func (f *Fetcher) CheckBodyPart(ctx context.Context, messageID string, groups []string, prio core.Priority, want core.ArticlePart) error
CheckBodyPart verifies that BODY really serves the expected yEnc part: providers often answer 223 to STAT while BODY returns a 451/430 DMCA takedown. It reads only the =ybegin/=ypart header lines (bounded, so a malicious unterminated line cannot consume memory), checks them against want where want is known (zero fields are unchecked), and then either drains a small remainder so the connection stays reusable or closes the connection so unread bytes cannot poison reuse. It returns an error wrapping core.ErrArticleMissing when every provider answered 430/451, and ErrCorrupt when a provider served something that is not the expected yEnc part.
func (*Fetcher) MaxConnections ¶
MaxConnections is the sum of all enabled primary providers' connection limits on this node: the configured counts, or this node's shares of them when SetConnectionShare divides them among cluster nodes.
func (*Fetcher) SetConnectionShare ¶
SetConnectionShare divides provider connection limits among cluster nodes: share receives a provider's configured connection count (the account-wide limit) and returns this node's part of it, clamped to 1..configured. Nil restores the configured counts (a standalone server). Pools resize in place and gracefully (see pool.setShare); providers added later by Update get their share as they are created. Call it again whenever membership changes.
func (*Fetcher) Stat ¶
Stat reports whether any provider has the article (STAT, at live priority since health checks gate playback). It returns (false, nil) when every provider able to answer said 430, and an error when that could not be established.
func (*Fetcher) StatMany ¶
StatMany checks a set of articles while pipelining commands over several connections to the first available provider. Missing articles are retried through the normal provider chain, preserving block-account backfill. Results are keyed by the message-ids exactly as given.
func (*Fetcher) StatStream ¶
func (f *Fetcher) StatStream(ctx context.Context, messageIDs []string, fn func(id string, present bool) bool) error
StatStream checks a set of articles with pipelined STAT, reporting each article to fn as soon as its fate is decided: present=true the moment any provider answers 223, and present=false once every provider that could answer said 430 (primary tiers first, then backfill). Results flow per pipelined chunk as they arrive instead of waiting for the slowest batch, so a caller that only needs the first confirmed miss (a health check) can stop early: fn returning false cancels all outstanding work and StatStream returns nil.
An article is never reported missing on the strength of provider errors: if some provider failed to answer for it, it stays undecided and StatStream returns the first such error after reporting everything that was decided. fn is called from one goroutine at a time, with ids exactly as the caller passed them.
func (*Fetcher) Stats ¶
func (f *Fetcher) Stats() []ProviderStats
Stats returns a snapshot per enabled provider, in configuration order.
func (*Fetcher) TestProvider ¶
TestProvider connects to p, logs in and runs DATE, using the fetcher's TLS and dialer options. It does not touch the fetcher's pools.
func (*Fetcher) Update ¶
func (f *Fetcher) Update(providers []config.UsenetProvider)
Update swaps in a new provider list. Providers whose settings are unchanged keep their pool, connections, statistics and circuit-breaker state; changed providers get a fresh pool (which also clears an authentication lockout); removed or disabled ones are closed gracefully: queued requests move on to other providers and busy connections close when their current article finishes.
func (*Fetcher) UpdateConfig ¶
UpdateConfig applies an edited Usenet config: the provider list (see Update) and the MinIdle warm floor, live and without a restart. MinIdle 0 takes the default; negative disables the floor.
type Options ¶
type Options struct {
// BodyPipelineDepth bounds queued BODY commands per socket (default 4,
// maximum 16). One disables pipelining. Readahead normally uses at most two,
// with a reserved live connection; explicit short bursts can borrow idle
// capacity and use this depth. Prefetch, recovery probes and single-connection readahead
// use one. Speculative batches stop growing when live work waits.
BodyPipelineDepth int
Logger *slog.Logger
// PrefetchFraction is the share of each provider's connections that
// PriorityPrefetch requests may hold at once (at least one).
PrefetchFraction float64
// DialTimeout bounds connect + TLS + greeting + login.
DialTimeout time.Duration
// CommandTimeout bounds one command, including reading an article body.
CommandTimeout time.Duration
// IdleReadTimeout bounds stalled command/body reads; default 5 seconds.
IdleReadTimeout time.Duration
// MinIdle keeps this many primary (non-backfill) connections per
// provider warm at all times: established in the background, held
// past IdleTimeout and DATE-pinged so Play never pays a cold dial.
// 0 = DefaultMinIdle; negative disables. A provider's own
// config.UsenetProvider.MinIdle overrides it.
MinIdle int
// IdleTimeout closes excess connections unused for this long.
IdleTimeout time.Duration
// KeepAlive pings (DATE) connections idle for this long so servers do not
// drop them; negative disables pings.
KeepAlive time.Duration
// MissTTL is how long a 430 from a provider is remembered per article;
// negative disables the miss cache.
MissTTL time.Duration
MissCacheSize int
// Retries is how many extra attempts a provider gets, each on another
// connection, after transient (network/protocol) errors.
Retries int
// Circuit breaker: after FailThreshold consecutive failed connects a
// provider is skipped for BackoffMin, doubling up to BackoffMax.
BackoffMin time.Duration
BackoffMax time.Duration
FailThreshold int
// AuthRetryMin/Max: a provider that rejected the credentials (481/482)
// is re-probed with one login after AuthRetryMin, doubling per failed
// probe up to AuthRetryMax, so transient auth outages self-heal.
AuthRetryMin time.Duration
AuthRetryMax time.Duration
// LimitCooloff: when a server refuses connections beyond some number
// while others work, the pool shrinks to that number for this long.
LimitCooloff time.Duration
// TLSConfig is the base TLS client configuration (custom roots, e.g. in
// tests). Nil uses the system roots.
TLSConfig *tls.Config
// DialContext overrides the network dialer.
DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
// UseGroup issues GROUP (first of the segment's groups) before BODY. Most
// providers serve BODY <message-id> without it.
UseGroup bool
// MaxArticleSize caps a decoded article; 0 = yenc.DefaultMaxSize.
MaxArticleSize int64
// TraceProtocol emits raw NNTP status lines at debug level.
TraceProtocol bool
}
Options tune the fetcher. Zero values take the defaults above.
type ProtocolError ¶
ProtocolError is an unexpected NNTP status reply.
func (*ProtocolError) Error ¶
func (e *ProtocolError) Error() string
func (*ProtocolError) Unwrap ¶
func (e *ProtocolError) Unwrap() error
Unwrap maps well-known codes to sentinel errors.
type ProviderError ¶
ProviderError is TestProvider's failure: a stable Kind for the UI and a human-readable Message suitable for showing to the owner as-is.
func (*ProviderError) Error ¶
func (e *ProviderError) Error() string
func (*ProviderError) Unwrap ¶
func (e *ProviderError) Unwrap() error
type ProviderStats ¶
type ProviderStats struct {
ID string `json:"id"`
Name string `json:"name"`
Host string `json:"host"`
Priority int `json:"priority"`
Backfill bool `json:"backfill"`
// State is StateOK, StateBackoff or StateAuthFail.
State string `json:"state"`
BackoffUntil time.Time `json:"backoffUntil,omitempty"`
LastError string `json:"lastError,omitempty"`
LastErrorAt time.Time `json:"lastErrorAt,omitempty"`
// MaxConnections is the effective limit; it drops below
// ConfiguredConnections for a while when the server refuses more.
MaxConnections int `json:"maxConnections"`
ConfiguredConnections int `json:"configuredConnections"`
LiveConnections int `json:"liveConnections"` // open sockets
ActiveConnections int `json:"activeConnections"` // slots in use
IdleConnections int `json:"idleConnections"`
// MinIdle is the configured warm floor (0 = none for this pool);
// WarmIdleConnections is how much of it is currently held idle.
MinIdle int `json:"minIdle"`
WarmIdleConnections int `json:"warmIdleConnections"`
BackgroundRedials int64 `json:"backgroundRedials"` // floor janitor dials
RedialFailures int64 `json:"redialFailures"`
PrefetchActive int `json:"prefetchActive"`
WaitingLive int `json:"waitingLive"`
WaitingReadahead int `json:"waitingReadahead"`
WaitingPrefetch int `json:"waitingPrefetch"`
// account's connections are divided among cluster nodes (0 otherwise).
NodeShare int `json:"nodeShare,omitempty"`
Bytes int64 `json:"bytes"` // received off the wire
Articles int64 `json:"articles"`
Misses int64 `json:"misses"`
BodyMisses int64 `json:"bodyMisses"` // BODY responses unavailable on this provider
BodyTakedowns int64 `json:"bodyTakedowns"` // subset of BodyMisses with status 451
BackfillSaves int64 `json:"backfillSaves"` // body fetches saved after earlier provider misses
Errors int64 `json:"errors"`
Dials int64 `json:"dials"`
AvgLatency time.Duration `json:"avgLatency"` // command round trip (EWMA)
AvgFetchTime time.Duration `json:"avgFetchTime"` // whole article (EWMA)
TCPDialTime time.Duration `json:"tcpDialTime"`
TLSHandshakeTime time.Duration `json:"tlsHandshakeTime"`
GreetingTime time.Duration `json:"greetingTime"`
AuthTime time.Duration `json:"authTime"`
BODYBatches int64 `json:"bodyBatches"`
BODYPipelined int64 `json:"bodyPipelined"`
STATBatches int64 `json:"statBatches"`
STATBatchTime time.Duration `json:"statBatchTime"`
}
ProviderStats is a snapshot of one provider for the admin dashboard.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package nntptest provides an in-memory NNTP server for tests across Ether: articles keyed by message-id (yEnc-encoded on the way in), optional AUTHINFO, TLS, missing articles, latency, a connection limit and fault injection, plus Post, which uploads files as multipart yEnc segments and returns a matching NZB document.
|
Package nntptest provides an in-memory NNTP server for tests across Ether: articles keyed by message-id (yEnc-encoded on the way in), optional AUTHINFO, TLS, missing articles, latency, a connection limit and fault injection, plus Post, which uploads files as multipart yEnc segments and returns a matching NZB document. |