Documentation
¶
Overview ¶
Package fakepool provides a deterministic in-process replacement for nntppool.Client used by tests that exercise AltMount's streaming and connection-management invariants.
The real nntppool.Client opens TCP sockets, dials providers, and runs background reconnect / quota / pipelining goroutines. None of that is useful when the question under test is "does the streaming pipeline cap in-flight downloads", "does a slow provider cause a retry storm", or "do ephemeral readers leak goroutines". For those questions we need a client we can drive moment-by-moment: control latency, inject specific errors, count concurrent calls, and observe whether the caller honors the backpressure we apply.
Client satisfies pool.NntpClient (see internal/pool/nntpclient.go). The production code never sees a difference; tests inject *Client directly via a pool getter closure that returns it as pool.NntpClient.
Observability primitives ¶
Every Body / BodyPriority / BodyAsync / Stat call increments InFlight on entry and decrements on exit. MaxInFlight records the high-water mark. Tests assert against these counters to pin invariants like "no more than N segment downloads are ever in flight".
Failure injection ¶
SegmentBehavior controls per-message-ID latency, byte payload, and error. Set via SetBehavior or SetDefaultBehavior. Behaviors are evaluated at the start of each call; later changes apply only to subsequent calls.
Backpressure simulation ¶
BlockUntil pins every in-flight call at the entry semaphore until the caller closes the returned channel. Use it to hold connections "open" while observing how the rest of the pipeline reacts.
All public methods are safe for concurrent use.
Index ¶
- Variables
- func AssertMaxInFlightLE(tb interface{ ... }, c *Client, n int32)
- type Client
- func (c *Client) BlockUntil(release <-chan struct{})
- func (c *Client) Body(ctx context.Context, messageID string, onMeta ...func(nntppool.YEncMeta)) (*nntppool.ArticleBody, error)
- func (c *Client) BodyAsync(ctx context.Context, messageID string, w io.Writer, ...) <-chan nntppool.BodyResult
- func (c *Client) BodyAsyncCalls() int64
- func (c *Client) BodyCalls() int64
- func (c *Client) BodyPriority(ctx context.Context, messageID string, onMeta ...func(nntppool.YEncMeta)) (*nntppool.ArticleBody, error)
- func (c *Client) BodyPriorityCalls() int64
- func (c *Client) InFlight() int32
- func (c *Client) MaxInFlight() int32
- func (c *Client) PerMessageCalls(messageID string) int64
- func (c *Client) ResetCounters()
- func (c *Client) SetBehavior(messageID string, b SegmentBehavior)
- func (c *Client) SetDefaultBehavior(b SegmentBehavior)
- func (c *Client) SetStats(s nntppool.ClientStats)
- func (c *Client) Stat(ctx context.Context, messageID string) (*nntppool.StatResult, error)
- func (c *Client) StatCalls() int64
- func (c *Client) StatMany(ctx context.Context, messageIDs []string, opts nntppool.StatManyOptions) <-chan nntppool.StatManyResult
- func (c *Client) Stats() nntppool.ClientStats
- func (c *Client) TotalCalls() int64
- type SegmentBehavior
Constants ¶
This section is empty.
Variables ¶
var ErrSimulated502 = errors.New("fakepool: simulated 502 service unavailable")
ErrSimulated502 is a generic transient error suitable for retry-storm scenarios. It is wrapped so errors.Is(err, ErrSimulated502) works.
Functions ¶
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a fake nntppool.Client suitable for unit and concurrency tests.
func New ¶
func New() *Client
New returns a fake client. Without further configuration it returns an empty (zero-byte) ArticleBody immediately for every message-ID.
func (*Client) BlockUntil ¶
func (c *Client) BlockUntil(release <-chan struct{})
BlockUntil installs a gate that pins every subsequent call inside the fake (after counter increment, before doing any work) until release is closed. Useful for asserting "exactly N calls are concurrently in flight while the gate is closed".
Pass nil to remove the gate. Setting a new gate replaces any prior one; previously gated calls observing the old gate continue to wait on it.
func (*Client) Body ¶
func (c *Client) Body(ctx context.Context, messageID string, onMeta ...func(nntppool.YEncMeta)) (*nntppool.ArticleBody, error)
Body satisfies pool.NntpClient. Returns either the configured error or an ArticleBody filled with the configured Bytes after the configured latency.
func (*Client) BodyAsync ¶
func (c *Client) BodyAsync(ctx context.Context, messageID string, w io.Writer, onMeta ...func(nntppool.YEncMeta)) <-chan nntppool.BodyResult
BodyAsync streams the configured Bytes (or error) to w and yields a BodyResult on the returned channel.
func (*Client) BodyAsyncCalls ¶
BodyAsyncCalls returns the count of BodyAsync invocations.
func (*Client) BodyPriority ¶
func (c *Client) BodyPriority(ctx context.Context, messageID string, onMeta ...func(nntppool.YEncMeta)) (*nntppool.ArticleBody, error)
BodyPriority is identical to Body but counted separately so tests can distinguish streaming from importer traffic.
func (*Client) BodyPriorityCalls ¶
BodyPriorityCalls returns the count of BodyPriority invocations. This is the most useful counter for streaming-path tests since UsenetReader uses BodyPriority exclusively.
func (*Client) MaxInFlight ¶
MaxInFlight returns the high-water mark of InFlight observed since the client was created (or since the last ResetCounters).
func (*Client) PerMessageCalls ¶
PerMessageCalls returns how many times the given message-ID was requested across all method types (Body / BodyPriority / BodyAsync / Stat).
This is the primary signal for retry-storm tests: if a retry policy is re-issuing failed requests, the per-ID count climbs faster than the number of distinct segments and the assertion fails.
func (*Client) ResetCounters ¶
func (c *Client) ResetCounters()
ResetCounters zeroes all observability counters. Behaviors and gates are preserved. Useful between phases of a single test.
func (*Client) SetBehavior ¶
func (c *Client) SetBehavior(messageID string, b SegmentBehavior)
SetBehavior sets a per-message-ID behavior. Overrides SetDefaultBehavior.
func (*Client) SetDefaultBehavior ¶
func (c *Client) SetDefaultBehavior(b SegmentBehavior)
SetDefaultBehavior sets the behavior used for any message-ID that has no per-segment override.
func (*Client) SetStats ¶
func (c *Client) SetStats(s nntppool.ClientStats)
SetStats overrides what Stats() returns. Tests that exercise the metrics tracker can use this to feed deterministic provider data.
func (*Client) Stat ¶
Stat returns a StatResult with the message-ID echoed, after the configured latency. If the behavior has Err set, it is returned.
func (*Client) StatMany ¶
func (c *Client) StatMany(ctx context.Context, messageIDs []string, opts nntppool.StatManyOptions) <-chan nntppool.StatManyResult
StatMany satisfies pool.NntpClient by fanning Stat out across up to opts.Concurrency goroutines, mirroring nntppool's StatMany semantics: results stream out of order, the channel closes once every dispatched check reports, and ctx cancellation stops dispatch and lets in-flight sends bail out.
func (*Client) Stats ¶
func (c *Client) Stats() nntppool.ClientStats
Stats returns the configured stats or a zero value.
func (*Client) TotalCalls ¶
TotalCalls returns the total number of method invocations served.
type SegmentBehavior ¶
type SegmentBehavior struct {
// Latency is the wall-clock delay added before the call returns.
// Honors ctx cancellation: if the context fires first, the call
// returns ctx.Err() and does not pretend to have produced data.
Latency time.Duration
// Bytes is the payload returned in ArticleBody.Bytes. Length is also
// used for BytesDecoded.
Bytes []byte
// Err, if non-nil, is returned instead of a body. Use nntppool sentinel
// errors (e.g. nntppool.ErrArticleNotFound) to exercise specific paths
// in the retry/dispatch logic.
Err error
// YEnc, if non-zero, is populated on ArticleBody.YEnc and fired via any
// onMeta callbacks. Use this to inject yEnc header metadata (FileName,
// PartSize, FileSize, etc.) without needing a real NNTP server.
YEnc nntppool.YEncMeta
// FailFirst makes the first N calls to this message-ID return FailErr (a
// transient error) before subsequent calls succeed normally. Use it to
// exercise retry logic that must distinguish transient failures from a
// permanent article-not-found. Zero disables this behavior.
FailFirst int
// FailErr is the transient error returned during the FailFirst window.
// Defaults to a generic "all providers exhausted"-style error when nil.
FailErr error
}
SegmentBehavior describes how the fake should respond to a single message-ID. The zero value returns an empty body with no delay.