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) ArticleDateFor(messageID string) (time.Time, bool)
- func (c *Client) BlockUntil(release <-chan struct{})
- func (c *Client) BodyAsyncCalls() int64
- func (c *Client) BodyBackgroundCalls() int64
- func (c *Client) BodyCalls() int64
- func (c *Client) BodyPriorityCalls() int64
- func (c *Client) BodyStreamPriorityCalls() int64
- func (c *Client) Exists(ctx context.Context, r nntppool.Req) (*nntppool.StatResult, error)
- func (c *Client) ExistsMany(ctx context.Context, messageIDs []string, opts nntppool.ManyOptions) <-chan nntppool.ExistsResult
- func (c *Client) Fetch(ctx context.Context, r nntppool.Req) (*nntppool.ArticleBody, error)
- func (c *Client) FetchAsync(ctx context.Context, r nntppool.Req) <-chan nntppool.BodyResult
- func (c *Client) InFlight() int32
- func (c *Client) MaxInFlight() int32
- func (c *Client) PerMessageBodyCalls(messageID string) int64
- 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) StatCalls() int64
- func (c *Client) StatPriorityCalls() int64
- 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) ArticleDateFor ¶
ArticleDateFor returns the ArticleDate last passed for the given message-ID, and whether it was requested at all.
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) BodyAsyncCalls ¶
BodyAsyncCalls returns the count of BodyAsync invocations.
func (*Client) BodyBackgroundCalls ¶
BodyBackgroundCalls reports how many BodyBackground (repair) fetches were made.
func (*Client) BodyPriorityCalls ¶
BodyPriorityCalls returns the count of priority-lane body invocations, buffered (BodyPriority) and streamed (BodyStreamPriority) alike. Tests asserting "streaming uses the priority lane" read this.
func (*Client) BodyStreamPriorityCalls ¶
BodyStreamPriorityCalls returns the count of BodyStreamPriority invocations.
func (*Client) Exists ¶
Exists returns a StatResult with the message-ID echoed, after the configured latency. If the behavior has Err set, it is returned. A priority-lane check counts toward both StatCalls and StatPriorityCalls, so "no existence check happened" assertions hold for either lane.
func (*Client) ExistsMany ¶
func (c *Client) ExistsMany(ctx context.Context, messageIDs []string, opts nntppool.ManyOptions) <-chan nntppool.ExistsResult
ExistsMany satisfies pool.NntpClient by fanning Exists out across up to opts.Concurrency goroutines, mirroring nntppool's 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) Fetch ¶
Fetch satisfies pool.NntpClient. It returns either the configured error or an ArticleBody filled with the configured Bytes after the configured latency, and when r.Writer is set writes the payload to it in ChunkSize pieces (one write when zero), pausing on TailGate after the first chunk when set and failing after the first chunk on the first call when FailAfterFirstChunk is set.
nntppool v5 folded lane and delivery mode into Req, so one method now covers what used to be Body, BodyPriority, BodyBackground, and BodyStreamPriority. The per-lane counters are kept and derived from the Req instead, so assertions that distinguish playback from import from repair traffic keep working.
func (*Client) FetchAsync ¶
FetchAsync mirrors Fetch on its own goroutine, yielding one BodyResult.
func (*Client) MaxInFlight ¶
MaxInFlight returns the high-water mark of InFlight observed since the client was created (or since the last ResetCounters).
func (*Client) PerMessageBodyCalls ¶
PerMessageBodyCalls returns how many times the message-ID was fetched, ignoring existence checks. Use it to pin retry behaviour when the code under test may also STAT the same article.
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) StatPriorityCalls ¶
StatPriorityCalls reports existence checks made on the priority lane. StatCalls counts those too, so "no STAT at all" assertions keep working.
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
// ChunkSize splits a streamed payload (writer-based calls) into writes of
// this many bytes; zero writes the whole payload at once.
ChunkSize int
// TailGate, when non-nil, is waited on after the first chunk is written
// and before the rest, so a test can prove bytes were served while the
// article was provably still arriving. Close it to release the tail.
TailGate <-chan struct{}
// FailAfterFirstChunk makes the first call to this message-ID write one
// chunk and then return FailErr, modelling a connection lost mid-article.
// Later calls serve the payload normally.
FailAfterFirstChunk bool
}
SegmentBehavior describes how the fake should respond to a single message-ID. The zero value returns an empty body with no delay.