polling

package
v1.6.0 Latest Latest
Warning

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

Go to latest
Published: May 7, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package polling iterates an HTTP endpoint on an interval, reusing the supplied *http.Client so the configured transport stack (RateLimit, Throttle, Retry, oauth2, ETag) applies per attempt.

Two entry points:

  • Poll yields *http.Response per attempt. The caller drains and closes each body. WithDone is header/status-only; the body is consumed by the time the predicate runs.
  • As[T] decodes each response into T (default JSON) and yields the typed value. The iterator owns and closes each body. Use WithDoneT to stop on the decoded value; use WithDecode to swap the decoder.

Stop conditions: ctx cancel, predicate match, WithMaxAttempts hit, WithMaxWallClock expiry, transport error, predicate panic. On MaxAttempts and MaxWallClock the iterator yields (lastResp, sentinel) so the caller can inspect headers/status of what triggered the limit; lastResp.Body has already been drained and closed by the caller's prior normal-yield iteration (per Poll's body-ownership contract). lastResp may be nil if WithChangeOnly suppressed every yield. ErrMaxWallClockExceeded wraps context.DeadlineExceeded.

Sharp edges:

  • Each c.Do may itself loop through retry.Transport (default 3 attempts, 200ms-2s jitter). When polling owns the outer loop, pass retry.WithMaxAttempts(1) on the client.
  • throttle.WithRequestsPerSecond below 1/interval dominates cadence; effective_interval = max(interval, 1/rps).
  • With WithETagCache and an unchanged resource, As[T] decodes identical bytes per tick. WithChangeOnly silently skips those.
  • gofri ctx-cancel during a secondary cooldown is observed on the next inner round-trip, not synchronously.

Determinism: production uses time.NewTimer + select-on-ctx. Tests inject WithSleepFunc / WithNowFunc. Jitter is a deterministic mid-point clamp; not applied when honoring Retry-After.

Body argument: the body []byte passed to Poll/As is not deep-copied; the iterator constructs a fresh bytes.NewReader(body) per attempt that aliases the slice. Do not mutate the slice while polling.

Retry-After interaction with WithMaxWallClock: an honored Retry-After exceeding the remaining wall-clock budget is truncated by the ctx deadline and surfaces as ErrMaxWallClockExceeded.

WithNowFunc and ctx.Deadline: WithNowFunc affects the value passed to context.WithDeadline at construction (now() + MaxWallClock); the resulting deadline still fires on real wall-clock. A frozen-past nowFunc therefore cancels immediately.

Slog field allowlist (polling_predicate_panic):

  • event: "polling_predicate_panic"
  • panic_type: fmt.Sprintf("%T", recovered) (type name, no payload)

No request/response headers are emitted to slog; the panic event echoes only the recovered value's Go type name.

Index

Constants

This section is empty.

Variables

View Source
var ErrInvalidInterval = errors.New("polling: interval must be > 0")

ErrInvalidInterval is returned when interval <= 0.

View Source
var ErrInvalidOption = errors.New("polling: invalid option")

ErrInvalidOption flags malformed Option values (nil seam, n < 1, generic T mismatch).

View Source
var ErrMaxAttemptsExceeded = errors.New("polling: max attempts exceeded")

ErrMaxAttemptsExceeded is the boundary sentinel for WithMaxAttempts.

View Source
var ErrMaxWallClockExceeded = fmt.Errorf("polling: max wall-clock exceeded: %w", context.DeadlineExceeded)

ErrMaxWallClockExceeded is the boundary sentinel for WithMaxWallClock; wraps context.DeadlineExceeded.

View Source
var ErrPredicatePanic = errors.New("polling: predicate panicked")

ErrPredicatePanic is yielded when WithDone or WithDoneT panics.

Functions

func As

func As[T any](
	ctx context.Context,
	c *http.Client,
	method, url string,
	headers http.Header,
	body []byte,
	interval time.Duration,
	opts ...Option,
) iter.Seq2[T, error]

As decodes each polling response into T (default: encoding/json). The iterator owns and closes each body via defer; decode errors yield once and stop, decoder panics propagate with the body closed. Use WithDoneT to stop on the decoded value.

func Poll

func Poll(
	ctx context.Context,
	c *http.Client,
	method, url string,
	headers http.Header,
	body []byte,
	interval time.Duration,
	opts ...Option,
) iter.Seq2[*http.Response, error]

Poll iterates an HTTP endpoint on interval, reusing the supplied *http.Client so the transport stack applies per attempt. The caller drains and closes each yielded body. See package doc for stop conditions and sharp edges.

Types

type Option

type Option func(*config)

Option configures a polling iterator.

func WithChangeOnly

func WithChangeOnly() Option

WithChangeOnly skips iterations where the etag layer signals a cache hit. No-op when the etag layer is not in the chain.

func WithDecode

func WithDecode[T any](decode func(*http.Response) (T, error)) Option

WithDecode overrides the default JSON decoder on As[T]. T must match the type parameter on As[T]; mismatches surface as ErrInvalidOption at the first iteration.

func WithDone

func WithDone(predicate func(*http.Response) bool) Option

WithDone stops the iterator when the predicate returns true. Runs after the caller's range body, so by then the body is consumed and closed; the predicate must inspect status and headers only. For body-aware stop logic, use As[T] with WithDoneT.

func WithDoneT

func WithDoneT[T any](predicate func(T) bool) Option

WithDoneT stops As[T] when the predicate returns true. T must match the type parameter on As[T]; mismatches surface as ErrInvalidOption at the first iteration.

func WithHonorRetryAfter

func WithHonorRetryAfter(b bool) Option

WithHonorRetryAfter (default true) honors the upstream Retry-After header. The honored value is clamped to [interval, max(interval, MaxWallClock)]; with MaxWallClock=0 the upper bound collapses to interval, ignoring saturated upstream values.

func WithJitter

func WithJitter(frac float64) Option

WithJitter applies a deterministic mid-point jitter: interval + (interval * frac / 2), clamped to [interval/2, 3*interval/2]. Frac is clamped to [0, 1]. Not applied when honoring Retry-After.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger receives the polling_predicate_panic event. Default discards.

func WithMaxAttempts

func WithMaxAttempts(n int) Option

WithMaxAttempts caps total attempts (including the first). n must be >= 1; n=1 means "one attempt only". Omitting the option leaves attempts unbounded; pair with WithMaxWallClock plus WithDoneT or WithDone for safety on unbounded runs. Zero or negative n surfaces as ErrInvalidOption at construction.

Validation aligns with retry.WithMaxAttempts (both reject n < 1); the sentinel error type differs (polling.ErrInvalidOption vs retry.ErrInvalidMaxAttempts).

func WithMaxWallClock

func WithMaxWallClock(d time.Duration) Option

WithMaxWallClock derives a child context deadline. Zero is unbounded. On expiry the iterator yields (lastResp, ErrMaxWallClockExceeded), which wraps context.DeadlineExceeded.

func WithNowFunc

func WithNowFunc(f func() time.Time) Option

WithNowFunc is a test seam. Nil errors with ErrInvalidOption at construction.

func WithSleepFunc

func WithSleepFunc(f func(time.Duration)) Option

WithSleepFunc is a test seam. Nil errors with ErrInvalidOption at construction. Production uses time.NewTimer + select-on-ctx.

Jump to

Keyboard shortcuts

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